从0到1实现一个虚拟DOM
- Virtual DOM 是真实 DOM 的映射 
- 当虚拟 DOM 树中的某些节点改变时,会得到一个新的虚拟树。算法对这两棵树(新树和旧树)进行比较,找出差异,然后只需要在真实的 DOM 上做出相应的改变。 
用 JS 对象模拟 DOM 树
  
    
    
  
   
     
     
   <ul class="”list”">
   
     
     
    <li>item 1</li>
   
     
     
    <li>item 2</li>
   
     
     
   </ul>
  
    
    
    
  
    
    
  
   
     
     
   { type: ‘ul’, props: { ‘class’: ‘list’ }, children: [
   
     
     
    { type: ‘li’, props: {}, children: [‘item 1’] },
   
     
     
    { type: ‘li’, props: {}, children: [‘item 2’] }
   
     
     
   ] }
  
    
    
    
- 用如下对象表示 DOM 元素 
  
    
    
  
   
     
     
   { type: ‘…’, props: { … }, children: [ … ] }
  
    
    
    
- 用普通 JS 字符串表示 DOM 文本节点 
  
    
    
  
   
     
     
   function h(type, props, …children) {
   
     
     
    return { type, props, children };
   
     
     
   }
  
    
    
    
  
    
    
  
   
     
     
   h(‘ul’, { ‘class’: ‘list’ },
   
     
     
    h(‘li’, {}, ‘item 1’),
   
     
     
    h(‘li’, {}, ‘item 2’),
   
     
     
   );
  
    
    
    
  
    
    
  
   
     
     
   <ul className="”list”">
   
     
     
    <li>item 1</li>
   
     
     
    <li>item 2</li>
   
     
     
   </ul>
  
    
    
    
  
    
    
  
   
     
     
   React.createElement(‘ul’, { className: ‘list’ },
   
     
     
    React.createElement(‘li’, {}, ‘item 1’),
   
     
     
    React.createElement(‘li’, {}, ‘item 2’),
   
     
     
   );
  
    
    
    
h(...) 
  函数代替  
 React.createElement(…) 
 ,那么我们也能使用 JSX 语法。其实,只需要在源文件头部加上这么一句注释: 
  
    
    
  
   
     
     
   /** @jsx h */
   
     
     
   <ul className="”list”">
   
     
     
    <li>item 1</li>
   
     
     
    <li>item 2</li>
   
     
     
   </ul>
  
    
    
    
h(...) 
  函数代替  
 React.createElement(…) 
 ,然后 Babel 就开始编译。’ 
  
    
    
  
   
     
     
   /** @jsx h */ const a = (
   
     
     
   <ul className="”list”">
   
     
     
    <li>item 1</li>
   
     
     
    <li>item 2</li>
   
     
     
   </ul>
   
     
     
   );
  
    
    
    
  
    
    
  
   
     
     
   const a = (
   
     
     
    h(‘ul’, { className: ‘list’ },
   
     
     
    h(‘li’, {}, ‘item 1’),
   
     
     
    h(‘li’, {}, ‘item 2’),
   
     
     
    );
   
     
     
   );
  
    
    
    
“h” 
  执行时,它将返回普通 JS 对象-即我们的虚拟 DOM: 
  
    
    
  
   
     
     
   const a = (
   
     
     
    { type: ‘ul’, props: { className: ‘list’ }, children: [
   
     
     
    { type: ‘li’, props: {}, children: [‘item 1’] },
   
     
     
    { type: ‘li’, props: {}, children: [‘item 2’] }
   
     
     
    ] }
   
     
     
   );
  
    
    
    
从 Virtual DOM 映射到真实 DOM
- 使用以’ - $‘开头的变量表示真正的 DOM 节点(元素,文本节点),因此 \$parent 将会是一个真实的 DOM 元素
- 虚拟 DOM 使用名为 - node的变量表示
createElement(…) 
 ,它将获取一个虚拟 DOM 节点并返回一个真实的 DOM 节点。这里先不考虑  
 props 
  和  
 children 
  属性: 
  
    
    
  
   
     
     
   function createElement(node) {
   
     
     
    if (typeof node === ‘string’) {
   
     
     
    return document.createTextNode(node);
   
     
     
    }
   
     
     
    return document.createElement(node.type);
   
     
     
   }
  
    
    
    
  
    
    
  
   
     
     
   { type: ‘…’, props: { … }, children: [ … ] }
   
     
     
   
  
