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

前言

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

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

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

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

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

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

1、實現一個 utils 方法庫

俗話說的好,磨刀不廢砍柴功,爲了後面的方便,我會在這先帶着你們實現後面常常用到的一些方法,畢竟要是每次都寫一遍用的方法,豈不得瘋,由於代碼簡單,因此這裏我就直接貼上代碼了node

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 節點的展現形式。真實節點以下git

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

咱們須要完成一個 Element 模擬上面的真實節點,形式以下github

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 對象,如圖

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

因此,纔會衍生出 list diff 這種算法,專門進行負責收集 removeinsertmove 操做,固然對於這個操做咱們須要提早在節點的 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 作的事情就很簡單明瞭啦。

  • 第一步,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

複製代碼

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)
    }
  })
}
複製代碼

具體的 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

我的準備從新撿回本身的公衆號了,以後每週保證一篇高質量好文,感興趣的小夥伴能夠關注一波。

最後送小夥伴一句名言

相關文章
相關標籤/搜索