这不是什么难事,一般来说没必要动用虚拟 DOM。
今年早些时候我写了一篇文章,声称Web组件最终将取代前端框架。
这篇文章引起了很多争议,这大大出乎我的意料,但也让我收获良多。有很多人同意我的观点,也有很多人持否定态度,甚至有人觉得我根本就是脑子进水,应该永远禁止我再写代码了。总的来说,争论的双方都提出了很不错的观点。
批评声音主要指出现有的框架提供了一种通过数据绑定编写视图的声明式途径,这是原生 Web 组件天生不具备的能力。这一观点本身没错,但其实 Web 组件是很容易实现数据绑定的,我将在本文中演示具体做法。
声明式数据绑定的情况
数据绑定最早是被 Angular、Backbone 和 Ember 等框架推广而流行开来的,现在则在某种程度上是编写视图的标准途径。它能让“视图作为数据的函数”,意味着每当某些数据发生变化时,相关视图将“自动”更新。
不需要冗长的 DOM 操作来保持数据和视图同步,只需更新数据,视图就会随之变化。这是一项杀手级功能,如今但凡理智的开发人员就会用它。所以很容易理解为什么开发人员会使用提供了数据绑定功能的框架,就算框架对于应用来说太大材小用也无所谓:既然框架打理好了一切,何必要费心费力处理那些麻烦的 DOM 操作呢?
但数据绑定并不是什么魔法,你用不着为了用它而动用整个框架。在 Web 组件里只需要几行代码就能轻松搞定数据绑定了,没什么特殊的。
如何实现
就像我上面说的那样,数据绑定并没有那么神奇。当基础数据发生变化时,你的视图不是凭空“神奇”地更新的。在框架深处,不为人知的某个角落里的设置代码负责在数据更改时调用并更新视图。
AngularJS 使用了所谓的“摘要循环”:这是一种粗暴的检查机制,不断检查哪些数据已更改,以便随时更新对应的视图。
React 面世时提供了另一种据称性能更好的解决方案,称为虚拟 DOM:它是一种 DOM 的 JavaScript 表示,只更新已更改的 DOM 部分。这对列表来说很合适——列表的少数项目发生变化时无需重新渲染整个列表,只需更新已更改的项目。
哪种工具最合适?
这对于具有复杂 UI 的较大应用程序来说非常有用,但对于大多数应用程序来说实在有些杀鸡用牛刀了。编写一些监视数据的代码并在数据发生变化时更新相关视图并不是什么难事。问题是这些数据通常需要传递给也需要数据绑定的子组件,所以最后往往会有很多 DOM 操作。
你需要的是一种在数据被推送到子组件时触发子组件中相同的数据绑定操作的方法。只要父组件的数据发生更改并且某些数据绑定到子组件的视图,该子组件的视图也需要更新。
一种方法是利用所有组件的基类,因为 Web 组件是使用 JavaScript 类创建的,所以这是一个很好的选择。默认情况下 Web 组件扩展 HTMLElement,但我们也可以创建自己的基类来扩展 HTMLElement,如下所示:
然后我们创建的每个 Web 组件都扩展了这个 CustomElement 基类:
如果你想直接查看代码,可以访问Github链接。
我们将 CustomElement 的内部 state 属性绑定到视图来实现数据绑定:
我的第一个想法是将 this.state 实现为 Proxy,这样 state 对象的任何突变都将被自动拦截;但由于 Proxy 可能会影响性能,因此我决定实现一个 setter,它还能同时设置多个属性:
setState 方法遍历 newState 对象的所有条目,并将所有值设置为 this.state 上的对应属性,随后我们就应该使用这些值来更新视图。
通过标准 data 属性将值绑定到视图上,在本例中为 data-bind:
这里的 textContent 绑定到负责管理视图的组件内的 this.state.title 的值上:
这种绑定可以达到任意深度,所以下面这种情况也能做到:
还可以将数据绑定到 Web 组件的特定属性。在此示例中,数据绑定到的 title 属性上:
视图的更新实际是在 CustomElement 中的 updateBindings 方法中实现的。通过 setState 方法更新 state 时,它会解析更新的属性以查找绑定到这些属性的 HTML 元素。
例如下面这样:
更新 Web 组件内的 this.state.user.address.city,并将数据对象中的键转换为 user.address.city,然后使用它来查找这个数据绑定的元素:
这将查找所有的 data-bind 属性以 user.address.city 结尾的元素(注意 data-bind),因此它将找到 data-bind=“user.address.city”,但也可以找到 data-bind=“ name:user.address.city“,其中数据专门绑定到 name 属性。
每当数据绑定到元素的特定属性(如 data-bind=“name:user.address.city”)时,组件将检查该元素是否也是扩展 CustomElement 的 Web 组件;如果是,则通过它的 setState 方法更新该属性。这样,数据绑定就能一直传播到所有子组件上。
如果绑定数据的元素是常规 HTML 元素,那么将简单地更新其 textContent。在这两种情况下,只需几行代码即可有效实现 DOM 的更新。
列表该怎么处理?
像虚拟 DOM 这样的解决方案真正的用武之地是渲染列表。例如只更改列表的一部分时,虚拟 DOM 将仅更新已更改的部分,而不是重新渲染整个列表。
其机制是在第一次渲染列表时创建 DOM 节点,然后在列表更改时只更新这些现有节点(textContent、属性等)。复用已经创建的节点比重新渲染整个列表重建所有节点的开销小得多,因此对于非常大的列表来说这种方法效率更高。
但如果你的列表平均只有 25 个项目时,你可能会想知道重新渲染整个列表能比虚拟 DOM 的方法慢多少。当你渲染 250 个项目时可能会变得很慢,但理智的开发人员在这种情况下就应该分页了。
我不是说大家就应该抛弃虚拟 DOM,因为它的确是很棒的技术。如果遇到真正需要虚拟 DOM 的情况,自然一定要使用它。我只是希望大家遇到比较轻量的问题时先思考一下能不能找到一个比较轻量的解决方案,而不是上来就动用重量级的杀手锏。
customElement 演示的Github仓库包含一个 Web 组件,它在 li 标签内渲染任何设置为其 items 属性的字符串数组。只要将 items 设置为新数组它就会重新渲染整个列表,但是复用现有的 li 标记也很简单。
结论
答案很清楚了,只需几行代码即可对 Web 组件进行声明式数据绑定。我觉得自己已经清楚地证明了数据绑定很容易实现,并且你不需要动用整个框架也能使用它。
上面提到的 Github 仓库中的代码不是 React 或 Vue.js 等框架的替代品,本来也没这个意思。框架提供的并不只有数据绑定,本文和涉及的代码是为了证明你不一定需要一个框架来实现声明式数据绑定。
除了数据绑定之外,customElement 还提供了一些方便的方法来选择元素以及显示和隐藏元素。
我请大家好好看看我的代码,仔细研究一下,能给我反馈的话我会感激不尽!
评论