初探虛擬 DOM

前言

若是有這麼一張表格要你維護。javascript

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

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

表格簡單的時候還好,用 JavaScript 操做起來還算方便。但隨着應用愈來愈複雜,須要處理的數據也愈來愈大,愈來愈複雜的時候,須要利用 JavaScript 操做的地方也會愈來愈多,這個時候準確地修改數據就變得不是那麼容易了。java

虛擬 DOM 的產生

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

好比這一段 HTML 代碼對應的 DOM,算法

<div>
  <div>
    <span>hello</span>
  </div>
  <span>world</span>
</div>
複製代碼

咱們用另外的一個對象來表示它app

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 了。ui

當咱們的須要改變 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 的過程當中,只更新了發生了變化的那一部分,沒有發生變化的地方是沒動的,這樣就優化了性能。

結語

這是一個很是粗糙的實現,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>
複製代碼
相關文章
相關標籤/搜索