合格前端系列第五彈- Virtual Dom && Diff(遷移)

qiangdada發表於2019-04-09

前言

這是一篇很長的文章!!!堅持看到最後有彩蛋哦!!!

文章開篇,我們先思考一個問題,大家都說 virtual dom 這,virtual dom 那的,那麼 virtual dom 到底是啥?

首先,我們得明確一點,所謂的 virtual dom,也就是虛擬節點。它通過 JSObject 物件模擬 DOM 中的節點,然後再通過特定的 render 方法將其渲染成真實的 DOM 節點。

其次我們還得知道一點,那就是 virtual dom 做的一件事情到底是啥。我們知道的對於頁面的重新渲染一般的做法是通過操作 dom,重置 innerHTML 去完成這樣一件事情。而 virtual dom 則是通過 JS 層面的計算,返回一個 patch 物件,即補丁物件,在通過特定的操作解析 patch 物件,完成頁面的重新渲染。具體 virtual dom 渲染的一個流程如圖所示

合格前端系列第五彈- Virtual Dom && Diff(遷移)

接下來,我會老規矩,邊上程式碼,邊解析,帶著小夥伴們一起實現一個virtual dom && diff。具體步驟如下

  1. 實現一個 utils 方法庫
  2. 實現一個 Element(virtual dom)
  3. 實現 diff 演算法
  4. 實現 patch

一、實現一個 utils 方法庫

俗話說的好,磨刀不廢砍柴功,為了後面的方便,我會在這先帶著大家實現後面經常用到的一些方法,畢竟要是每次都寫一遍用的方法,豈不得瘋,因為程式碼簡單,所以這裡我就直接貼上程式碼了

const _ = exports

_.setAttr = function setAttr (node, key, value) {
  switch (key) {
    case 'style':
      node.style.cssText = value
      break;
    case 'value':
      let tagName = node.tagName || ''
      tagName = tagName.toLowerCase()
      if (
        tagName === 'input' || tagName === 'textarea'
      ) {
        node.value = value
      } else {
        // 如果節點不是 input 或者 textarea, 則使用 `setAttribute` 去設定屬性
        node.setAttribute(key, value)
      }
      break;
    default:
      node.setAttribute(key, value)
      break;
  }
}

_.slice = function slice (arrayLike, index) {
  return Array.prototype.slice.call(arrayLike, index)
}


_.type = function type (obj) {
  return Object.prototype.toString.call(obj).replace(/\[object\s|\]/g, '')
}

_.isArray = function isArray (list) {
  return _.type(list) === 'Array'
}

_.toArray = function toArray (listLike) {
  if (!listLike) return []

  let list = []
  for (let i = 0, l = listLike.length; i < l; i++) {
    list.push(listLike[i])
  }
  return list
}

_.isString = function isString (list) {
  return _.type(list) === 'String'
}

_.isElementNode = function (node) {
  return node.nodeType === 1
}

複製程式碼

二、實現一個 Element

這裡我們需要做的一件事情很 easy ,那就是實現一個 Object 去模擬 DOM 節點的展示形式。真實節點如下

<ul id="list">
  <li class="item">item1</li>
  <li class="item">item2</li>
  <li class="item">item3</li>
</ul>
複製程式碼

我們需要完成一個 Element 模擬上面的真實節點,形式如下

let ul = {
  tagName: 'ul',
  attrs: {
    id: 'list'
  },
  children: [
    { tagName: 'li', attrs: { class: 'item' }, children: ['item1'] },
    { tagName: 'li', attrs: { class: 'item' }, children: ['item1'] },
    { tagName: 'li', attrs: { class: 'item' }, children: ['item1'] },
  ]
}
複製程式碼

看到這裡,我們可以看到的是 el 物件中的 tagNameattrschildren 都可以提取出來到 Element 中去,即

class Element {
  constructor(tagName, attrs, children) {
    this.tagName  = tagName
    this.attrs    = attrs
    this.children = children
  }
}
function el (tagName, attrs, children) {
  return new Element(tagName, attrs, children)
}
module.exports = el;
複製程式碼