createElement 
  传入虚拟文本节点和虚拟元素节点——这是可行的。 
appendChild() 
  添加到我们的元素中: 
  
    
    
  
   
     
     
   function createElement(node) {
   
     
     
    if (typeof node === ‘string’) {
   
     
     
    return document.createTextNode(node);
   
     
     
    }
   
     
     
    const $el = document.createElement(node.type);
   
     
     
    node.children
   
     
     
    .map(createElement)
   
     
     
    .forEach($el.appendChild.bind($el));
   
     
     
    return $el;
   
     
     
   }
  
    
    
    
props 
  属性放到一边。待会再谈。我们不需要它们来理解虚拟 DOM 的基本概念,因为它们会增加复杂性。 
  
    
    
  
   
     
     
   /** @jsx h */
   
     
     
   
function h(type, props, ...children) {
   
     
     
    return { type, props, children };
   
     
     
   }
   
     
     
   
function createElement(node) {
   
     
     
    if (typeof node === "string") {
   
     
     
    return document.createTextNode(node);
   
     
     
    }
   
     
     
    const $el = document.createElement(node.type);
   
     
     
    node.children.map(createElement).forEach($el.appendChild.bind($el));
   
     
     
    return $el;
   
     
     
   }
   
     
     
   
const a = (
   
     
     
    <ul class="list">
   
     
     
    <li>item 1</li>
   
     
     
    <li>item 2</li>
   
     
     
    </ul>
   
     
     
   );
   
     
     
   
const $root = document.getElementById("root");
   
     
     
   $root.appendChild(createElement(a));
  
    
    
    
比较两棵虚拟 DOM 树的差异
- 添加新节点,使用 appendChild(…) 方法添加节点 
- 移除老节点,使用 removeChild(…) 方法移除老的节点 
- 节点的替换,使用 replaceChild(…) 方法 
$parent、newNode 和 oldNode,其中 \$parent 是虚拟节点的一个实际 DOM 元素的父元素。现在来看看如何处理上面描述的所有情况。 
添加新节点
  
    
    
  
   
     
     
   function updateElement($parent, newNode, oldNode) {
   
     
     
    if (!oldNode) {
   
     
     
    $parent.appendChild(createElement(newNode));
   
     
     
    }
   
     
     
   }
  
    
    
    
移除老节点
$parent.removeChild(…) 
  方法把变化映射到真实的 DOM 上。但前提是我们得知道我们的节点在父元素上的索引,我们才能通过 \$parent.childNodes[index] 得到该节点的引用。 
  
    
    
  
   
     
     
   function updateElement($parent, newNode, oldNode, index = 0) {
   
     
     
    if (!oldNode) {
   
     
     
    $parent.appendChild(createElement(newNode));
   
     
     
    } else if (!newNode) {
   
     
     
    $parent.removeChild($parent.childNodes[index]);
   
     
     
    }
   
     
     
   }
  
    
    
    
节点的替换
  
    
    
  
   
     
     
   function changed(node1, node2) {
   
     
     
    return typeof node1 !== typeof node2 ||
   
     
     
    typeof node1 === ‘string’ && node1 !== node2 ||
   
     
     
    node1.type !== node2.type
   
     
     
   }
  
    
    
    
  
    
    
  
   
     
     
   function updateElement($parent, newNode, oldNode, index = 0) {
   
     
     
    if (!oldNode) {
   
     
     
    $parent.appendChild(createElement(newNode));
   
     
     
    } else if (!newNode) {
   
     
     
    $parent.removeChild($parent.childNodes[index]);
   
     
     
    } else if (changed(newNode, oldNode)) {
   
     
     
    $parent.replaceChild(createElement(newNode), $parent.childNodes[index]);
   
     
     
    }
   
     
     
   }
  
    
    
    
