来源文章如何理解虚拟DOM?
为什么虚拟 dom 会提高性能?
虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能。
用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了。
javascript对象与DOM树
真正的DOM元素是非常庞大,属性之多,操作它们可能会导致页面重排。如下把简单的div的属性都打印出来
相对于DOM对象,原生的javascript对象处理起来更快,而且简单。DOM树上的结构、属性信息我们都可以很容易的用javascript对象表示出来。
var element = {
tagName: ul,
props: {
id: 'list',
},
children: [
{tagName: 'li', props: {class: 'item', children: ['item 1']}},
{tagName: 'li', props: {class: 'item', children: ['item 2']}},
{tagName: 'li', props: {class: 'item', children: ['item 3']}},
]
}
上面对应的HTML写法是:
<ul id="list">
<li class="item">item 1</li>
<li class="item">item 2</li>
<li class="item">item 3</li>
</ul>
既然真正的DOM树的信息都可以用javascript对象来表示,反过来,你就可以根据这个用javascript对象表示的树结构来构建一颗真正的DOM树。
Virtual DOM算法步骤
Virtual DOM算法的步骤:
- 1、用js对象结构表示DOM树结构;然后用这个树构建一个真正的DOM树,插入到文档中
- 2、当状态变更的时候,重新构造一颗新的js对象树。然后用新的树和旧的树进行比较,记录两颗树差异
- 3、把2步骤所记录的差异应用到步骤1构建的真正的DOM树上,视图就更新了
Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然DOM这么慢,我们就在它们JS和DOM之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。
Virtual DOM算法的实现
步骤1:用js对象模拟DOM树
用js来表示一个DOM节点是很简单的事情,你只需要记录它的节点类型、属性,还有子节点:
element.js
function Element(tagName, props, children){
this.tagName = tagName;
this.props = props;
this.children = children;
}
module.exports = function(tagName, props, children){
return new Element(tagName, props, children);
}
例如上面的DOM结构就可以简单的表示:
var el = require('./element');
var ul = el(
'ul',
{id: 'list'},
[
el('li', {class: 'item'}, ['item 1']),
el('li', {class: 'item'}, ['item 2']),
el('li', {class: 'item'}, ['item 3']),
])
现在ul只是一个js对象表示的DOM结构,页面上还没有这个结构,我们可以根据这个ul构建真正的ul:
//渲染真正的DOM
Element.prototype.render = function(){
var el = document.createElement(this.tagName);
var props = this.props || {};
for(let propName in props){
var propValue = props[propName];
el.setAttribute(propName, propValue);
}
var children = this.children || [];
children.forEach(function(child){
var childEl = (child instanceof Element) ? child.render() : document.createTextNode(child);
el.appendChild(childEl);
});
return el;
}
render方法会根据tagName构建一个真正的DOM节点,然后设置这个节点的属性,最后递归的把自己的子节点构建起来,所以只需要:
var ulRoot = ul.render();
document.body.appendChild(ulRoot);
把ulRoot真正的DOM节点塞到文档中
步骤2:比较两颗虚拟DOM树的差异
比较两颗DOM树的差异是Virtual DOM算法最核心的部分,即Virtual DOM的diff算法。两颗树的完全的diff算法是一个时间复杂度为O(n^3)的问题。但是当中,你很少会跨越层级的移动DOM元素,所以Virtual DOM只会对同一个层级的元素进行对比。这样算法复杂度就可以到O(n)
在实际的代码中,会对新旧两颗树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:
每遍历到一个节点就把该节点和新的树进行对比,如果有差异就记录到一个对象里面步骤3:把差异应用到真正的DOM树上
因为步骤1所构建的js对象树和render出来真正的DOM树的信息、结构是一样的。所以我们可以对那颗DOM树也进行深度优先的遍历,遍历的时候从步骤2生成的js对象中找出当前遍历的节点差异,然后进行DOM操作
function patch(node, patches){
var walker = {index: 0};
dfsWalk(node, walker, patches);
}
function dfsWalk(node, walker, patches){
var currentPatches = patches[walker.index]; // 从patches拿出当前节点的差异
var len = node.childrenNodes ? node.childrenNodes.length : 0;
for (var i = 0; i < len; i++) {// 深度遍历子节点
var child = node.childrenNodes[i];
walker.index++;
dfsWalk(child, walker, patches);
}
if (currentPatches){
applyPatches(node, currentPatches); // 对当前节点进行DOM操作
}
}
applyPatches,根据不同类型的差异对当前节点进行DOM操作
function applyPatches(node, currentPatches){
currentPatches.forEach(currentPatch => {
switch (currentPatch.type) {
case REPLACE://替换
node.parentNode.replaceChild(currentPatch.node.render(), node); //新节点,旧节点
break;
case REORDER://重新排序
reorderChildren(node, currentPatch.moves);
break;
case PROPS:
setProps(node, currentPatch.props);
break;
case TEXT:
node.textContent = currentPatch.content;
break;
default:
throw new Error('Unknown patch type ' + currentPatch.type);
}
});
}
使用实例
Virtual DOM算法主要实现上面步骤的三个函数:element、diff、patch
// 1. 构建虚拟DOM
var tree = el('div', {'id': 'container'}, [
el('h1', {style: 'color: blue'}, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li')])
])
// 2. 通过虚拟DOM构建真正的DOM
var root = tree.render()
document.body.appendChild(root)
// 3. 生成新的虚拟DOM
var newTree = el('div', {'id': 'container'}, [
el('h1', {style: 'color: red'}, ['simple virtal dom']),
el('p', ['Hello, virtual-dom']),
el('ul', [el('li'), el('li')])
]);
// 4. 比较两棵虚拟DOM树的不同
var patches = diff(tree, newTree)
// 5. 在真正的DOM元素上应用变更
patch(root, patches)