那麼上面的ul就可以用更簡化的方式進行書寫了,即

let ul = el('ul', { id: 'list' }, [
  el('li', { class: 'item' }, ['Item 1']),
  el('li', { class: 'item' }, ['Item 2']),
  el('li', { class: 'item' }, ['Item 3'])
])
複製程式碼

ul 則是 Element 物件,如圖

合格前端系列第五彈- Virtual Dom && Diff(遷移)

OK,到這我們 Element 算是實現一半,剩下的一般則是提供一個 render 函式,將 Element 物件渲染成真實的 DOM 節點。完整的 Element 的程式碼如下

import _ from './utils'

/**
 * @class Element Virtrual Dom
 * @param { String } tagName
 * @param { Object } attrs   Element's attrs, 如: { id: 'list' }
 * @param { Array <Element|String> } 可以是Element物件,也可以只是字串,即textNode
 */
class Element {
  constructor(tagName, attrs, children) {
    // 如果只有兩個引數
    if (_.isArray(attrs)) {
      children = attrs
      attrs = {}
    }

    this.tagName  = tagName
    this.attrs    = attrs || {}
    this.children = children
    // 設定this.key屬性,為了後面list diff做準備
    this.key = attrs
      ? attrs.key
      : void 0
  }

  render () {
    let el    = document.createElement(this.tagName)
    let attrs = this.attrs

    for (let attrName in attrs) { // 設定節點的DOM屬性
      let attrValue = attrs[attrName]
      _.setAttr(el, attrName, attrValue)
    }

    let children = this.children || []
    children.forEach(child => {
      let childEl = child instanceof Element
        ? child.render() // 若子節點也是虛擬節點,遞迴進行構建
        : document.createTextNode(child)  // 若是字串,直接構建文字節點
      el.appendChild(childEl)
    })

    return el
  }
}
function el (tagName, attrs, children) {
  return new Element(tagName, attrs, children)
}
module.exports = el;

複製程式碼

這個時候我們執行寫好的 render 方法,將 Element 物件渲染成真實的節點

let ulRoot = ul.render()
document.body.appendChild(ulRoot);
複製程式碼

效果如圖

合格前端系列第五彈- Virtual Dom && Diff(遷移)

至此,我們的 Element 便得以實現了。

三、實現 diff 演算法

這裡我們做的就是實現一個 diff 演算法進行虛擬節點 Element 的對比,並返回一個 patch 物件,用來儲存兩個節點不同的地方。這也是整個 virtual dom 實現最核心的一步。而 diff 演算法又包含了兩個不一樣的演算法,一個是 O(n),一個則是 O(max(m, n))

1、同層級元素比較(O(n))

首先,我們的知道的是,如果元素之間進行完全的一個比較,即新舊 Element 物件的父元素,本身,子元素之間進行一個混雜的比較,其實現的時間複雜度為 O(n^3)。但是在我們前端開發中,很少會出現跨層級處理節點,所以這裡我們會做一個同級元素之間的一個比較,則其時間複雜度則為 O(n)。演算法流程如圖所示

合格前端系列第五彈- Virtual Dom && Diff(遷移)

在這裡,我們做同級元素比較時,可能會出現四種情況

  • 整個元素都不一樣,即元素被 replace
  • 元素的 attrs 不一樣
  • 元素的 text 文字不一樣
  • 元素順序被替換,即元素需要 reorder

上面列舉第四種情況屬於 diff 的第二種演算法,這裡我們先不討論,我們在後面再進行詳細的討論
針對以上四種情況,我們先設定四個常量進行表示。diff 入口方法及四種狀態如下

const REPLACE = 0  // replace => 0
const ATTRS   = 1  // attrs   => 1
const TEXT    = 2  // text    => 2
const REORDER = 3  // reorder => 3

// diff 入口,比較新舊兩棵樹的差異
function diff (oldTree, newTree) {
  let index   = 0
  let patches = {} // 用來記錄每個節點差異的補丁物件
  walk(oldTree, newTree, index, patches)
  return patches
}
複製程式碼

