前言
這是一篇很長的文章!!!堅持看到最後有彩蛋哦!!!
文章開篇,我們先思考一個問題,大家都說 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
一、實現一個 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
專案裡面都有。
喜歡的小夥伴可以動動小手點一下 star 按鈕
QQ討論群-前端大雜燴:731175396
最後送小夥伴一句名言