初探虛擬 DOM

YyzclYang發表於2018-11-03

前言

如果有這麼一張表格要你維護。

初探虛擬 DOM

後續涉及到表的增刪改,你會怎麼做?

  • 增:先找到正確的位置,再插元素進去?
  • 刪:找到正確的元素,刪掉它?
  • 改:找到正確的元素,修改它?

表格簡單的時候還好,用 JavaScript 操作起來還算方便。但隨著應用越來越複雜,需要處理的資料也越來越大,越來越複雜的時候,需要利用 JavaScript 操作的地方也會越來越多,這個時候準確地修改資料就變得不是那麼容易了。

虛擬 DOM 的產生

針對前面的情況,那麼能不能用一個東西來儲存頁面的檢視狀態,當檢視狀態傳送變化時,讀取這個東西,然後更新頁面?

比如這一段 HTML 程式碼對應的 DOM,

<div>
  <div>
    <span>hello</span>
  </div>
  <span>world</span>
</div>
複製程式碼

我們用另外的一個物件來表示它

let nodesData = {
  tag: 'div',
  children: [
    {
      tag: 'div',
      children: [
        {
          tag: 'span',
          children: [
            {
              tag: '#text',
              text: 'hello'
            }
          ]
        }
      ]
    },
    {
      tag: 'span',
        children: [
          {
            tag: '#text',
            text: 'world'
          }
        ]
    }
  ]
}
複製程式碼

用這個物件來表示 DOM 結構,我們可以根據這個物件來構建真正的 DOM。

現在我們需要寫一個函式,將這個虛假的 DOM 轉化為真實的 DOM。

化假為真

function vNode({tag, children, text}){
  this.tag = tag
  this.children = children
  this.text = text
}

vNode.prototype.render = function(){
  if(this.tag === '#text'){
    return document.createTextNode(this.text)
  }
  
  let el = document.createElement(this.tag)
  this.children.map((vChild) => {
    el.appendChild(new vNode(vChild).render())
  })
  
  return el
}
複製程式碼

呼叫上面的這個函式可以將我們用來表示 DOM 的物件(虛假 DOM)變成真正的 DOM。

let node = new vNode(nodesData)
node.render()
複製程式碼

這樣,就化假 DOM 為真 DOM 了。

當我們的需要改變 DOM 時,只需要改變其對應的虛假 DOM,再呼叫一下 render 函式,就可以改變真實 DOM,不需要我們親自用 JavaScript 去操作頁面中的 DOM。

區域性更新

上面雖然實現了從虛假 DOM 到真實 DOM 的轉化,但是也有一個問題,那就是每次轉化都會遍歷所有的 DOM 結構,通通的全部轉化一遍。如果只有一個小地方發生了改變,也需要將全部的 DOM 更新一遍,那這樣就太耗費效能了,我們應該比較虛假 DOM 的變化,只更新變化的地方。

function patchElement(parent, newVNode, oldVNode, index = 0) {
  if (!oldVNode) {
    parent.appendChild(newVNode.render());
  } else if (!newVNode) {
    parent.removeChild(parent.childNodes[index]);
  } else if (newVNode.tag !== oldVNode.tag || newVNode.text !== oldVNode.text) {
    parent.replaceChild(new vNode(newVNode).render(), parent.childNodes[index]);
  } else {
    for (
      let i = 0;
      i < newVNode.children.length || i < oldVNode.children.length;
      i++
    ) {
      patchElement(
        parent.childNodes[index],
        newVNode.children[i],
        oldVNode.children[i],
        i
      );
    }
  }
}
複製程式碼

通過這個演算法,逐層比較新舊虛假 DOM 的結構變化,如果沒變,就繼續往下遍歷;如果發現結構發生了變化,就重新生成真實 DOM 替換掉舊的。

來看一看效果。

初探虛擬 DOM

從圖中可以看到,當虛假 DOM 發生變化時,在更新真實 DOM 的過程中,只更新了發生了變化的那一部分,沒有發生變化的地方是沒動的,這樣就優化了效能。

結語

這是一個非常粗糙的實現,diff 演算法非常簡單地比較了差異,這裡僅僅表達了一下虛擬 DOM 的實現思想,在實際運用過程還有很多地方需要考慮。

這裡貼個完整程式碼。

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>

<body>
  <div id="test"></div>
  <script>
    let nodesData = {
      tag: 'div',
      children: [
        {
          tag: 'div',
          children: [
            {
              tag: 'span',
              children: [
                {
                  tag: '#text',
                  text: 'hello'
                }
              ]
            }
          ]
        },
        {
          tag: 'span',
          children: [
            {
              tag: '#text',
              text: 'world'
            }
          ]
        }
      ]
    };
    let nodesData2 = {
      tag: 'div',
      children: [
        {
          tag: 'div',
          children: [
            {
              tag: 'span',
              children: [
                {
                  tag: '#text',
                  text: 'HELLO'
                }
              ]
            }
          ]
        },
        {
          tag: 'span',
          children: [
            {
              tag: '#text',
              text: 'WORLD'
            }
          ]
        }
      ]
    };

    function vNode({ tag, children, text }) {
      this.tag = tag;
      this.children = children;
      this.text = text;
    }

    vNode.prototype.render = function () {
      if (this.tag === '#text') {
        return document.createTextNode(this.text);
      }

      let el = document.createElement(this.tag);

      this.children.map(vChild => {
        el.appendChild(new vNode(vChild).render());
      });

      return el;
    };

    function patchElement(parent, newVNode, oldVNode, index = 0) {
      if (!oldVNode) {
        parent.appendChild(newVNode.render());
      } else if (!newVNode) {
        parent.removeChild(parent.childNodes[index]);
      } else if (newVNode.tag !== oldVNode.tag || newVNode.text !== oldVNode.text) {
        parent.replaceChild(new vNode(newVNode).render(), parent.childNodes[index]);
      } else {
        for (
          let i = 0;
          i < newVNode.children.length || i < oldVNode.children.length;
          i++
        ) {
          patchElement(
            parent.childNodes[index],
            newVNode.children[i],
            oldVNode.children[i],
            i
          );
        }
      }
    }

    let node1 = new vNode(nodesData);
    let node2 = new vNode(nodesData2);

    let test = document.querySelector('#test');
    test.appendChild(node1.render());

    setTimeout(() => {
      patchElement(test, node2, node1, 0);
    }, 5000);
  </script>
</body>

</html>
複製程式碼

相關文章