几行代码就能完成Web组件的数据绑定

2019 年 9 月 05 日

几行代码就能完成Web组件的数据绑定

这不是什么难事,一般来说没必要动用虚拟 DOM。

今年早些时候我写了一篇文章,声称 Web 组件最终将取代前端框架

这篇文章引起了很多争议,这大大出乎我的意料,但也让我收获良多。有很多人同意我的观点,也有很多人持否定态度,甚至有人觉得我根本就是脑子进水,应该永远禁止我再写代码了。总的来说,争论的双方都提出了很不错的观点。

批评声音主要指出现有的框架提供了一种通过数据绑定编写视图的声明式途径,这是原生 Web 组件天生不具备的能力。这一观点本身没错,但其实 Web 组件是很容易实现数据绑定的,我将在本文中演示具体做法。

声明式数据绑定的情况

数据绑定最早是被 Angular、Backbone 和 Ember 等框架推广而流行开来的,现在则在某种程度上是编写视图的标准途径。它能让“视图作为数据的函数”,意味着每当某些数据发生变化时,相关视图将“自动”更新。

不需要冗长的 DOM 操作来保持数据和视图同步,只需更新数据,视图就会随之变化。这是一项杀手级功能,如今但凡理智的开发人员就会用它。所以很容易理解为什么开发人员会使用提供了数据绑定功能的框架,就算框架对于应用来说太大材小用也无所谓:既然框架打理好了一切,何必要费心费力处理那些麻烦的 DOM 操作呢?

但数据绑定并不是什么魔法,你用不着为了用它而动用整个框架。在 Web 组件里只需要几行代码就能轻松搞定数据绑定了,没什么特殊的。

如何实现

就像我上面说的那样,数据绑定并没有那么神奇。当基础数据发生变化时,你的视图不是凭空“神奇”地更新的。在框架深处,不为人知的某个角落里的设置代码负责在数据更改时调用并更新视图。

AngularJS 使用了所谓的“摘要循环”:这是一种粗暴的检查机制,不断检查哪些数据已更改,以便随时更新对应的视图。

React 面世时提供了另一种据称性能更好的解决方案,称为虚拟 DOM:它是一种 DOM 的 JavaScript 表示,只更新已更改的 DOM 部分。这对列表来说很合适——列表的少数项目发生变化时无需重新渲染整个列表,只需更新已更改的项目。

哪种工具最合适?

图源: https://xkcd.com/974/

这对于具有复杂 UI 的较大应用程序来说非常有用,但对于大多数应用程序来说实在有些杀鸡用牛刀了。编写一些监视数据的代码并在数据发生变化时更新相关视图并不是什么难事。问题是这些数据通常需要传递给也需要数据绑定的子组件,所以最后往往会有很多 DOM 操作。

你需要的是一种在数据被推送到子组件时触发子组件中相同的数据绑定操作的方法。只要父组件的数据发生更改并且某些数据绑定到子组件的视图,该子组件的视图也需要更新。

一种方法是利用所有组件的基类,因为 Web 组件是使用 JavaScript 类创建的,所以这是一个很好的选择。默认情况下 Web 组件扩展 HTMLElement,但我们也可以创建自己的基类来扩展 HTMLElement,如下所示:

复制代码
export class CustomElement extends HTMLElement

然后我们创建的每个 Web 组件都扩展了这个 CustomElement 基类:

复制代码
export class MyComponent extends CustomElement

如果你想直接查看代码,可以访问 Github 链接

我们将 CustomElement 的内部 state 属性绑定到视图来实现数据绑定:

复制代码
class CustomElement extends HTMLElement {
constructor() {
super();
this.state = {};
}
}

我的第一个想法是将 this.state 实现为 Proxy,这样 state 对象的任何突变都将被自动拦截;但由于 Proxy 可能会影响性能,因此我决定实现一个 setter,它还能同时设置多个属性:

复制代码
class CustomElement extends HTMLElement {
...
setState(newState) {
Object.entries(newState)
.forEach(([key, value]) => {
this.state[key] = this.isObject(this.state[key]) &&
this.isObject(value) ? {...this.state[key], ...value} : value;
});
}
}

setState 方法遍历 newState 对象的所有条目,并将所有值设置为 this.state 上的对应属性,随后我们就应该使用这些值来更新视图。

通过标准 data 属性将值绑定到视图上,在本例中为 data-bind:

复制代码
<p data-bind="title"></p>

这里的 textContent 绑定到负责管理视图的组件内的 this.state.title 的值上:

复制代码
class DemoElement extends CustomElement {
constructor() {
super();
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<p data-bind="title"></p>
`;
}
}
const element = document.querySelector('demo-element');
element.setState({title: 'Hello World'});
// the paragraph will now contain the text "Hello World"

这种绑定可以达到任意深度,所以下面这种情况也能做到:

复制代码
<p data-bind="user.address.city"></p>
element.setState({
user: {
address: {
city: 'Amsterdam'
}
}
});
---> <p>Amsterdam</p>

还可以将数据绑定到 Web 组件的特定属性。在此示例中,数据绑定到的 title 属性上:

复制代码
<parent-element>
<demo-element data-bind="title:name"></demo-element>
</parent-element>
parentElement.setState({name: 'foo'}); //demoElement.title === 'foo'

视图的更新实际是在 CustomElement 中的 updateBindings 方法中实现的。通过 setState 方法更新 state 时,它会解析更新的属性以查找绑定到这些属性的 HTML 元素。

例如下面这样:

复制代码
element.setState({
user: {
address: {
city: 'Amsterdam'
}
}
});

更新 Web 组件内的 this.state.user.address.city,并将数据对象中的键转换为 user.address.city,然后使用它来查找这个数据绑定的元素:

复制代码
const elements = this.shadowRoot.querySelectorAll('[data-bind$="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 还提供了一些方便的方法来选择元素以及显示和隐藏元素。

我请大家好好看看我的代码,仔细研究一下,能给我反馈的话我会感激不尽!

英文原文: https://medium.com/swlh/https-medium-com-drmoerkerke-data-binding-for-web-components-in-just-a-few-lines-of-code-33f0a46943b3

2019 年 9 月 05 日 11:12 6122

评论

发布
暂无评论
发现更多内容

架构师训练营-作业5

紫极

Python中的@staticmethod和@classmethod的区别

Young先生

Python classmethod staticmethod

Newbe.Claptrap 框架入门,第一步 —— 创建项目,实现简易购物车

newbe36524

Docker Reactive ASP.NET Core

重学 Java 设计模式:实战访问者模式「模拟家长与校长,对学生和老师的不同视角信息的访问场景」

小傅哥

设计模式 小傅哥 重构 代码优化 访问者模式

字节跳动面试经验分享,已拿 Offer!

伍陆柒

Java 面试 大厂

推荐一款Python开源库,技术人必备的造数据神器!

狂师

Python 开源 自动化 开发工具 开发数据

不是完成你学习的 KPI ,而是要形成指导你行动的 OKR

非著名程序员

学习方法 程序员 提升认知 知识管理 程序员成长

Rust是如何保障内存安全的

博文视点Broadview

读书笔记 rust

JVM中栈的frames详解

程序那些事

JVM 堆栈 性能调优 JIT GC

Node.js与二进制数据流

自然醒

JavaScript node.js 前端 二进制

ArrayList源码阅读

慌张而黑糖

ArrayList 源码阅读

聊聊Spring的IOC以及JVM的类加载

小隐乐乐

工厂方法模式

Leetao

Python 设计模式 工厂方法模式

​区块链技术的重要性

CECBC区块链专委会

关于计划的思考

zhongzhq

带你解析MySQL binlog

Simon

MySQL Binlog

redis系列之——分布式锁

诸葛小猿

Java redis 分布式 分布式锁

分布式缓存与消息队列

紫极

阿里四面你都知道吗?

java金融

Java 程序员 互联网 阿里 简历

浅析 VO、DTO、DO、PO 的概念、区别和用处!

Java小咖秀

学习 设计模式 模型 经验分享

Python类中的__new__和__init__的区别

Young先生

Python __init__ __new__

Redis进阶篇三——主从复制

多选参数

redis redis高可用 redis6.0.0 Redis项目

猿灯塔:spring Boot Starter开发及源码刨析(二)

猿灯塔

Java 猿灯塔 源码刨析

这样的二维码,你见过吗?

诸葛小猿

Java Python 后端开发 二维码 myqr

typora设置图片自动上传,实现快速发文章

诸葛小猿

Typora PicGo gitee 上传图片

数据库分片

Arthur

Scala中如何优雅地实现break操作

吴慧民

scala

kubernetes集群安装(二进制)

小小文

Kubernetes 容器 容器技术

Tomcat8.5源码构建

知春秋

tomcat tomcat构建 tomcat源码解读 tomcat剖析

架构师训练营第六周总结

Melo

极客大学架构师训练营

Linux 进程必知必会

cxuan

Linux 操作系统

几行代码就能完成Web组件的数据绑定-InfoQ