OK,狀態定義好了,接下來開搞。我們一個一個實現,獲取到每個狀態的不同。這裡需要注意的一點就是,我們這裡的 diff 比較只會和上面的流程圖顯示的一樣,只會兩兩之間進行比較,如果有節點 remove 掉,這裡會 pass 掉,直接走 list diff

a、首先我們先從最頂層的元素依次往下進行比較,直到最後一層元素結束,並把每個層級的差異存到 patch 物件中去,即實現walk方法

/**
 * walk 遍歷查詢節點差異
 * @param  { Object } oldNode
 * @param  { Object } newNode
 * @param  { Number } index   - currentNodeIndex
 * @param  { Object } patches - 記錄節點差異的物件
 */
function walk (oldNode, newNode, index, patches) {
  let currentPatch = []

  // 如果oldNode被remove掉了
  if (newNode === null || newNode === undefined) {
    // 先不做操作, 具體交給 list diff 處理
  }
  // 比較文字之間的不同
  else if (_.isString(oldNode) && _.isString(newNode)) {
    if (newNode !== oldNode) currentPatch.push({ type: TEXT, content: newNode })
  }
  // 比較attrs的不同
  else if (
    oldNode.tagName === newNode.tagName &&
    oldNode.key     === newNode.key
  ) {
    let attrsPatches = diffAttrs(oldNode, newNode)
    if (attrsPatches) {
      currentPatch.push({ type: ATTRS, attrs: attrsPatches })
    }
    // 遞迴進行子節點的diff比較
    diffChildren(oldNode.children, newNode.children, index, patches)
  }
  else {
    currentPatch.push({ type: REPLACE, node: newNode})
  }

  if (currentPatch.length) {
    patches[index] = currentPatch
  }
}

function diffAttrs (oldNode, newNode) {
  let count    = 0
  let oldAttrs = oldNode.attrs
  let newAttrs = newNode.attrs

  let key, value
  let attrsPatches = {}

  // 如果存在不同的 attrs
  for (key in oldAttrs) {
    value = oldAttrs[key]
    // 如果 oldAttrs 移除掉一些 attrs, newAttrs[key] === undefined
    if (newAttrs[key] !== value) {
      count++
      attrsPatches[key] = newAttrs[key]
    }
  }
  // 如果存在新的 attr
  for (key in newAttrs) {
    value = newAttrs[key]
    if (!oldAttrs.hasOwnProperty(key)) {
      count++
      attrsPatches[key] = value
    }
  }

  if (count === 0) {
    return null
  }

  return attrsPatches
}
複製程式碼

b、實際上我們需要對新舊元素進行一個深度的遍歷,為每個節點加上一個唯一的標記,具體流程如圖所示

合格前端系列第五彈- Virtual Dom && Diff(遷移)

如上圖,我們接下來要做的一件事情就很明確了,那就是在做深度遍歷比較差異的時候,將每個元素節點,標記上一個唯一的標識。具體做法如下

// 設定節點唯一標識
let key_id = 0
// diff with children
function diffChildren (oldChildren, newChildren, index, patches) {
  // 存放當前node的標識,初始化值為 0
  let currentNodeIndex = index

  oldChildren.forEach((child, i) => {
    key_id++
    let newChild = newChildren[i]
    currentNodeIndex = key_id

    // 遞迴繼續比較
    walk(child, newChild, currentNodeIndex, patches)
  })
}
複製程式碼

OK,這一步偶了。我們呼叫一下看下效果,看看兩個不同的 Element 物件比較會返回一個哪種形式的 patch 物件

let ul = el('ul', { id: 'list' }, [
  el('li', { class: 'item' }, ['Item 1']),
  el('li', { class: 'item' }, ['Item 2'])
])
let ul1 = el('ul', { id: 'list1' }, [
  el('li', { class: 'item1' }, ['Item 4']),
  el('li', { class: 'item2' }, ['Item 5'])
])
let patches = diff(ul, ul1);
console.log(patches);
複製程式碼