比较子节点
- 当节点是 DOM 元素时我们才需要比较( 文本节点没有子节点 ) 
- 我们需要传递当前的节点的引用作为父节点 
- 我们应该一个一个的比较所有的子节点,即使它是 - undefined也没有关系,我们的函数也会正确处理它。
- 最后是 index,它是子数组中子节点的 index 
  
    
    
  
   
     
     
   function updateElement($parent, newNode, oldNode, index = 0) {
   
     
     
    if (!oldNode) {
   
     
     
    $parent.appendChild(createElement(newNode));
   
     
     
    } else if (!newNode) {
   
     
     
    $parent.removeChild($parent.childNodes[index]);
   
     
     
    } else if (changed(newNode, oldNode)) {
   
     
     
    $parent.replaceChild(createElement(newNode), $parent.childNodes[index]);
   
     
     
    } else if (newNode.type) {
   
     
     
    const newLength = newNode.children.length;
   
     
     
    const oldLength = oldNode.children.length;
   
     
     
    for (let i = 0; i < newLength || i < oldLength; i++) {
   
     
     
    updateElement(
   
     
     
    $parent.childNodes[index],
   
     
     
    newNode.children[i],
   
     
     
    oldNode.children[i],
   
     
     
    i
   
     
     
    );
   
     
     
    }
   
     
     
    }
   
     
     
   }
  
    
    
    
完整的代码
 
/*_ @jsx h_ /
  
    
    
  
   
     
     
   function h(type, props, ...children) {
   
     
     
    return { type, props, children };
   
     
     
   }
   
     
     
   
function createElement(node) {
   
     
     
    if (typeof node === "string") {
   
     
     
    return document.createTextNode(node);
   
     
     
    }
   
     
     
    const $el = document.createElement(node.type);
   
     
     
    node.children.map(createElement).forEach($el.appendChild.bind($el));
   
     
     
    return $el;
   
     
     
   }
   
     
     
   
function changed(node1, node2) {
   
     
     
    return (
   
     
     
    typeof node1 !== typeof node2 ||
   
     
     
    (typeof node1 === "string" && node1 !== node2) ||
   
     
     
    node1.type !== node2.type
   
     
     
    );
   
     
     
   }
   
     
     
   
function updateElement($parent, newNode, oldNode, index = 0) {
   
     
     
    if (!oldNode) {
   
     
     
    $parent.appendChild(createElement(newNode));
   
     
     
    } else if (!newNode) {
   
     
     
    $parent.removeChild($parent.childNodes[index]);
   
     
     
    } else if (changed(newNode, oldNode)) {
   
     
     
    $parent.replaceChild(createElement(newNode), $parent.childNodes[index]);
   
     
     
    } else if (newNode.type) {
   
     
     
    const newLength = newNode.children.length;
   
     
     
    const oldLength = oldNode.children.length;
   
     
     
    for (let i = 0; i < newLength || i < oldLength; i++) {
   
     
     
    updateElement(
   
     
     
    $parent.childNodes[index],
   
     
     
    newNode.children[i],
   
     
     
    oldNode.children[i],
   
     
     
    i
   
     
     
    );
   
     
     
    }
   
     
     
    }
   
     
     
   }
   
     
     
   
// ---------------------------------------------------------------------
   
     
     
   
const a = (
   
     
     
    <ul>
   
     
     
    <li>item 1</li>
   
     
     
    <li>item 2</li>
   
     
     
    </ul>
   
     
     
   );
   
     
     
   
const b = (
   
     
     
    <ul>
   
     
     
    <li>item 1</li>
   
     
     
    <li>hello!</li>
   
     
     
    </ul>
   
     
     
   );
   
     
     
   
const $root = document.getElementById("root");
   
     
     
   const $reload = document.getElementById("reload");
   
     
     
   
updateElement($root, a);
   
     
     
   $reload.addEventListener("click", () => {
   
     
     
    updateElement($root, b, a);
   
     
     
   });
  
    
    
    
  
    
    
  
   
     
     
   <button id="reload">RELOAD</button>
   
     
     
   <div id="root"></div>
  
    
    
    
  
    
    
  
   
     
     
   #root {
   
     
     
    border: 1px solid black;
   
     
     
    padding: 10px;
   
     
     
    margin: 30px 0 0 0;
   
     
     
   }
  
    
    
    
总结
- 设置元素属性(props)并进行 diffing/updating 
- 处理事件——向元素中添加事件监听 
- 让虚拟 DOM 与组件一起工作,比如 React 
- 获取对实际 DOM 节点的引用 
- 使用带有库的虚拟 DOM,这些库可以直接改变真实的 DOM,比如 jQuery 及其插件 
