<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 7759字,需瀏覽 16分鐘

           ·

          2020-08-20 18:28

          來(lái)源 |?https://segmentfault.com/a/1190000021331850
          要構(gòu)建自己的虛擬 DOM,需要知道兩件事。你甚至不需要深入 React 的源代碼或者深入任何其他虛擬 DOM 實(shí)現(xiàn)的源代碼,因?yàn)樗鼈兪侨绱她嫶蠛蛷?fù)雜——但實(shí)際上,虛擬 DOM 的主要部分只需不到 50 行代碼。
          有兩個(gè)概念:
          • Virtual DOM 是真實(shí) DOM 的映射

          • 當(dāng)虛擬 DOM 樹中的某些節(jié)點(diǎn)改變時(shí),會(huì)得到一個(gè)新的虛擬樹。算法對(duì)這兩棵樹(新樹和舊樹)進(jìn)行比較,找出差異,然后只需要在真實(shí)的 DOM 上做出相應(yīng)的改變。

          用 JS 對(duì)象模擬 DOM 樹

          首先,我們需要以某種方式將 DOM 樹存儲(chǔ)在內(nèi)存中??梢允褂闷胀ǖ?JS 對(duì)象來(lái)做。假設(shè)我們有這樣一棵樹:
          <ul class="”list”"> <li>item 1li> <li>item 2li>ul>
          看起來(lái)很簡(jiǎn)單,對(duì)吧? 如何用 JS 對(duì)象來(lái)表示呢?
          { 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)

          但是用這種方式表示內(nèi)容很多的 Dom 樹是相當(dāng)困難的。這里來(lái)寫一個(gè)輔助函數(shù),這樣更容易理解:
          function h(type, props, …children) { return { type, props, children };}
          用這個(gè)方法重新整理一開始代碼:
          h(‘ul’, { ‘class’: ‘list’ }, h(‘li’, {}, ‘item 1’), h(‘li’, {}, ‘item 2’),);
          這樣看起來(lái)簡(jiǎn)潔多了,還可以更進(jìn)一步。這里使用 JSX,如下:
          <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’),);
          是不是看起來(lái)有點(diǎn)熟悉?如果能夠用我們剛定義的?h(...)?函數(shù)代替?React.createElement(…),那么我們也能使用 JSX 語(yǔ)法。其實(shí),只需要在源文件頭部加上這么一句注釋:
          /** @jsx h */<ul className="”list”"> <li>item 1li> <li>item 2li>ul>
          它實(shí)際上告訴 Babel ‘ 嘿,小老弟幫我編譯?JSX?語(yǔ)法,用?h(...)?函數(shù)代替?React.createElement(…),然后?Babel?就開始編譯?!?/span>
          綜上所述,我們將 DOM 寫成這樣:
          /** @jsx h */ const a = (<ul className="”list”"> <li>item 1li> <li>item 2li>ul>);
          Babel 會(huì)幫我們編譯成這樣的代碼:
          const a = ( h(‘ul’, { className: ‘list’ }, h(‘li’, {}, ‘item 1’), h(‘li’, {}, ‘item 2’), ););
          當(dāng)函數(shù)?“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

          好了,現(xiàn)在我們有了 DOM 樹,用普通的 JS 對(duì)象表示,還有我們自己的結(jié)構(gòu)。這很酷,但我們需要從它創(chuàng)建一個(gè)真正的 DOM。
          首先讓我們做一些假設(shè)并聲明一些術(shù)語(yǔ):
          • 使用以’?$?‘開頭的變量表示真正的 DOM 節(jié)點(diǎn)(元素,文本節(jié)點(diǎn)),因此 \$parent 將會(huì)是一個(gè)真實(shí)的 DOM 元素

          • 虛擬 DOM 使用名為?node?的變量表示

          *就像在React中一樣,只能有一個(gè)根節(jié)點(diǎn)——所有其他節(jié)點(diǎn)都在其中。
          那么,來(lái)編寫一個(gè)函數(shù)?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);}
          上述方法我也可以創(chuàng)建有兩種節(jié)點(diǎn)分別是文本節(jié)點(diǎn)和 Dom 元素節(jié)點(diǎn),它們是類型為的 JS 對(duì)象:
          { type: ‘…’, props: { … }, children: [ … ] }
          因此,可以在函數(shù)?createElement?傳入虛擬文本節(jié)點(diǎn)和虛擬元素節(jié)點(diǎn)——這是可行的。
          現(xiàn)在讓我們考慮子節(jié)點(diǎn)——它們中的每一個(gè)都是文本節(jié)點(diǎn)或元素。所以它們也可以用?createElement(…)?函數(shù)創(chuàng)建。是的,這就像遞歸一樣,所以我們可以為每個(gè)元素的子元素調(diào)用?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;}
          哇,看起來(lái)不錯(cuò)。先把節(jié)點(diǎn)?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 樹的差異

          現(xiàn)在我們可以將虛擬 DOM 轉(zhuǎn)換為真實(shí)的 DOM,這就需要考慮比較兩棵 DOM 樹的差異?;镜模覀冃枰粋€(gè)算法來(lái)比較新的樹和舊的樹,它能夠讓我們知道什么地方改變了,然后相應(yīng)的去改變真實(shí)的 DOM。
          怎么比較 DOM 樹?需要處理下面的情況:
          • 添加新節(jié)點(diǎn),使用?appendChild(…)?方法添加節(jié)點(diǎn)

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

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

          如果節(jié)點(diǎn)相同的——就需要需要深度比較子節(jié)點(diǎn)

          編寫一個(gè)名為?updateElement(…)?的函數(shù),它接受三個(gè)參數(shù)——?$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)

          這里遇到了一個(gè)問(wèn)題——如果在新虛擬樹的當(dāng)前位置沒(méi)有節(jié)點(diǎn)——我們應(yīng)該從實(shí)際的 DOM 中刪除它—— 這要如何做呢?
          如果我們已知父元素(通過(guò)參數(shù)傳遞),我們就能調(diào)用?$parent.removeChild(…)?方法把變化映射到真實(shí)的 DOM 上。但前提是我們得知道我們的節(jié)點(diǎn)在父元素上的索引,我們才能通過(guò)?\$parent.childNodes[index]?得到該節(jié)點(diǎn)的引用。
          好的,讓我們假設(shè)這個(gè)索引將被傳遞給?updateElement?函數(shù)(它確實(shí)會(huì)被傳遞——稍后將看到)。代碼如下:
          function updateElement($parent, newNode, oldNode, index = 0) { if (!oldNode) { $parent.appendChild(createElement(newNode)); } else if (!newNode) { $parent.removeChild($parent.childNodes[index]); }}

          節(jié)點(diǎn)的替換

          首先,需要編寫一個(gè)函數(shù)來(lái)比較兩個(gè)節(jié)點(diǎn)(舊節(jié)點(diǎn)和新節(jié)點(diǎn)),并告訴節(jié)點(diǎn)是否真的發(fā)生了變化。還有需要考慮這個(gè)節(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ǎn)單的用新節(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)該遍歷這兩個(gè)節(jié)點(diǎn)的每一個(gè)子節(jié)點(diǎn)并比較它們——實(shí)際上為每個(gè)節(jié)點(diǎn)調(diào)用updateElement(…)方法,同樣需要用到遞歸。
          • 當(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 ); } }}

          完整的代碼

          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”按鈕時(shí)應(yīng)用的更改。

          總結(jié)

          現(xiàn)在我們已經(jīng)編寫了虛擬 DOM 實(shí)現(xiàn)及了解它的工作原理。作者希望,在閱讀了本文之后,對(duì)理解虛擬 DOM 如何工作的基本概念以及在幕后如何進(jìn)行響應(yīng)有一定的了解。
          然而,這里有一些東西沒(méi)有突出顯示(將在以后的文章中介紹它們):
          • 設(shè)置元素屬性(props)并進(jìn)行 diffing/updating

          • 處理事件——向元素中添加事件監(jiān)聽

          • 讓虛擬 DOM 與組件一起工作,比如 React

          • 獲取對(duì)實(shí)際 DOM 節(jié)點(diǎn)的引用

          • 使用帶有庫(kù)的虛擬 DOM,這些庫(kù)可以直接改變真實(shí)的 DOM,比如 jQuery 及其插件


          瀏覽 57
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲青青视频 | 国产成人精品免费视频麻豆大全 | 91情趣网 | 国产精品成人电影 | 天天干天天操综合网 |