控制檯結果如圖

合格前端系列第五彈- Virtual Dom && Diff(遷移)

完整的 diff 程式碼如下(包含了呼叫 list diff 的方法,如果你在跟著文章踩坑的話,把裡面一些程式碼註釋掉即可)

import _ from './utils'
import listDiff from './list-diff'

const REPLACE = 0
const ATTRS   = 1
const TEXT    = 2
const REORDER = 3

// diff 入口,比較新舊兩棵樹的差異
function diff (oldTree, newTree) {
  let index   = 0
  let patches = {} // 用來記錄每個節點差異的補丁物件
  walk(oldTree, newTree, index, patches)
  return patches
}

/**
 * walk 遍歷查詢節點差異
 * @param  { Object } oldNode
 * @param  { Object } newNode
 * @param  { Number } index   - currentNodeIndex
 * @param  { Object } patches - 記錄節點差異的物件
 */
function walk (oldNode, newNode, index, patches) {

  let currentPatch = []

  // 如果oldNode被remove掉了,即 newNode === null的時候
  if (newNode === null || newNode === undefined) {
    // 先不做操作, 具體交給 list diff 處理
  }
  // 比較文字之間的不同
  else if (_.isString(oldNode) && _.isString(newNode)) {
    if (newNode !== oldNode) currentPatch.push({ type: TEXT, content: newNode })
  }
  // 比較attrs的不同
  else if (
    oldNode.tagName === newNode.tagName &&
    oldNode.key     === newNode.key
  ) {
    let attrsPatches = diffAttrs(oldNode, newNode)
    if (attrsPatches) {
      currentPatch.push({ type: ATTRS, attrs: attrsPatches })
    }
    // 遞迴進行子節點的diff比較
    diffChildren(oldNode.children, newNode.children, index, patches, currentPatch)
  }
  else {
    currentPatch.push({ type: REPLACE, node: newNode})
  }

  if (currentPatch.length) {
    patches[index] = currentPatch
  }
}

function diffAttrs (oldNode, newNode) {
  let count    = 0
  let oldAttrs = oldNode.attrs
  let newAttrs = newNode.attrs

  let key, value
  let attrsPatches = {}

  // 如果存在不同的 attrs
  for (key in oldAttrs) {
    value = oldAttrs[key]
    // 如果 oldAttrs 移除掉一些 attrs, newAttrs[key] === undefined
    if (newAttrs[key] !== value) {
      count++
      attrsPatches[key] = newAttrs[key]
    }
  }
  // 如果存在新的 attr
  for (key in newAttrs) {
    value = newAttrs[key]
    if (!oldAttrs.hasOwnProperty(key)) {
      attrsPatches[key] = value
    }
  }

  if (count === 0) {
    return null
  }

  return attrsPatches
}

// 設定節點唯一標識
let key_id = 0
// diff with children
function diffChildren (oldChildren, newChildren, index, patches, currentPatch) {
  let diffs = listDiff(oldChildren, newChildren, 'key')
  newChildren = diffs.children

  if (diffs.moves.length) {
    let reorderPatch = { type: REORDER, moves: diffs.moves }
    currentPatch.push(reorderPatch)
  }

  // 存放當前node的標識,初始化值為 0
  let currentNodeIndex = index

  oldChildren.forEach((child, i) => {
    key_id++
    let newChild = newChildren[i]
    currentNodeIndex = key_id

    // 遞迴繼續比較
    walk(child, newChild, currentNodeIndex, patches)
  })
}

module.exports = diff

複製程式碼

看到這裡的小夥伴們,如果覺得只看到 patch 物件而看不到 patch 解析後頁面重新渲染的操作而覺得比較無聊的話,可以先跳過 list diff 這一章節,直接跟著 patch 方法實現那一章節進行強懟,可能會比較帶勁吧!也希望小夥伴們可以和我達成共識(因為我自己原來好像也是這樣乾的)。

合格前端系列第五彈- Virtual Dom && Diff(遷移)

2、listDiff實現 O(m*n) => O(max(m, n))

