Obeta

VirtualDOM

我们已经听说过很多次Virtual DOM了,只是知道这个东西可以让我们页面更新更快速更有效率.

话说关于 DOM 的还有Shadow DOM,先立个 flag,下次写关于Shadow DOM的文.

为什么需要 Virtual DOM

在深入了解之前我只知道是能加快页面更新速度,响应的更快,减少浏览器重新绘制等一系列昂贵耗时的操作,但是实际上内部做了哪些以及如何实现的都不是很清楚.

在了解Virtual DOM之前需要温习一下DOM,DOM是文档对象模型,因此浏览器提供的是一个类似对象的东西,还有提供相应的 API 给我们操作这个对象.

举个简单的例子:

<!DOCTYPE html>
<html>
	<head></head>
	<body>
		<ul class="list">
			<li class="list-item">list-one</li>
		</ul>
	</body>
</html>

DOM树(DOM Tree)可以如下表示:

html └──head └──body └──ul class="list" └──li class="list-item" └──"list-one"

再使用 DOM 提供的 API 来修改 DOM:

  • "list-one" -> "list-one-item"
  • 增加一个 li
const listItemOne = document.getElementsByClassName('list-item')[0];
listItemOne.textContent = 'list-one-item';

const list = document.getElementsByClassName('list')[0];
const listItemTwo = document.createElement('li');
listItemTwo.classList.add('list-item');
listItemTwo.textContent = 'list-two-item';
list.appendChild(listItemTwo);

在互联网早期,w3c 规范也没有考虑到这种情况,毕竟那时候没有那么频繁更新 DOM 的需求,因此问题不大.

但是在如今,频繁刷新修改 DOM 节点是很常见的一种需求,比如document.getElementsByClassName()这样的简单方法小规模使用还是可以的,但是如果频繁刷新和修改 DOM 节点,每隔几秒钟更新页面上的多个元素,那么不断查询和更新 DOM 的操作会变得非常昂贵(消耗大量性能导致回流和重绘).

更进一步了解,由于 DOM 的相关 API 的设置方式,执行更昂贵的操作通常比查找和更新特定元素更简单,在某些方面,用新列表替换整个无序列表比修改特定元素更容易.

const list = document.getElementsByClassName('list')[0];
list.innerHTML = `
<li class="list-item">list-one-item</li>
<li class="list-item">list-two-item</li>
`;

同样的,针对某种问题不能抛开量的维度来讲影响,因此在这个特定的例子中,这些方法之间的性能差异可能微不足道.然而随着对 API 的使用量的增长,这种更新 DOM 的操作会变成你页面性能的瓶颈.

来看看 Virtual DOM

创建Virtual DOM是为了解决这些需要以更高性能的方式频繁更新 DOM 的问题.与 DOM 和Shadow DOm不同,Virtual DOM并不是官方规范,而是一种提供与实际 DOM 接口的接近的新方法.

Virtual DOM并不神秘,可以简单的描述它的工作:Virtual DOM可以被认为是原始 DOM 的副本.无需使用 DOM APIs,就可以频繁地操作和更新这个副本.一旦对Virtual DOM进行了所有更新,我们就可以查看需要对原始 DOM 进行哪些特定的更改,并以有针对性和优化的方式进行更改.

那么 Virtual DOM 是什么样子?

我们可以用 js 或者 json 方式来对DOM Tree进行描述,这就是Virtual DOM:

const vdom = {
	tagName: 'html',
	children: [
		{ tagName: 'head' },
		{
			tagName: 'body',
			children: [
				{
					tagName: 'ul',
					attributes: { class: 'list' },
					children: [
						{
							tagName: 'li',
							attributes: { class: 'list-item' },
							textContent: 'list-one',
						}, // end li
					],
				}, // end ul
			],
		}, // end body
	],
}; // end html

我们可以把这个对象看作我们的Virtual DOM.像原始 DOM 一样,它是我们 HTML 文档的基于对象的表示.但是由于它是一个简单的 Javascript 对象,我们可以自由频繁地操作它,而不需要接触实际的 DOM,直到我们需要更新真实的 DOM 为止.

与其对整个对象使用一个对象,更常见的是使用Virtual DOM的小部分.例如,我们可以处理一个列表组件,它将对应于我们无序的列表元素:

const list = {
	tagName: 'ul',
	attributes: { class: 'list' },
	children: [
		{
			tagName: 'li',
			attributes: { class: 'list-item' },
			textContent: 'list-one',
		},
	],
};

如何使用

现在我们已经看到了Virtual DOM的样子,它如何解决 DOM 的性能和可用性问题?

正如我所提到的,我们可以使用Virtual DOM来挑选出需要对 DOM 进行的特定更改,并单独进行这些特定更新.还是以上面那个例子使用 DOM API 进行相同的更改.

我们要做的第一件事是制作Virtual DOM的副本,其中包含我们想要进行的更改.由于我们不需要使用 DOM API,因此我们实际上只需创建一个新对象.

const originDom = {
	tagName: 'ul',
	attributes: { class: 'list' },
	children: [
		{
			tagName: 'li',
			attributes: { class: 'list-item' },
			textContent: 'list-one',
		},
		{
			tagName: 'li',
			attributes: { class: 'list-item' },
			textContent: 'list-two',
		},
	],
};

const copyDom = JSON.parse(JSON.stringify(originDom));

在我们对copyDom做一些更新和修改后,让它与originDom做比较,得到一个diffs数组:

const diffs = [
  {
    newNode: { /* new version of list item one */ },
    oldNode: { /* original version of list item one */ },
    index: /* index of element in parent's list of child nodes */
  },
  {
    newNode: { /* list item two */ },
    index: { /* */ }
  }
]

此 diff 提供了有关如何更新实际 DOM 的说明.一旦收集了所有差异,我们就可以批量更改 DOM,只进行所需的更新.例如,我们可以循环遍历每个差异,并根据 diff 指定的内容添加新的子代或更新旧的子代.

const domElement = document.getElementsByClassName('list')[0];

diffs.forEach(diff => {
	const newElement = document.createElement(diff.newNode.tagName);
	/* Add attributes ... */

	if (diff.oldNode) {
		// If there is an old version, replace it with the new version
		domElement.replaceChild(diff.newNode, diff.index);
	} else {
		// If no old version exists, create a new node
		domElement.appendChild(diff.newNode);
	}
});

注意上面只是一个简化的版本,并没有涉及其他对一些特殊情况.

使用 Virtual DOM 的一些框架

ReactVue这类大型框架都是使用Virtual DOM来优化更新 DOM 结构的.具体就不说了,用过的基本都知道.

后面还可以使用WASM来计算 Dom Tree 不同的部分,提高 diff 效率,还可以使用 web worker 开启多线程支持,目前工作也遇不到太深入的操作需求,因此也就不太深入的了解了.

引用

个人随笔记录,内容不保证完全正确,若需要转载,请注明作者和出处.