话说关于 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 的一些框架
React
与Vue
这类大型框架都是使用Virtual DOM
来优化更新 DOM 结构的.具体就不说了,用过的基本都知道.
后面还可以使用WASM
来计算 Dom Tree 不同的部分,提高 diff 效率,还可以使用 web worker 开启多线程支持,目前工作也遇不到太深入的操作需求,因此也就不太深入的了解了.