首先我們得明確一下為什麼需要 list diff 這種演算法的存在,list diff 做的一件事情是怎樣的,然後它又是如何做到這麼一件事情的。

舉個例子,我有新舊兩個 Element 物件,分別為

let oldTree = el('ul', { id: 'list' }, [
  el('li', { class: 'item1' }, ['Item 1']),
  el('li', { class: 'item2' }, ['Item 2']),
  el('li', { class: 'item3' }, ['Item 3'])
])
let newTree = el('ul', { id: 'list' }, [
  el('li', { class: 'item3' }, ['Item 3']),
  el('li', { class: 'item1' }, ['Item 1']),
  el('li', { class: 'item2' }, ['Item 2'])
])
複製程式碼

如果要進行 diff 比較的話,我們直接用上面的方法就能比較出來,但我們可以看出來這裡只做了一次節點的 move。如果直接按照上面的 diff 進行比較,並且通過後面的 patch 方法進行 patch 物件的解析渲染,那麼將需要操作三次 DOM 節點才能完成檢視最後的 update。

當然,如果只有三個節點的話那還好,我們的瀏覽器還能吃的消,看不出啥效能上的區別。那麼問題來了,如果有 N 多節點,並且這些節點只是做了一小部分 removeinsertmove 的操作,那麼如果我們還是按照一一對應的 DOM 操作進行 DOM 的重新渲染,那豈不是操作太昂貴?

所以,才會衍生出 list diff 這種演算法,專門進行負責收集 removeinsertmove 操作,當然對於這個操作我們需要提前在節點的 attrs 裡面申明一個 DOM 屬性,表示該節點的唯一性。另外上張圖說明一下 list diff 的時間複雜度,小夥伴們可以看圖瞭解一下

合格前端系列第五彈- Virtual Dom && Diff(遷移)

OK,接下來我們舉個具體的例子說明一下 list diff 具體如何進行操作的,程式碼如下

let oldTree = el('ul', { id: 'list' }, [
  el('li', { key: 1 }, ['Item 1']),
  el('li', {}, ['Item']),
  el('li', { key: 2 }, ['Item 2']),
  el('li', { key: 3 }, ['Item 3'])
])
let newTree = el('ul', { id: 'list' }, [
  el('li', { key: 3 }, ['Item 3']),
  el('li', { key: 1 }, ['Item 1']),
  el('li', {}, ['Item']),
  el('li', { key: 4 }, ['Item 4'])
])
複製程式碼

對於上面例子中的新舊節點的差異對比,如果我說直接讓小夥伴們看程式碼捋清楚節點操作的流程,估計大家都會說我耍流氓。所以我整理了一幅流程圖,解釋了 list diff 具體如何進行計算節點差異的,如下

合格前端系列第五彈- Virtual Dom && Diff(遷移)

我們看圖說話,list diff 做的事情就很簡單明瞭啦。

  • 第一步,newChildrenoldChildren 的形式靠近進行操作(移動操作,程式碼中做法是直接遍歷 oldChildren 進行操作),得到 simulateChildren = [key1, 無key, null, key3]
    step1. oldChildren 第一個元素 key1 對應 newChildren 中的第二個元素
    step2. oldChildren 第二個元素 無key 對應 newChildren 中的第三個元素
    step3. oldChildren 第三個元素 key2newChildren 中找不到,直接設為 null step4. oldChildren 第四個元素 key3 對應 newChildren 中的第一個元素
  • 第二步,稍微處理一下得出的 simulateChildren,將 null 元素以及 newChildren 中的新元素加入,得到 simulateChildren = [key1, 無key, key3, key4]
  • 第三步,將得出的 simulateChildrennewChildren 的形式靠近,並將這裡的移動操作全部記錄下來(注:元素的 move 操作這裡會當成 removeinsert 操作的結合)。所以最後我們得出上圖中的一個 moves 陣列,儲存了所有節點移動類的操作。

