步骤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);
步骤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)