從0到1實(shí)現(xiàn)一個(gè)虛擬DOM

Virtual DOM 是真實(shí) DOM 的映射
當(dāng)虛擬 DOM 樹中的某些節(jié)點(diǎn)改變時(shí),會(huì)得到一個(gè)新的虛擬樹。算法對(duì)這兩棵樹(新樹和舊樹)進(jìn)行比較,找出差異,然后只需要在真實(shí)的 DOM 上做出相應(yīng)的改變。
用 JS 對(duì)象模擬 DOM 樹
<ul class="”list”"><li>item 1li><li>item 2li>ul>
{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [{ type: ‘li’, props: {}, children: [‘item 1’] },{ type: ‘li’, props: {}, children: [‘item 2’] }] }
用如下對(duì)象表示 DOM 元素
{ type: ‘…’, props: { … }, children: [ … ] }
用普通 JS 字符串表示 DOM 文本節(jié)點(diǎn)
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 1li><li>item 2li>ul>
React.createElement(‘ul’, { className: ‘list’ },React.createElement(‘li’, {}, ‘item 1’),React.createElement(‘li’, {}, ‘item 2’),);
h(...)?函數(shù)代替?React.createElement(…),那么我們也能使用 JSX 語(yǔ)法。其實(shí),只需要在源文件頭部加上這么一句注釋:/** @jsx h */<ul className="”list”"><li>item 1li><li>item 2li>ul>
h(...)?函數(shù)代替?React.createElement(…),然后?Babel?就開始編譯?!?/span>/** @jsx h */ const a = (<ul className="”list”"><li>item 1li><li>item 2li>ul>);
const a = (h(‘ul’, { className: ‘list’ },h(‘li’, {}, ‘item 1’),h(‘li’, {}, ‘item 2’),););
“h”?執(zhí)行時(shí),它將返回普通 JS 對(duì)象-即我們的虛擬 DOM:const a = ({ type: ‘ul’, props: { className: ‘list’ }, children: [{ type: ‘li’, props: {}, children: [‘item 1’] },{ type: ‘li’, props: {}, children: [‘item 2’] }] });
從 Virtual DOM 映射到真實(shí) DOM
使用以’?
$?‘開頭的變量表示真正的 DOM 節(jié)點(diǎn)(元素,文本節(jié)點(diǎn)),因此 \$parent 將會(huì)是一個(gè)真實(shí)的 DOM 元素虛擬 DOM 使用名為?
node?的變量表示
createElement(…),它將獲取一個(gè)虛擬 DOM 節(jié)點(diǎn)并返回一個(gè)真實(shí)的 DOM 節(jié)點(diǎn)。這里先不考慮?props?和?children?屬性:function createElement(node) {if (typeof node === ‘string’) {return document.createTextNode(node);}return document.createElement(node.type);}
{ type: ‘…’, props: { … }, children: [ … ] }
createElement?傳入虛擬文本節(jié)點(diǎn)和虛擬元素節(jié)點(diǎn)——這是可行的。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?屬性放到一邊。待會(huì)再談。我們不需要它們來(lái)理解虛擬 DOM 的基本概念,因?yàn)樗鼈儠?huì)增加復(fù)雜性。/** @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 1li><li>item 2li>ul>);const $root = document.getElementById("root");$root.appendChild(createElement(a));
比較兩棵虛擬 DOM 樹的差異
添加新節(jié)點(diǎn),使用?appendChild(…)?方法添加節(jié)點(diǎn)

移除老節(jié)點(diǎn),使用?removeChild(…)?方法移除老的節(jié)點(diǎn)

節(jié)點(diǎn)的替換,使用?replaceChild(…)?方法


$parent、newNode?和?oldNode,其中?\$parent?是虛擬節(jié)點(diǎn)的一個(gè)實(shí)際 DOM 元素的父元素?,F(xiàn)在來(lái)看看如何處理上面描述的所有情況。添加新節(jié)點(diǎn)
function?updateElement($parent,?newNode,?oldNode)?{if (!oldNode) {$parent.appendChild(createElement(newNode));}}
移除老節(jié)點(diǎn)
$parent.removeChild(…)?方法把變化映射到真實(shí)的 DOM 上。但前提是我們得知道我們的節(jié)點(diǎn)在父元素上的索引,我們才能通過(guò)?\$parent.childNodes[index]?得到該節(jié)點(diǎn)的引用。function updateElement($parent, newNode, oldNode, index = 0) {if (!oldNode) {$parent.appendChild(createElement(newNode));} else if (!newNode) {$parent.removeChild($parent.childNodes[index]);}}
節(jié)點(diǎn)的替換
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]);}}
比較子節(jié)點(diǎn)
當(dāng)節(jié)點(diǎn)是 DOM 元素時(shí)我們才需要比較( 文本節(jié)點(diǎn)沒(méi)有子節(jié)點(diǎn) )
我們需要傳遞當(dāng)前的節(jié)點(diǎn)的引用作為父節(jié)點(diǎn)
我們應(yīng)該一個(gè)一個(gè)的比較所有的子節(jié)點(diǎn),即使它是?
undefined?也沒(méi)有關(guān)系,我們的函數(shù)也會(huì)正確處理它。最后是?index,它是子數(shù)組中子節(jié)點(diǎn)的 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 1li><li>item 2li>ul>);const b = (<ul><li>item 1li><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">RELOADbutton><div id="root">div>
#root {border: 1px solid black;padding: 10px;margin: 30px 0 0 0;}

總結(jié)
設(shè)置元素屬性(props)并進(jìn)行 diffing/updating
處理事件——向元素中添加事件監(jiān)聽
讓虛擬 DOM 與組件一起工作,比如 React
獲取對(duì)實(shí)際 DOM 節(jié)點(diǎn)的引用
使用帶有庫(kù)的虛擬 DOM,這些庫(kù)可以直接改變真實(shí)的 DOM,比如 jQuery 及其插件