OK,整體流程我們捋清楚了,接下來要做的事情就會簡單很多了。我們只需要用程式碼把上面列出來要做的事情得以實現即可。(注:這裡本來我是想分步驟一步一步實現,但是每一步牽扯到的東西有點多,怕到時貼出來的程式碼太多,我還是直接把 list diff 所有程式碼寫上註釋貼上吧)

/**
 * Diff two list in O(N).
 * @param {Array} oldList - 原始列表
 * @param {Array} newList - 經過一些操作的得出的新列表
 * @return {Object} - {moves: <Array>}
 *                  - moves list操作記錄的集合
 */
function diff (oldList, newList, key) {
  let oldMap = getKeyIndexAndFree(oldList, key)
  let newMap = getKeyIndexAndFree(newList, key)

  let newFree = newMap.free

  let oldKeyIndex = oldMap.keyIndex
  let newKeyIndex = newMap.keyIndex
  // 記錄所有move操作
  let moves = []

  // a simulate list
  let children = []
  let i = 0
  let item
  let itemKey
  let freeIndex = 0

  // newList 向 oldList 的形式靠近進行操作
  while (i < oldList.length) {
    item = oldList[i]
    itemKey = getItemKey(item, key)
    if (itemKey) {
      if (!newKeyIndex.hasOwnProperty(itemKey)) {
        children.push(null)
      } else {
        let newItemIndex = newKeyIndex[itemKey]
        children.push(newList[newItemIndex])
      }
    } else {
      let freeItem = newFree[freeIndex++]
      children.push(freeItem || null)
    }
    i++
  }
  let simulateList = children.slice(0)

  // 移除列表中一些不存在的元素
  i = 0
  while (i < simulateList.length) {
    if (simulateList[i] === null) {
      remove(i)
      removeSimulate(i)
    } else {
      i++
    }
  }
  // i  => new list
  // j  => simulateList
  let j = i = 0
  while (i < newList.length) {
    item = newList[i]
    itemKey = getItemKey(item, key)

    let simulateItem = simulateList[j]
    let simulateItemKey = getItemKey(simulateItem, key)

    if (simulateItem) {
      if (itemKey === simulateItemKey) {
        j++
      }
      else {
        // 如果移除掉當前的 simulateItem 可以讓 item在一個正確的位置,那麼直接移除
        let nextItemKey = getItemKey(simulateList[j + 1], key)
        if (nextItemKey === itemKey) {
          remove(i)
          removeSimulate(j)
          j++ // 移除後,當前j的值是正確的,直接自加進入下一迴圈
        } else {
          // 否則直接將item 執行 insert
          insert(i, item)
        }
      }
    // 如果是新的 item, 直接執行 inesrt
    } else {
      insert(i, item)
    }
    i++
  }
  // if j is not remove to the end, remove all the rest item
  // let k = 0;
  // while (j++ < simulateList.length) {
  //   remove(k + i);
  //   k++;
  // }

  // 記錄remove操作
  function remove (index) {
    let move = {index: index, type: 0}
    moves.push(move)
  }
  // 記錄insert操作
  function insert (index, item) {
    let move = {index: index, item: item, type: 1}
    moves.push(move)
  }
  // 移除simulateList中對應實際list中remove掉節點的元素
  function removeSimulate (index) {
    simulateList.splice(index, 1)
  }
  // 返回所有操作記錄
  return {
    moves: moves,
    children: children
  }
}
/**
 * 將 list轉變成  key-item keyIndex 物件的形式進行展示.
 * @param {Array} list
 * @param {String|Function} key
 */
function getKeyIndexAndFree (list, key) {
  let keyIndex = {}
  let free = []
  for (let i = 0, len = list.length; i < len; i++) {
    let item = list[i]
    let itemKey = getItemKey(item, key)
    if (itemKey) {
      keyIndex[itemKey] = i
    } else {
      free.push(item)
    }
  }

  // 返回 key-item keyIndex
  return {
    keyIndex: keyIndex,
    free: free
  }
}

function getItemKey (item, key) {
  if (!item || !key) return void 0
  return typeof key === 'string'
    ? item[key]
    : key(item)
}

module.exports = diff

複製程式碼

