步骤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:
//渲染真正的DOMElement.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. 构建虚拟DOMvar tree = el('div', {'id': 'container'}, [el('h1', {style: 'color: blue'}, ['simple virtal dom']),el('p', ['Hello, virtual-dom']),el('ul', [el('li')])])// 2. 通过虚拟DOM构建真正的DOMvar root = tree.render()document.body.appendChild(root)// 3. 生成新的虚拟DOMvar 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)
