合格前端系列第五彈-Virtual Dom && Diff 頂 原 薦

前言

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

文章開篇,我們先思考一個問題,大家都說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。具體步驟如下

  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對象中的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便得以實現了。

三、實現diff算法

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

1、同層級元素比較(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方法實現那一章節進行強懟,可能會比較帶勁吧!也希望小夥伴們可以和我達成共識(因爲我自己原來好像也是這樣乾的)。

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多節點,並且這些節點只是做了一小部分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

四、實現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。

注:本人水平有限,悟到的也就這麼多了,如果發現哪裏出現不嚴謹的地方,歡迎各位小夥伴拍磚指出,當然喜歡的可以給個贊(收藏不讚的都是流氓哦)!

以上所有代碼在我github的overwrite項目裏面都有。

完整代碼傳送門

github-https://github.com/xuqiang521/overwrite
碼雲-http://git.oschina.net/qiangdada_129/overwrite

喜歡overwrite的可以star一波,具體幹啥,項目的homepage有介紹。

最後送小夥伴一句名言

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章