從零開始一步一步寫一個簡單的Virtual DOM實現

原文地址html

Github 系列文章地址前端

在閱讀此文以前,你要明確兩個概念。這篇文章不會長篇大論地跟你介紹React中的源代碼實現或者其餘一些相似的Virtual DOM的實現。它們過於複雜了,其實一個Virtual DOM的實現只要不超過50行代碼便可。好了,下面便是你要了解的兩個概念:node

  • Virtual DOM是真正DOM的一種表現git

  • 當Virtual DOM Tree發生變化時,算法會自動比較新舊兩棵樹,找出其中的差別,而且只對真實的DOM樹作最小化改變github

本文便是按部就班地闡述這兩個概念。web

DOM樹的表示

首先,咱們須要將DOM樹存放於內存中,最簡單的,咱們能夠將DOM樹表示爲一個JavaScript的Object對象,假設咱們有一棵這樣的DOM樹:算法

<ul class=」list」>

  <li>item 1</li>

  <li>item 2</li>

</ul>

而該DOM樹對應的JS對象以下:app

{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [

  { type: ‘li’, props: {}, children: [‘item 1’] },

  { type: ‘li’, props: {}, children: [‘item 2’] }

] }

兩相比較,咱們能夠發現,咱們將DOM中的任一元素表示爲:frontend

{ type: ‘…’, props: { … }, children: [ … ] }

而DOM中的純文本節點會被表示爲普通的JavaScript中的字符串。不過這仍是一個簡單的DOM樹,若是是一個較大型的樹,咱們就須要一個輔助函數來構造結構:dom

function h(type, props, …children) {

  return { type, props, children };

}

基於這個輔助函數,咱們能夠把上面那個簡單的DOM樹用以下方式表示:

h(‘ul’, { ‘class’: ‘list’ },

  h(‘li’, {}, ‘item 1’),

  h(‘li’, {}, ‘item 2’),

);

看上去是否是清晰了不少呀?這種結構和轉化方程看上去很像大名鼎鼎的JSX啊,以Babel解釋器爲例,它會把上面說起的DOM樹轉化爲以下結構:

React.createElement(‘ul’, { className: ‘list’ },

  React.createElement(‘li’, {}, ‘item 1’),

  React.createElement(‘li’, {}, ‘item 2’),

);

總結而言,咱們能夠按照以下JSX的語法編寫DOM樹:

/** @jsx h */

const a = (
 <ul className=」list」>
   <li>item 1</li>
   <li>item 2</li>
 </ul>
);

而Babel會將JSX轉化爲以下格式:

const a = (

  h(‘ul’, { className: ‘list’ },

    h(‘li’, {}, ‘item 1’),

    h(‘li’, {}, ‘item 2’),

  );

);

h函數執行以後,整個對象會轉化爲基本的JS對象:

const a = (

  { type: ‘ul’, props: { className: ‘list’ }, children: [

    { type: ‘li’, props: {}, children: [‘item 1’] },

    { type: ‘li’, props: {}, children: [‘item 2’] }

  ] }

);

本部分在JSFiddle上的地址是:這裏

完整的Babel可編譯的源代碼爲:

/** @jsx h */

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

const a = (
  <ul class="list">
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

console.log(a);

Applying our DOM Representation

如今已經能夠將DOM樹用純粹的JS對象進行表示,那麼下一步咱們就是須要將自定義的虛擬DOM結構體轉化到真實的DOM樹中。首先闡述下下文會用到的一些術語表達式:

  • 全部真實DOM節點,譬如元素與文本節點,都以$開頭描述,譬如$parent就是一個真實的DOM元素

  • 全部的Virtual DOM將會用變量node描述

  • 跟React中相似,只能夠有一個根節點存在,其餘全部的節點都會包含在該根節點內

那麼下面咱們就要來編寫函數createElement,負責將輸入的虛擬DOM轉化爲一個真實的DOM,這裏暫時不考慮propschildren,那麼最簡單的函數實現是:

function createElement(node) {

  if (typeof node === ‘string’) {

    return document.createTextNode(node);

  }

  return document.createElement(node.type);

}

由於咱們須要考慮到同時處理文本節點與元素節點的須要,因此進行了一個簡單的分支判斷,這是最簡單的實現,下面咱們要考慮怎麼對子元素進行渲染。每一個節點的子節點多是個文本節點,也多是元素節點,換言之,咱們要沿着虛擬節點的樹一直從根節點處理到葉子節點,差很少就是要用迭代的思想進行構造,而後用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;
}

本部分的JSFiddle調試在這裏,完整的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;
}

const a = (
  <ul class="list">
    <li>item 1</li>
    <li>item 2</li>
  </ul>
);

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

Handling Changes

如今咱們已經成功地將Virtual DOM轉化爲了真實的DOM節點,下面就要考慮下Virtual DOM核心的算法,即差別檢測。咱們先來寫一個最簡單的Virtual DOM比較算法,保證只會對真實的DOM節點作最小改動。首先咱們仍是來看下可能有幾種發生改變的狀況:

(1)添加了部分節點,須要調用appendChild()函數進行添加

(2)移除了部分節點,須要調用removeChild()函數進行刪除

(3)部分節點變成了其餘節點,須要調用replaceChild()進行替換

(4)某個節點的標籤發生了變化,或者被掛載到了其餘地方

對於以上這幾種狀況,咱們統一使用updateElement()函數對DOM樹進行更新,該函數會傳入三個參數:

  • $parent 表明Virtual DOM掛載在DOM樹上的根節點

  • newNode 新的Virtual DOM

  • oldNode 老的Virtual DOM

初始化時候沒有老的Virtual DOM狀況

若是oldNode直接爲空,那麼咱們只要簡單地建立新的節點便可:

function updateElement($parent, newNode, oldNode) {

  if (!oldNode) {

    $parent.appendChild(

      createElement(newNode)

    );

  }

}

整個newNode被置空,即從DOM樹中移除了

若是newNode爲空,即整個Virtual DOM樹上沒有掛載任何節點,那麼咱們須要將VirtualDOM對應的節點樹從DOM中移除,最簡單的方法就是調用$parent.removeChild()函數,而後傳入整個真實DOM元素的引用。不過實際上,咱們在內存裏只有Virtual DOM而沒有真實DOM的引用。那咱們換個思路,若是咱們知道Virtual DOM對應處於真實DOM中的第幾個子節點,就能夠根據下標刪除了,大概是這個樣子:

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

}

有了這個比較函數和當前Virtual DOM映射的真實DOM在父節點中的序號,咱們就能夠將更新函數完善成以下介個樣子:

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]

    );

  }

}

注意,上面比較函數中,在節點發生變化的狀況,只考慮了Virtual DOM中根節點發生了變化的狀況,比較的方式也是直接比較內存地址,是不是新對象,從這一點也能夠看出Immutable的重要意義。

Diff children

上面說起的算法裏並無對子節點進行檢查,而在實際狀況下,咱們不只要檢查根節點,還要遞歸檢查子節點是否發生了變化,即遞歸找到變化的那個節點,在編寫代碼以前,咱們腦中要清楚如下幾點:

  • 只有對元素節點才須要進行子節點對比,文本節點是沒有子節點的

  • 遞歸過程當中,會不斷傳入當前節點做爲子節點對比的根節點處理

  • 上面說的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

      );

    }

  }

}

最終代碼的調試地址是JSFiddle,其效果爲:

到這裏咱們就完成了一個最簡單的Virtual DOM算法,不過其與真正可以投入實戰的Virtual DOM算法仍是有很大距離,進一步閱讀推薦:

相關文章
相關標籤/搜索