虚拟dom_从0到1实现一个虚拟DOM
Virtual DOM 是真實 DOM 的映射
當(dāng)虛擬 DOM 樹中的某些節(jié)點(diǎn)改變時,會得到一個新的虛擬樹。算法對這兩棵樹(新樹和舊樹)進(jìn)行比較,找出差異,然后只需要在真實的 DOM 上做出相應(yīng)的改變。
用 JS 對象模擬 DOM 樹
首先,我們需要以某種方式將 DOM 樹存儲在內(nèi)存中。可以使用普通的 JS 對象來做。假設(shè)我們有這樣一棵樹:<ul class="”list”"> <li>item 1li> <li>item 2li>ul>看起來很簡單,對吧? 如何用 JS 對象來表示呢?{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [ { type: ‘li’, props: {}, children: [‘item 1’] }, { type: ‘li’, props: {}, children: [‘item 2’] }] }這里有兩件事需要注意:用如下對象表示 DOM 元素
用普通 JS 字符串表示 DOM 文本節(jié)點(diǎn)
從 Virtual DOM 映射到真實 DOM
好了,現(xiàn)在我們有了 DOM 樹,用普通的 JS 對象表示,還有我們自己的結(jié)構(gòu)。這很酷,但我們需要從它創(chuàng)建一個真正的 DOM。首先讓我們做一些假設(shè)并聲明一些術(shù)語:使用以’?$?‘開頭的變量表示真正的 DOM 節(jié)點(diǎn)(元素,文本節(jié)點(diǎn)),因此 \$parent 將會是一個真實的 DOM 元素
虛擬 DOM 使用名為?node?的變量表示
比較兩棵虛擬 DOM 樹的差異
現(xiàn)在我們可以將虛擬 DOM 轉(zhuǎn)換為真實的 DOM,這就需要考慮比較兩棵 DOM 樹的差異。基本的,我們需要一個算法來比較新的樹和舊的樹,它能夠讓我們知道什么地方改變了,然后相應(yīng)的去改變真實的 DOM。怎么比較 DOM 樹?需要處理下面的情況:添加新節(jié)點(diǎn),使用?appendChild(…)?方法添加節(jié)點(diǎn)
移除老節(jié)點(diǎn),使用?removeChild(…)?方法移除老的節(jié)點(diǎn)
節(jié)點(diǎn)的替換,使用?replaceChild(…)?方法
添加新節(jié)點(diǎn)
function?updateElement($parent,?newNode,?oldNode)?{ if (!oldNode) { $parent.appendChild(createElement(newNode)); }}移除老節(jié)點(diǎn)
這里遇到了一個問題——如果在新虛擬樹的當(dāng)前位置沒有節(jié)點(diǎn)——我們應(yīng)該從實際的 DOM 中刪除它—— 這要如何做呢?如果我們已知父元素(通過參數(shù)傳遞),我們就能調(diào)用?$parent.removeChild(…)方法把變化映射到真實的 DOM 上。但前提是我們得知道我們的節(jié)點(diǎn)在父元素上的索引,我們才能通過?\$parent.childNodes[index]?得到該節(jié)點(diǎn)的引用。好的,讓我們假設(shè)這個索引將被傳遞給?updateElement?函數(shù)(它確實會被傳遞——稍后將看到)。代碼如下:function updateElement($parent, newNode, oldNode, index = 0) { if (!oldNode) { $parent.appendChild(createElement(newNode)); } else if (!newNode) { $parent.removeChild($parent.childNodes[index]); }}節(jié)點(diǎn)的替換
首先,需要編寫一個函數(shù)來比較兩個節(jié)點(diǎn)(舊節(jié)點(diǎn)和新節(jié)點(diǎn)),并告訴節(jié)點(diǎn)是否真的發(fā)生了變化。還有需要考慮這個節(jié)點(diǎn)可以是元素或是文本節(jié)點(diǎn):function changed(node1, node2) { return typeof node1 !== typeof node2 || typeof node1 === ‘string’ && node1 !== node2 || node1.type !== node2.type}現(xiàn)在,當(dāng)前的節(jié)點(diǎn)有了?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]); } else if (changed(newNode, oldNode)) { $parent.replaceChild(createElement(newNode), $parent.childNodes[index]); }}比較子節(jié)點(diǎn)
最后,但并非最不重要的是——我們應(yīng)該遍歷這兩個節(jié)點(diǎn)的每一個子節(jié)點(diǎn)并比較它們——實際上為每個節(jié)點(diǎn)調(diào)用updateElement(…)方法,同樣需要用到遞歸。當(dāng)節(jié)點(diǎn)是 DOM 元素時我們才需要比較( 文本節(jié)點(diǎn)沒有子節(jié)點(diǎn) )
我們需要傳遞當(dāng)前的節(jié)點(diǎn)的引用作為父節(jié)點(diǎn)
我們應(yīng)該一個一個的比較所有的子節(jié)點(diǎn),即使它是?undefined?也沒有關(guān)系,我們的函數(shù)也會正確處理它。
最后是?index,它是子數(shù)組中子節(jié)點(diǎn)的 index
完整的代碼
Babel+JSX/*_ @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);});HTML<button id="reload">RELOADbutton><div id="root">div>CSS#root { border: 1px solid black; padding: 10px; margin: 30px 0 0 0;}打開開發(fā)者工具,并觀察當(dāng)按下“Reload”按鈕時應(yīng)用的更改。
總結(jié)
現(xiàn)在我們已經(jīng)編寫了虛擬 DOM 實現(xiàn)及了解它的工作原理。作者希望,在閱讀了本文之后,對理解虛擬 DOM 如何工作的基本概念以及在幕后如何進(jìn)行響應(yīng)有一定的了解。然而,這里有一些東西沒有突出顯示(將在以后的文章中介紹它們):設(shè)置元素屬性(props)并進(jìn)行 diffing/updating
處理事件——向元素中添加事件監(jiān)聽
讓虛擬 DOM 與組件一起工作,比如 React
獲取對實際 DOM 節(jié)點(diǎn)的引用
使用帶有庫的虛擬 DOM,這些庫可以直接改變真實的 DOM,比如 jQuery 及其插件
總結(jié)
以上是生活随笔為你收集整理的虚拟dom_从0到1实现一个虚拟DOM的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【LeetCode笔记】剑指 Offer
- 下一篇: jflash view log_塑胶产品