<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>

          通過編寫簡(jiǎn)易虛擬DOM,來學(xué)習(xí)虛擬DOM 的知識(shí)!

          共 13326字,需瀏覽 27分鐘

           ·

          2021-12-10 14:58

          作者:前端小智

          簡(jiǎn)介:掘金、思否百萬閱讀,勵(lì)志退休后,回家擺地?cái)偟娜?/span>

          來源:SegmentFault  思否社區(qū)


          要構(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ì)象來做。假設(shè)我們有這樣一棵樹:


          <ul class=”list”>
            <li>item 1</li>
            <li>item 2</li>
          </ul>


          看起來很簡(jiǎn)單,對(duì)吧? 如何用JS對(duì)象來表示呢?


          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)困難的。這里來寫一個(gè)輔助函數(shù),這樣更容易理解:


          function h(type, props, …children) {
            return { type, props, children };
          }


          用這個(gè)方法重新整理一開始代碼:


          h(‘ul’, { ‘class’: ‘list’ },
            h(‘li’, {}, ‘item 1’),
            h(‘li’, {}, ‘item 2’),
          );


          這樣看起來簡(jiǎn)潔多了,還可以更進(jìn)一步。這里使用 JSX,如下:


          <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’),
          );


          是不是看起來有點(diǎn)熟悉?如果能夠用我們剛定義的 h(...) 函數(shù)代替 React.createElement(…),那么我們也能使用JSX 語法。其實(shí),只需要在源文件頭部加上這么一句注釋:


          /** @jsx h */
          <ul className=”list”>
            <li>item 1</li>
            <li>item 2</li>
          </ul>


          它實(shí)際上告訴 Babel ' 嘿,小老弟幫我編譯 JSX 語法,用 h(...) 函數(shù)代替 React.createElement(…),然后 Babel 就開始編譯。'


          綜上所述,我們將DOM寫成這樣:


          /** @jsx h */
          const a = (
            <ul className=”list”>
              <li>item 1</li>
              <li>item 2</li>
            </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ù)語:


          • 使用以' $ '開頭的變量表示真正的DOM節(jié)點(diǎn)(元素,文本節(jié)點(diǎn)),因此 $parent 將會(huì)是一個(gè)真實(shí)的DOM元素

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


          就像在 React 中一樣,只能有一個(gè)根節(jié)點(diǎn)——所有其他節(jié)點(diǎn)都在其中

          那么,來編寫一個(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;
          }


          哇,看起來不錯(cuò)。先把節(jié)點(diǎn) props 屬性放到一邊。待會(huì)再談。我們不需要它們來理解虛擬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 1</li>
              <li>item 2</li>
            </ul>
          );

          const $root = document.getElementById('root');
          $root.appendChild(createElement(a));


          比較兩棵虛擬DOM樹的差異


          現(xiàn)在我們可以將虛擬 DOM 轉(zhuǎn)換為真實(shí)的 DOM,這就需要考慮比較兩棵 DOM 樹的差異。基本的,我們需要一個(gè)算法來比較新的樹和舊的樹,它能夠讓我們知道什么地方改變了,然后相應(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ù)—— $parentnewNodeoldNode,其中 $parent 是虛擬節(jié)點(diǎn)的一個(gè)實(shí)際 DOM 元素的父元素。現(xiàn)在來看看如何處理上面描述的所有情況。


          添加新節(jié)點(diǎn)


          function updateElement($parent, newNode, oldNode) {
            if (!oldNode) {
              $parent.appendChild(
                createElement(newNode)
              );
            }
          }


          移除老節(jié)點(diǎn)


          這里遇到了一個(gè)問題——如果在新虛擬樹的當(dāng)前位置沒有節(jié)點(diǎn)——我們應(yīng)該從實(shí)際的 DOM 中刪除它—— 這要如何做呢?


          如果我們已知父元素(通過參數(shù)傳遞),我們就能調(diào)用 $parent.removeChild(…) 方法把變化映射到真實(shí)的 DOM 上。但前提是我們得知道我們的節(jié)點(diǎn)在父元素上的索引,我們才能通過 $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ù)來比較兩個(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)沒有子節(jié)點(diǎn) )

          • 我們需要傳遞當(dāng)前的節(jié)點(diǎn)的引用作為父節(jié)點(diǎn)

          • 我們應(yīng)該一個(gè)一個(gè)的比較所有的子節(jié)點(diǎn),即使它是 undefined 也沒有關(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 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);
          });


          HTML


          <button id="reload">RELOAD</button>
          <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)有一定的了解。


          然而,這里有一些東西沒有突出顯示(將在以后的文章中介紹它們):


          • 設(shè)置元素屬性(props)并進(jìn)行 diffing/updating

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

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

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

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




          點(diǎn)擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動(dòng)和交流,掃描下方”二維碼“或在“公眾號(hào)后臺(tái)回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -


          瀏覽 51
          點(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>
                  操逼3级黄色毛片 | 精品视频天天在线免费 | 狼人综合网 | vi7青草视频 | 日韩国产免费一区二区 |