手寫一個(gè)虛擬DOM庫,徹底讓你理解diff算法

創(chuàng)建虛擬DOM對(duì)象
{tag: '',// 元素標(biāo)簽children: [],// 子元素text: '',// 子元素是文本節(jié)點(diǎn)的話,保存文本el: null// 對(duì)應(yīng)的真實(shí)dom}
h函數(shù)根據(jù)接收的參數(shù)返回該對(duì)象即可:
export const h = (tag, children) => {let text = ''let el// 子元素是文本節(jié)點(diǎn)if (typeof children === 'string' || typeof children === 'number') {text = childrenchildren = undefined} else if (!Array.isArray(children)) {children = undefined}return {tag, // 元素標(biāo)簽children, // 子元素text, // 文本子節(jié)點(diǎn)的文本el// 真實(shí)dom}}
比如我們要?jiǎng)?chuàng)建一個(gè)div的VNode可以這樣使用:
h('div', '我是文本')h('div', [h('span')])
詳解patch過程
patch函數(shù)是我們的主函數(shù),主要用來進(jìn)行新舊VNode的對(duì)比,找到差異來更新實(shí)際DOM,它接收兩個(gè)參數(shù)。
第一個(gè)參數(shù)可以是DOM元素或者是VNode,表示舊的VNode。
第二參數(shù)表示新的VNode,一般只有第一次調(diào)用時(shí)才會(huì)傳DOM元素。
如果第一個(gè)參數(shù)為DOM元素的話我們直接忽略它的子元素把它轉(zhuǎn)為一個(gè)VNode:
export const patch = (oldVNode, newVNode) => {// dom元素if (!oldVNode.tag) {let el = oldVNodeel.innerhtml = ''oldVNode = h(oldVNode.tagName.toLowerCase())oldVNode.el = el}}
接下來新舊兩個(gè)VNode就可以進(jìn)行比較了:
export const patch = (oldNode, newNode) => {// ...patchVNode(oldVNode, newVNode)// 返回新的vnodereturn newVNode}
在patchVNode方法里我們對(duì)新舊VNode進(jìn)行比較及更新DOM。
首先如果兩個(gè)VNode的類型不同,那么不用比較,直接使用新的VNode替換舊的:
const patchVNode = (oldNode, newNode) => {if (oldVNode === newVNode) {return}// 元素標(biāo)簽相同,進(jìn)行patchif (oldVNode.tag === newVNode.tag) {// ...} else { // 類型不同那么根據(jù)新的VNode創(chuàng)建新的dom節(jié)點(diǎn),然后插入新節(jié)點(diǎn),移除舊節(jié)點(diǎn)let newEl = createEl(newVNode)let parent = oldVNode.el.parentNodeparent.insertBefore(newEl, oldVNode.el)parent.removeChild(oldVNode.el)}}
createEl方法用來遞歸的把VNode轉(zhuǎn)換成真實(shí)的DOM節(jié)點(diǎn):
const createEl = (vnode) => {let el = document.createElement(vnode.tag)vnode.el = el// 創(chuàng)建子節(jié)點(diǎn)if (vnode.children && vnode.children.length > 0) {vnode.children.forEach((item) => {el.appendChild(createEl(item))})}// 創(chuàng)建文本節(jié)點(diǎn)if (vnode.text) {el.appendChild(document.createTextNode(vnode.text))}return el}
如果類型相同,那么就要根據(jù)其子節(jié)點(diǎn)的情況來判斷進(jìn)行哪種操作。
如果新節(jié)點(diǎn)只有一個(gè)文本子節(jié)點(diǎn),那么移除舊節(jié)點(diǎn)的所有子節(jié)點(diǎn)(如果有的話),創(chuàng)建一個(gè)文本子節(jié)點(diǎn):
const patchVNode = (oldVNode, newVNode) => {// 元素標(biāo)簽相同,進(jìn)行patchif (oldVNode.tag === newVNode.tag) {// 元素類型相同,那么舊元素肯定是進(jìn)行復(fù)用的let el = newVNode.el = oldVNode.el// 新節(jié)點(diǎn)的子節(jié)點(diǎn)是文本節(jié)點(diǎn)if (newVNode.text) {// 移除舊節(jié)點(diǎn)的子節(jié)點(diǎn)if (oldVNode.children) {oldVNode.children.forEach((item) => {el.removeChild(item.el)})}// 文本內(nèi)容不相同則更新文本if (oldVNode.text !== newVNode.text) {el.textContent = newVNode.text}} else {// ...}} else { // 不同使用newNode替換oldNode// ...}}
如果新節(jié)點(diǎn)的子節(jié)點(diǎn)非文本節(jié)點(diǎn),那也有幾種情況:
1.新節(jié)點(diǎn)不存在子節(jié)點(diǎn),而舊節(jié)點(diǎn)存在,那么移除舊節(jié)點(diǎn)的子節(jié)點(diǎn);
2.新節(jié)點(diǎn)不存在子節(jié)點(diǎn),舊節(jié)點(diǎn)存在文本節(jié)點(diǎn),那么移除該文本節(jié)點(diǎn);
3.新節(jié)點(diǎn)存在子節(jié)點(diǎn),舊節(jié)點(diǎn)存在文本節(jié)點(diǎn),那么移除該文本節(jié)點(diǎn),然后插入新節(jié)點(diǎn);
4.新舊節(jié)點(diǎn)都有子節(jié)點(diǎn)的話那么就需要進(jìn)入到diff階段;
const patchVNode = (oldVNode, newVNode) => {// 元素標(biāo)簽相同,進(jìn)行patchif (oldVNode.tag === newVNode.tag) {// ...// 新節(jié)點(diǎn)的子節(jié)點(diǎn)是文本節(jié)點(diǎn)if (newVNode.text) {// ...} else {// 新節(jié)點(diǎn)不存在文本節(jié)點(diǎn)// 新舊節(jié)點(diǎn)都存在子節(jié)點(diǎn),那么就要進(jìn)行diffif (oldVNode.children && newVNode.children) {diff(el, oldVNode.children, newVNode.children)} else if (oldVNode.children) {// 新節(jié)點(diǎn)不存在子節(jié)點(diǎn),那么移除舊節(jié)點(diǎn)的所有子節(jié)點(diǎn)oldVNode.children.forEach((item) => {el.removeChild(item.el)})} else if (newVNode.children) {// 新節(jié)點(diǎn)存在子節(jié)點(diǎn)// 舊節(jié)點(diǎn)存在文本節(jié)點(diǎn)則移除if (oldVNode.text) {el.textContent = ''}// 添加新節(jié)點(diǎn)的子節(jié)點(diǎn)newVNode.children.forEach((item) => {el.appendChild(createEl(item))})} else if (oldVNode.text) {// 新節(jié)點(diǎn)啥也沒有,舊節(jié)點(diǎn)存在文本節(jié)點(diǎn)el.textContent = ''}}} else { // 不同使用newNode替換oldNode// ...}}
如果當(dāng)新舊節(jié)點(diǎn)都存在非文本的子節(jié)點(diǎn)的話,那么就要進(jìn)入到著名的diff階段了,diff算法的目的主要是用來盡可能復(fù)用舊的節(jié)點(diǎn),以減小DOM操作的開銷。
圖解diff算法
首先最簡單的diff顯然是同位置的新舊節(jié)點(diǎn)兩兩比較,但是在WEB場景下,倒序、排序、換位都是經(jīng)常有可能發(fā)生的。
所以同位置比較很多時(shí)候都很低效,無法滿足這種常見場景,各種所謂的diff算法就是用來盡量能檢查出這些情況。
然后進(jìn)行復(fù)用,snabbdom里的diff算法是一種雙端比較的策略,同時(shí)從新舊節(jié)點(diǎn)的兩端向中間開始比較,每一輪都會(huì)進(jìn)行四次比較,所以需要四個(gè)指針,如下圖:

即上述四個(gè)位置的排列組合:oldStartIdx與newStartIdx、oldStartIdx與newEndIdx、oldEndIdx與newStartIdx、oldEndIdx與newEndIdx,每當(dāng)發(fā)現(xiàn)所比較的兩個(gè)節(jié)點(diǎn)可能可以復(fù)用的話,那么就對(duì)這兩個(gè)節(jié)點(diǎn)進(jìn)行patch和相應(yīng)操作,并更新指針進(jìn)入下一輪比較,那怎么判斷兩個(gè)節(jié)點(diǎn)是否能復(fù)用呢?
這就需要使用到key了,因?yàn)楣饪词欠袷峭愋偷墓?jié)點(diǎn)是遠(yuǎn)遠(yuǎn)不夠的,因?yàn)橥粋€(gè)列表基本上類型都是一樣的,那就跟從頭開始的兩兩比較沒有區(qū)別了,先修改一下我們的h函數(shù):
export const h = (tag, data = {}, children) => {// ...let key// 文本節(jié)點(diǎn)// ...if (data && data.key) {key = data.key}return {// ...key}}
現(xiàn)在創(chuàng)建VNode的時(shí)候可以傳入key:
h('div', {key: 1}, '我是文本')
比較的終止條件也很明顯,其中一個(gè)列表已經(jīng)比較完了,也就是oldStartIdx>oldEndIdx或newStartIdx>newEndIdx,先把算法基本框架寫一下:
// 判斷兩個(gè)節(jié)點(diǎn)是否可進(jìn)行復(fù)用const isSameNode = (a, b) => {return a.key === b.key && a.tag === b.tag}// 進(jìn)行diffconst diff = (el, oldChildren, newChildren) => {// 位置指針let oldStartIdx = 0let oldEndIdx = oldChildren.length - 1let newStartIdx = 0let newEndIdx = newChildren.length - 1// 節(jié)點(diǎn)指針let oldStartVNode = oldChildren[oldStartIdx]let oldEndVNode = oldChildren[oldEndIdx]let newStartVNode = newChildren[newStartIdx]let newEndVNode = newChildren[newEndIdx]while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isSameNode(oldStartVNode, newStartVNode)) {} else if (isSameNode(oldStartVNode, newEndVNode)) {} else if (isSameNode(oldEndVNode, newStartVNode)) {} else if (isSameNode(oldEndVNode, newEndVNode)) {}}}
新增了四個(gè)變量用來保存四個(gè)位置的節(jié)點(diǎn),接下來以上圖為例來完善代碼。
第一輪會(huì)發(fā)現(xiàn)oldEndVNode與newEndVNode是可復(fù)用節(jié)點(diǎn),那么對(duì)它們進(jìn)行patch,因?yàn)槎荚谧詈蟮奈恢茫圆恍枰苿?dòng)DOM節(jié)點(diǎn),更新指針即可:
const diff = (el, oldChildren, newChildren) => {// ...while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isSameNode(oldStartVNode, newStartVNode)) {}else if (isSameNode(oldStartVNode, newEndVNode)) {}else if (isSameNode(oldEndVNode, newStartVNode)) {}else if (isSameNode(oldEndVNode, newEndVNode)) {patchVNode(oldEndVNode, newEndVNode)// 更新指針oldEndVNode = oldChildren[--oldEndIdx]newEndVNode = newChildren[--newEndIdx]}}}
此時(shí)的位置信息如下:

下一輪會(huì)發(fā)現(xiàn)oldStartIdx與newEndIdx是可復(fù)用節(jié)點(diǎn),那么對(duì)oldStartVNode和newEndVNode兩個(gè)節(jié)點(diǎn)進(jìn)行patch,同時(shí)該節(jié)點(diǎn)在新列表里的位置是當(dāng)前比較區(qū)間的最后一個(gè),所以需要把oldStartIdx的真實(shí)DOM移動(dòng)到舊列表當(dāng)前比較區(qū)間的最后,也就是oldEndVNode之后:

const diff = (el, oldChildren, newChildren) => {// ...while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isSameNode(oldStartVNode, newStartVNode)) {}else if (isSameNode(oldStartVNode, newEndVNode)) {patchVNode(oldStartVNode, newEndVNode)// 把節(jié)點(diǎn)移動(dòng)到oldEndVNode之后el.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling)// 更新指針oldStartVNode = oldChildren[++oldStartIdx]newEndVNode = newChildren[--newEndIdx]}else if (isSameNode(oldEndVNode, newStartVNode)) {}else if (isSameNode(oldEndVNode, newEndVNode)) {}}}
這輪以后位置如下:

下一輪比較很明顯oldStartVNode與newStartVNode是可復(fù)用節(jié)點(diǎn),那么對(duì)它們進(jìn)行patch,因?yàn)槎荚诘谝粋€(gè)位置,所以也不需要移動(dòng)節(jié)點(diǎn),更新指針即可:
const diff = (el, oldChildren, newChildren) => {// ...while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isSameNode(oldStartVNode, newStartVNode)) {patchVNode(oldStartVNode, newStartVNode)// 更新指針oldStartVNode = oldChildren[++oldStartIdx]newStartVNode = newChildren[++newStartIdx]}else if (isSameNode(oldStartVNode, newEndVNode)) {}else if (isSameNode(oldEndVNode, newStartVNode)) {}else if (isSameNode(oldEndVNode, newEndVNode)) {}}}
這輪過后位置如下:

再下一輪會(huì)發(fā)現(xiàn)oldEndVNode與newStartVNode是可復(fù)用節(jié)點(diǎn),在新的列表里位置變成了當(dāng)前比較區(qū)間的第一個(gè),所以patch完后需要把節(jié)點(diǎn)移動(dòng)到oldStartVNode的前面:
const diff = (el, oldChildren, newChildren) => {// ...while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isSameNode(oldStartVNode, newStartVNode)) {}else if (isSameNode(oldStartVNode, newEndVNode)) {}else if (isSameNode(oldEndVNode, newStartVNode)) {patchVNode(oldEndVNode, newStartVNode)// 把oldEndVNode節(jié)點(diǎn)移動(dòng)到oldStartVNode前el.insertBefore(oldEndVNode.el, oldStartVNode.el)// 更新指針oldEndVNode = oldChildren[--oldEndIdx]newStartVNode = newChildren[++newStartIdx]}else if (isSameNode(oldEndVNode, newEndVNode)) {}}}
這輪后位置如下:

再下一輪會(huì)發(fā)現(xiàn)四次比較都沒有發(fā)現(xiàn)可以復(fù)用的節(jié)點(diǎn),這咋辦呢,因?yàn)樽罱K我們需要讓舊列表變成新列表,所以當(dāng)前的newStartVNode如果在舊列表里沒找到可復(fù)用的,需要直接創(chuàng)建一個(gè)新節(jié)點(diǎn)插進(jìn)去,但是我們一眼就看到了舊節(jié)點(diǎn)里有c節(jié)點(diǎn)。
只是不在此輪比較的四個(gè)位置上,那么我們可以直接在舊的列表里搜索,找到了就進(jìn)行patch,并且把該節(jié)點(diǎn)移動(dòng)到當(dāng)前比較區(qū)間的第一個(gè),也就是oldStartIdx之前,這個(gè)位置空下來了就置為null,后續(xù)遍歷到就跳過,如果沒找到,那么說明這丫節(jié)點(diǎn)真的是新增的,直接創(chuàng)建該節(jié)點(diǎn)插入到oldStartIdx之前即可:
// 在列表里找到可以復(fù)用的節(jié)點(diǎn)const findSameNode = (list, node) => {return list.findIndex((item) => {return item && isSameNode(item, node)})}const diff = (el, oldChildren, newChildren) => {// ...while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {// 某個(gè)位置的節(jié)點(diǎn)為null跳過此輪比較,只更新指針if (oldStartVNode === null) {oldStartVNode = oldChildren[++oldStartIdx]} else if (oldEndVNode === null) {oldEndVNode = oldChildren[--oldEndIdx]} else if (newStartVNode === null) {newStartVNode = oldChildren[++newStartIdx]} else if (newEndVNode === null) {newEndVNode = oldChildren[--newEndIdx]}else if (isSameNode(oldStartVNode, newStartVNode)) {}else if (isSameNode(oldStartVNode, newEndVNode)) {}else if (isSameNode(oldEndVNode, newStartVNode)) {}else if (isSameNode(oldEndVNode, newEndVNode)) {}else {let findIndex = findSameNode(oldChildren, newStartVNode)// newStartVNode在舊列表里不存在,那么是新節(jié)點(diǎn),創(chuàng)建并插入之if (findIndex === -1) {el.insertBefore(createEl(newStartVNode), oldStartVNode.el)} else {// 在舊列表里存在,那么進(jìn)行patch,并且移動(dòng)到oldStartVNode前let oldVNode = oldChildren[findIndex]patchVNode(oldVNode, newStartVNode)el.insertBefore(oldVNode.el, oldStartVNode.el)// 原位置空了置為nulloldChildren[findIndex] = null}// 更新指針newStartVNode = newChildren[++newStartIdx]}}}
具體到我們的示例上,在舊的列表里找到了,所以這輪過后位置信息如下:

再下一輪比較和上輪一樣,會(huì)進(jìn)入搜索的分支,并且找到了d,所以也是path加移動(dòng)節(jié)點(diǎn),本輪過后如下:

因?yàn)閚ewStartIdx大于newEndIdx,所以while循環(huán)就結(jié)束了,但是我們發(fā)現(xiàn)舊的列表里多了g和h節(jié)點(diǎn),這兩個(gè)在新列表里沒有,所以需要把它們移除,反過來,如果新的列表里多了舊列表里沒有的節(jié)點(diǎn),那么就創(chuàng)建和插入之:
const diff = (el, oldChildren, newChildren) => {// ...while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isSameNode(oldStartVNode, newStartVNode)) {}else if (isSameNode(oldStartVNode, newEndVNode)) {}else if (isSameNode(oldEndVNode, newStartVNode)) {}else if (isSameNode(oldEndVNode, newEndVNode)) {}else {}}// 舊列表里存在新列表里沒有的節(jié)點(diǎn),需要?jiǎng)h除if (oldStartIdx <= oldEndIdx) {for(let i = oldStartIdx; i <= oldEndIdx; i++) {oldChildren[i] && el.removeChild(oldChildren[i].el)}} else if (newStartIdx <= newEndIdx) {// 新列表里存在舊列表沒有的節(jié)點(diǎn),創(chuàng)建和插入// 在newEndVNode的下一個(gè)節(jié)點(diǎn)前插入,如果下一個(gè)節(jié)點(diǎn)不存在,那么insertBefore方法會(huì)執(zhí)行appendChild的操作let before = newChildren[newEndIdx + 1] ? newChildren[newEndIdx + 1].el : nullfor(let i = newStartIdx; i <= newEndIdx; i++) {el.insertBefore(createEl(newChildren[i]), before)}}}
以上就是雙端diff的全過程,是不是還挺簡單,畫個(gè)圖就十分容易理解了。
屬性的更新
其他屬性都通過data參數(shù)傳入,先修改一下h函數(shù):
export const h = (tag, data = {}, children) => {// ...return {// ...data}}
類名
類名通過data選項(xiàng)的class字段傳遞,比如:
h('div',{class: {btn: true}}, '文本')
類名的更新在patchVNode方法里進(jìn)行,當(dāng)兩個(gè)節(jié)點(diǎn)的類型一樣,那么更新類名,替換的話就相當(dāng)于設(shè)置類名:
// 更新節(jié)點(diǎn)類名const updateClass = (el, newVNode) => {el.className = ''if (newVNode.data && newVNode.data.class) {let className = ''Object.keys(newVNode.data.class).forEach((cla) => {if (newVNode.data.class[cla]) {className += cla + ' '}})el.className = className}}const patchVNode = (oldVNode, newVNode) => {// ...// 元素標(biāo)簽相同,進(jìn)行patchif (oldVNode.tag === newVNode.tag) {let el = newVNode.el = oldVNode.el// 更新類名updateClass(el, newVNode)// ...} else { // 不同使用newNode替換oldNodelet newEl = createEl(newVNode)// 更新類名updateClass(newEl, newVNode)// ...}}
邏輯很簡單,直接把舊節(jié)點(diǎn)的類名替換成newVNode的類名。
樣式
樣式屬性使用data的style字段傳入:
h('div',{style: {fontSize: '30px'}}, '文本')
更新的時(shí)機(jī)和類名的位置一致:
// 更新節(jié)點(diǎn)樣式const updateStyle = (el, oldVNode, newVNode) => {let oldStyle = oldVNode.data.style || {}let newStyle = newVNode.data.style || {}// 移除舊節(jié)點(diǎn)里存在新節(jié)點(diǎn)里不存在的樣式Object.keys(oldStyle).forEach((item) => {if (newStyle[item] === undefined || newStyle[item] === '') {el.style[item] = ''}})// 添加舊節(jié)點(diǎn)不存在的新樣式Object.keys(newStyle).forEach((item) => {if (oldStyle[item] !== newStyle[item]) {el.style[item] = newStyle[item]}})}const patchVNode = (oldVNode, newVNode) => {// ...// 元素標(biāo)簽相同,進(jìn)行patchif (oldVNode.tag === newVNode.tag) {let el = newVNode.el = oldVNode.el// 更新樣式updateStyle(el, oldVNode, newVNode)// ...} else {let newEl = createEl(newVNode)// 更新樣式updateStyle(el, null, newVNode)// ...}}
其他屬性
其他屬性保存在data的attr字段上,更新方式及位置和樣式的完全一致:
// 更新節(jié)點(diǎn)屬性const updateAttr = (el, oldVNode, newVNode) => {let oldAttr = oldVNode && oldVNode.data.attr ? oldVNode.data.attr : {}let newAttr = newVNode.data.attr || {}// 移除舊節(jié)點(diǎn)里存在新節(jié)點(diǎn)里不存在的屬性Object.keys(oldAttr).forEach((item) => {if (newAttr[item] === undefined || newAttr[item] === '') {el.removeAttribute(item)}})// 添加舊節(jié)點(diǎn)不存在的新屬性Object.keys(newAttr).forEach((item) => {if (oldAttr[item] !== newAttr[item]) {el.setAttribute(item, newAttr[item])}})}const patchVNode = (oldVNode, newVNode) => {// ...// 元素標(biāo)簽相同,進(jìn)行patchif (oldVNode.tag === newVNode.tag) {let el = newVNode.el = oldVNode.el// 更新屬性updateAttr(el, oldVNode, newVNode)// ...} else {let newEl = createEl(newVNode)// 更新屬性updateAttr(el, null, newVNode)// ...}}
事件
最后來看一下事件的更新,事件與其他屬性不同的是如果刪除一個(gè)節(jié)點(diǎn)的話需要把它的事件先全部解綁,否則可能會(huì)存在內(nèi)存泄漏的問題,那么就需要在各個(gè)移除節(jié)點(diǎn)的時(shí)機(jī)都先解綁事件:
// 移除某個(gè)VNode對(duì)應(yīng)的dom的所有事件const removeEvent = (oldVNode) => {if (oldVNode && oldVNode.data && oldVNode.data.event) {Object.keys(oldVNode.data.event).forEach((item) => {oldVNode.el.removeEventListener(item, oldVNode.data.event[item])})}}// 更新節(jié)點(diǎn)事件const updateEvent = (el, oldVNode, newVNode) => {let oldEvent = oldVNode && oldVNode.data.event ? oldVNode.data.event : {}let newEvent = newVNode.data.event || {}// 解綁不再需要的事件Object.keys(oldEvent).forEach((item) => {if (newEvent[item] === undefined || oldEvent[item] !== newEvent[item]) {el.removeEventListener(item, oldEvent[item])}})// 綁定舊節(jié)點(diǎn)不存在的新事件Object.keys(newEvent).forEach((item) => {if (oldEvent[item] !== newEvent[item]) {el.addEventListener(item, newEvent[item])}})}const patchVNode = (oldVNode, newVNode) => {// ...// 元素標(biāo)簽相同,進(jìn)行patchif (oldVNode.tag === newVNode.tag) {// 元素類型相同,那么舊元素肯定是進(jìn)行復(fù)用的let el = newVNode.el = oldVNode.el// 更新事件updateEvent(el, oldVNode, newVNode)// ...} else {let newEl = createEl(newVNode)// 移除舊節(jié)點(diǎn)的所有事件removeEvent(oldNode)// 更新事件updateEvent(newEl, null, newVNode)// ...}}// 其他還有幾處需要添加removeEvent(),有興趣請(qǐng)看源碼
以上屬性的更新邏輯都比較粗糙,僅用于參考,可以參考snabbdom的源碼自行完善。
總結(jié)
以上代碼實(shí)現(xiàn)了一個(gè)簡單的虛擬DOM庫,詳細(xì)分解了patch過程和diff的過程,如果需要用在非瀏覽器平臺(tái)上,只要把DOM相關(guān)的操作抽象成接口,不同平臺(tái)上使用不同的接口即可,完整代碼在https://github.com/wanglin2/VNode-Demo。
感謝你的閱讀。
學(xué)習(xí)更多技能
請(qǐng)點(diǎn)擊下方公眾號(hào)
![]()

