20200103pm

 20200103

 框架

 vue的dom-diff是怎麼樣實現的?

https://mp.weixin.qq.com/s/B0...css

前言

  

文章開篇,咱們先思考一個問題,你們都說 virtual dom 這,virtual dom 那的,那麼 virtual dom 究竟是啥?

  

首先,咱們得明確一點,所謂的 virtual dom,也就是虛擬節點。它經過 JS 的 Object 對象模擬 DOM 中的節點,而後再經過特定的 render 方法將其渲染成真實的 DOM 節點。

  

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

  
  

接下來,我會老規矩,邊上代碼,邊解析,帶着小夥伴們一塊兒實現一個virtual dom && diff。具體步驟以下

  

實現一個 utils 方法庫

實現一個 Element(virtual dom)

實現 diff 算法

實現 patch

1、實現一個 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

}

2、實現一個 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 對象中的 tagName,attrs,children 均可以提取出來到 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 對象,如圖

  
  

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

效果如圖  

  
  

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

  

3、實現 diff 算法

  

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

  

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

  

首先,咱們的知道的是,若是元素之間進行徹底的一個比較,即新舊 Element 對象的父元素,自己,子元素之間進行一個混雜的比較,其實現的時間複雜度爲 O(n^3)。可是在咱們前端開發中,不多會出現跨層級處理節點,因此這裏咱們會作一個同級元素之間的一個比較,則其時間複雜度則爲 O(n)。算法流程如圖所示

  
  

在這裏,咱們作同級元素比較時,可能會出現四種狀況

  

整個元素都不同,即元素被 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、實際上咱們須要對新舊元素進行一個深度的遍歷,爲每一個節點加上一個惟一的標記,具體流程如圖所示

  
  

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

  

// 設置節點惟一標識

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

控制檯結果如圖  

  
  

完整的 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 方法實現那一章節進行強懟,可能會比較帶勁吧!也但願小夥伴們能夠和我達成共識(由於我本身原來好像也是這樣乾的)。

  
  

二、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 多節點,而且這些節點只是作了一小部分 remove,insert,move 的操做,那麼若是咱們仍是按照一一對應的 DOM 操做進行 DOM 的從新渲染,那豈不是操做太昂貴?

  

因此,纔會衍生出 list diff 這種算法,專門進行負責收集 remove,insert,move 操做,固然對於這個操做咱們須要提早在節點的 attrs 裏面申明一個 DOM 屬性,表示該節點的惟一性。另外上張圖說明一下 list 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 具體如何進行計算節點差別的,以下

  
  

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

  

第一步,newChildren 向 oldChildren 的形式靠近進行操做(移動操做,代碼中作法是直接遍歷 oldChildren 進行操做),獲得 simulateChildren = \[key1, 無key, null, key3\]  

step1. oldChildren 第一個元素 key1 對應 newChildren 中的第二個元素  

step2. oldChildren 第二個元素 無key 對應 newChildren 中的第三個元素  

step3. oldChildren 第三個元素 key2 在 newChildren 中找不到,直接設爲 null 

step4. oldChildren 第四個元素 key3 對應 newChildren 中的第一個元素

第二步,稍微處理一下得出的 simulateChildren,將 null 元素以及 newChildren 中的新元素加入,獲得 simulateChildren = \[key1, 無key, key3, key4\]

第三步,將得出的 simulateChildren 向 newChildren 的形式靠近,並將這裏的移動操做所有記錄下來(注:元素的 move 操做這裏會當成 remove 和 insert 操做的結合)。因此最後咱們得出上圖中的一個 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

4、實現 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)

    }

  })

}

具體的 setAttrs 和 reorder 的實現以下

  

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 對象解析,從而完成 DOM 的 update。
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息