四、實現 patch,解析 patch 物件

相信還是有不少小夥伴會直接從前面的章節跳過來,為了看到 diff 後頁面的重新渲染。

如果你是仔仔細細看完了 diff 同層級元素比較之後過來的,那麼其實這裡的操作還是蠻簡單的。因為他和前面的操作思路基本一致,前面是遍歷 Element,給其唯一的標識,那麼這裡則是順著 patch 物件提供的唯一的鍵值進行解析的。直接給大家上一些深度遍歷的程式碼

function patch (rootNode, patches) {
  let walker = { index: 0 }
  walk(rootNode, walker, patches)
}

function walk (node, walker, patches) {
  let currentPatches = patches[walker.index] // 從patches取出當前節點的差異

  let len = node.childNodes
    ? node.childNodes.length
    : 0
  for (let i = 0; i < len; i++) { // 深度遍歷子節點
    let child = node.childNodes[i]
    walker.index++
    walk(child, walker, patches)
  }

  if (currentPatches) {
    dealPatches(node, currentPatches)  // 對當前節點進行DOM操作
  }
}
複製程式碼

歷史總是驚人的相似,現在小夥伴應該知道之前深度遍歷給 Element 每個節點加上唯一標識的好處了吧。OK,接下來我們根據不同型別的差異對當前節點進行操作

function dealPatches (node, currentPatches) {
  currentPatches.forEach(currentPatch => {
    switch (currentPatch.type) {
      case REPLACE:
        let newNode = (typeof currentPatch.node === 'string')
          ? document.createTextNode(currentPatch.node)
          : currentPatch.node.render()
        node.parentNode.replaceChild(newNode, node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case ATTRS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        if (node.textContent) {
          node.textContent = currentPatch.content
        } else {
          // for ie
          node.nodeValue = currentPatch.content
        }
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
}
複製程式碼

具體的 setAttrsreorder 的實現如下

function setAttrs (node, props) {
  for (let key in props) {
    if (props[key] === void 0) {
      node.removeAttribute(key)
    } else {
      let value = props[key]
      _.setAttr(node, key, value)
    }
  }
}
function reorderChildren (node, moves) {
  let staticNodeList = _.toArray(node.childNodes)
  let maps = {} // 儲存含有key特殊欄位的節點

  staticNodeList.forEach(node => {
    // 如果當前節點是ElementNode,通過maps將含有key欄位的節點進行儲存
    if (_.isElementNode(node)) {
      let key = node.getAttribute('key')
      if (key) {
        maps[key] = node
      }
    }
  })

  moves.forEach(move => {
    let index = move.index
    if (move.type === 0) { // remove item
      if (staticNodeList[index] === node.childNodes[index]) { // maybe have been removed for inserting
        node.removeChild(node.childNodes[index])
      }
      staticNodeList.splice(index, 1)
    } else if (move.type === 1) { // insert item
      let insertNode = maps[move.item.key]
        ? maps[move.item.key] // reuse old item
        : (typeof move.item === 'object')
            ? move.item.render()
            : document.createTextNode(move.item)
      staticNodeList.splice(index, 0, insertNode)
      node.insertBefore(insertNode, node.childNodes[index] || null)
    }
  })
}
複製程式碼

到這,我們的 patch 方法也得以實現了,virtual dom && diff 也算完成了,終於可以鬆一口氣了。能夠看到這裡的小夥伴們,給你們一個大大的贊。

總結

文章先從 Element 模擬 DOM 節點開始,然後通過 render 方法將 Element 還原成真實的 DOM 節點。然後再通過完成 diff 演算法,比較新舊 Element 的不同,並記錄在 patch 物件中。最後在完成 patch 方法,將 patch 物件解析,從而完成 DOMupdate

以上所有程式碼在我 githuboverwrite 專案裡面都有。

喜歡的小夥伴可以動動小手點一下 star 按鈕

QQ討論群-前端大雜燴:731175396

最後送小夥伴一句名言

合格前端系列第五彈- Virtual Dom && Diff(遷移)

相關文章