Vue 源碼中虛擬 DOM 與 Diff 算法的實現借鑑了 snabbdom
這個庫,snabbdom
是一個虛擬 DOM 庫,它專一於簡單,模塊化,強大的功能和性能。要完全明白虛擬 DOM 與 Diff 算法就得分析 snabbdom
這個庫到底作了什麼?javascript
能夠經過npm i snabbdom -D
來下載 snabbdom
庫,這樣咱們既能看到 src
下用 Typescript 編寫的源碼,也能看到編譯好的 JavaScript 代碼。下面貼的源碼是 2.1.0
版本,如今已經更新到 3.0.3
版本了。建議將下方出現的源碼複製到下載的 snabbdom
庫中相應位置,這樣看源碼比較清晰。那咱們就開始分析源碼吧。java
經過調用 snabbdom
庫中的 h
函數就能夠對真實 DOM 節點進行抽象。咱們先來看看一個完整的虛擬 DOM 節點(vnode)是什麼樣的:node
{ sel: "div", // 當前vnode的選擇器 elm: undefined, // 當前vnode對應真實的DOM節點 key: undefined, // 當前vnode的惟一標識 data: {}, // 當前vnode的屬性,樣式等 children: undefined, // 當前vnode的子元素 text: '文本內容' // 當前vnode的文本節點內容 }
實際上,h
函數的做用就是用 JavaScript 對象模擬真實的 DOM 樹,對真實 DOM 樹進行抽象。調用 h
函數就能獲得由 vnode 組成的虛擬 DOM 樹。
調用 h
函數有多種形式:算法
① h('div') ② h('div', 'text') ③ h('div', h('p')) ④ h('div', []) ⑤ h('div', {}) ⑥ h('div', {}, 'text') ⑦ h('div', {}, h('span')) ⑧ h('div', {}, [])
使得 h
函數的第二和第三個參數比較靈活,要判斷的狀況也比較多,下面把這部分的核心源碼分析貼一下:npm
// h函數:根據傳入的參數推測出h函數的調用形式以及每一個vnode對應屬性的屬性值 export function h(sel: string): VNode export function h(sel: string, data: VNodeData | null): VNode export function h(sel: string, children: VNodeChildren): VNode export function h(sel: string, data: VNodeData | null, children: VNodeChildren): VNode export function h(sel: any, b?: any, c?: any): VNode { var data: VNodeData = {}; var children: any; var text: any; var i: number // c有值,狀況有:⑥ ⑦ ⑧ if (c !== undefined) { // c有值的狀況下b有值,狀況有:⑥ ⑦ ⑧ if (b !== null) { // 將b賦值給data data = b } // c的數據類型是數組,狀況有:⑧ if (is.array(c)) { children = c // 判斷c是文本節點,狀況有:⑥ } else if (is.primitive(c)) { text = c // 狀況有:⑦,⑦這條語句會先執行h('span')代碼,直接調用vnode函數,調用後會返回{sel: 'span'}, // 這時c有值而且c而且含有sel屬性 } else if (c && c.sel) { // 注:這裏的c不是h('span'),而是h('span')的返回值,是個{ sel: 'span' }這樣的對象, // 最後組裝成數組賦值給children children = [c] } // c沒有值,b有值,狀況有:② ③ ④ ⑤ } else if (b !== undefined && b !== null) { // b的數據類型是數組,狀況有:④ if (is.array(b)) { children = b // 判斷b是文本節點,狀況有:② } else if (is.primitive(b)) { text = b // 狀況有:③,③這條語句會先執行h('p')代碼,直接調用vnode函數,調用後會返回{sel: 'p'}, // 這時b有值而且b而且含有sel屬性 } else if (b && b.sel) { // 注:這裏的b不是h('p'),而是h('p')的返回值,是個{ sel: 'p' }這樣的對象, // 最後組裝成數組賦值給children children = [b] // 狀況有:⑤,將b賦值給data } else { data = b } } // children有值,遍歷children if (children !== undefined) { for (i = 0; i < children.length; ++i) { // 判斷children中的每一項的數據類型是不是string/number,調用vnode函數 if (is.primitive(children[i])) { children[i] = vnode(undefined, undefined, undefined, children[i], undefined) } } } /** * 調用vnode後返回形如 * { * sel: 'div', * data: { style: '#000' }, * children: undefined, * text: 'text', * elm: undefined, * key: undefined * } * 這樣的JavaScript對象 */ return vnode(sel, data, children, text, undefined); }
// vnode函數:根據傳入的參數組裝vnode結構 export function vnode(sel: string | undefined, data: any | undefined, children: Array<VNode | string> | undefined, text: string | undefined, elm: Element | Text | undefined): VNode { // 判斷data是否有值,有值就將data.key賦值給key,無值就將undefined賦值給key const key = data === undefined ? undefined : data.key // 將傳入vnode函數的參數組裝成一個對象返回 return { sel, data, children, text, elm, key } }
經過 h
函數獲得新舊虛擬節點 DOM 對象後就能夠進行差別比較了。在實際使用過程當中,咱們是直接調用 snabbdom
的 patch
函數,而後傳入兩個參數,經過 patch
函數內部處理就能夠獲得新舊虛擬節點 DOM 對象的差別,並將差別部分更新到真正的 DOM 樹上。api
首先,patch
函數會去判斷 oldVnode
是不是真實DOM節點,若是是則須要先轉換爲虛擬DOM節點 oldVnode = emptyNodeAt(oldVnode)
;而後去比較新舊 vnode 是不是同一個節點 sameVnode(oldVnode, vnode)
,若是是同一節點則精確比較新舊 vnode patchVnode(oldVnode, vnode, insertedVnodeQueue)
,若是不是則直接建立新 vnode 對應的真實 DOM 節點 createElm(vnode, insertedVnodeQueue)
,在 createElm 函數中建立新 vnode 的真實 DOM 節點以及它對應的子節點,並把子節點插入到相應位置。若是 oldVnode.elm 有父節點則把新 vnode 對應的真實 DOM 節點做爲子節點插入到相應位置,而且刪除舊節點。下面貼一下 patch
函數的源碼解析:數組
function patch(oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node const insertedVnodeQueue: VNodeQueue = [] for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]() // isVnode(oldVnode)判斷oldVnode.sel是否存在,不存在表示oldVnode是真實的DOM節點 if (!isVnode(oldVnode)) { // oldVnode多是真實的DOM節點,也多是舊的虛擬DOM節點, // 若是是真實的DOM節點要調用vnode函數組裝成虛擬DOM節點 oldVnode = emptyNodeAt(oldVnode) } // 判斷出是同一個虛擬DOM節點 if (sameVnode(oldVnode, vnode)) { // 精確比較兩個虛擬DOM節點 patchVnode(oldVnode, vnode, insertedVnodeQueue) } else { // oldVnode.elm是虛擬DOM節點對應的真實DOM節點 elm = oldVnode.elm! // api.parentNode(elm)獲取elm的父節點elm.parentNode parent = api.parentNode(elm) as Node // 建立vnode下真實DOM節點並更新到相應位置 createElm(vnode, insertedVnodeQueue) // elm的父節點存在 if (parent !== null) { // api.nextSibling(elm)-->elm.nextSibling 返回緊跟在elm以後的節點 // api.insertBefore(parent, B, C)-->-->parent.insertBefore(B, C),將B節點插入到C節點以前 api.insertBefore(parent, vnode.elm!, api.nextSibling(elm)) removeVnodes(parent, [oldVnode], 0, 0) // 刪除舊的DOM節點 } } for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]) } for (i = 0; i < cbs.post.length; ++i) cbs.post[i]() return vnode }
patch
函數中用到了 emptyNodeAt
函數,這個函數主要是處理 patch
函數第一個參數爲真實DOM節點的狀況。下面貼一下這個函數的源碼解析:app
function emptyNodeAt(elm: Element) { // 判斷傳入的DOM節點elm有沒有id屬性,由於虛擬DOM節點的sel屬性是選擇器,例如:div#wrap const id = elm.id ? '#' + elm.id : '' // 判斷傳入的ODM節點elm有沒有class屬性,由於虛擬DOM節點的sel屬性是選擇器,例如:div.wrap const c = elm.className ? '.' + elm.className.split(' ').join('.') : '' // 調用vnode函數將傳入的DOM節點組裝成虛擬DOM節點 return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm) }
patch
函數中用到了 sameVnode
函數,這個函數主要用來比較兩個虛擬DOM節點是不是同一個虛擬節點。下面貼一下這個函數的源碼分析:dom
function sameVnode(vnode1: VNode, vnode2: VNode): boolean { // 判斷vnode1和vnode2是不是同一個虛擬DOM節點 return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel }
根據 sameVnode
函數返回的結果,新舊 vnode 不是同一個虛擬節點。首先獲取到 oldVnode 對應真實 DOM 節點的父節點 parent
,而後調用 createElm
函數去建立 vnode 對應的真實 DOM 節點以及它的子節點和標籤屬性等等。判斷是否有 parent
, 若是有則將 vnode.elm 對應的DOM節點做爲子節點插入到 parent
節點下的相應位置。部分源碼分析在patch
函數中,下面貼一下 createElm
函數的源碼分析:模塊化
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node { let i: any let data = vnode.data if (data !== undefined) { const init = data.hook?.init if (isDef(init)) { init(vnode) data = vnode.data } } const children = vnode.children const sel = vnode.sel // 判斷sel值中是否包含! if (sel === '!') { if (isUndef(vnode.text)) { vnode.text = '' } // --> document.createComment(vnode.text!)建立註釋節點 vnode.elm = api.createComment(vnode.text!) } else if (sel !== undefined) { // 解析sel選擇器 // 查找sel屬性值中#的索引,沒找到返回-1 const hashIdx = sel.indexOf('#') // hashIdx做爲起始位置查找sel屬性值中.的索引,若是hashIdx < 0 那麼從位置0開始查找 const dotIdx = sel.indexOf('.', hashIdx) const hash = hashIdx > 0 ? hashIdx : sel.length const dot = dotIdx > 0 ? dotIdx : sel.length // 若id選擇器或class選擇器存在,則從0位開始截取到最小索引值的位置結束,截取出的就是標籤名稱 // 都不存在直接取sel值 const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel // 根據tag名建立DOM元素 const elm = vnode.elm = isDef(data) && isDef(i = data.ns) ? api.createElementNS(i, tag) : api.createElement(tag) // 設置id屬性 if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot)) // 設置calss屬性 if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' ')) for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode) // 判斷children是不是數組,是數組則遍歷children if (is.array(children)) { for (i = 0; i < children.length; ++i) { const ch = children[i] if (ch != null) { // createElm(ch as VNode, insertedVnodeQueue)遞歸建立子節點 // api.appendChild(A, B)-->A.appendChild(B)將B節點插入到指定父節點A的子節點列表的末尾 api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue)) } } // 判斷vnode.text有沒有值 } else if (is.primitive(vnode.text)) { // api.createTextNode(vnode.text)根據vnode.text建立文本節點 // api.appendChild(elm, B)-->A.appendChild(B)將文本節點B添加到父節點elm子節點列表的末尾處 api.appendChild(elm, api.createTextNode(vnode.text)) } const hook = vnode.data!.hook if (isDef(hook)) { hook.create?.(emptyNode, vnode) if (hook.insert) { insertedVnodeQueue.push(vnode) } } } else { // sel不存在直接建立文本節點 vnode.elm = api.createTextNode(vnode.text!) } return vnode.elm }
上面分析了新舊 vnode 不是同一個虛擬節點,那麼是同一個虛擬節點又該怎麼去處理?首先,調用 patchVnode
函數 patchVnode(oldVnode, vnode, insertedVnodeQueue)
,這個函數會對新舊 vnode 進行精確比較:
① 若是新舊虛擬 DOM 對象全等 oldVnode === vnode
,那麼不作任何操做,直接返回;
② 而後判斷 vnode 是否有文本節點 isUndef(vnode.text)
,若是沒有文本節點則判斷 oldVnode 與 vnode 有沒有子節點 isDef(oldCh) && isDef(ch)
,若是都有子節點且不相等則調用 updateChildren
函數去更新子節點;
③ 若是隻有 vnode 有子節點而 oldVnode 有文本節點或沒有內容,將 oldVnode 的文本節點置空或不作處理,調用 addVnodes
函數將 vnode 的子節點建立出對應的真實DOM並循環插入到父節點下;
④ 若是隻有 oldVnode 有子節點而 vnode 沒有內容,則直接刪除 oldVnode 下的子節點;
⑤ 若是隻有 oldVnode 有文本節點而 vnode 沒有內容,則將 oldVnode 對應的真實 DOM 節點的文本置空;
⑥ 若是 vnode 有文本節點,oldVnode 有子節點就將對應真實 DOM 節點的子節點刪除,沒有就不處理,而後將 vnode 的文本節點做爲子節點插入到對應真實 DOM 節點下。
部分源碼分析在patch
函數中,下面貼一下 patchVnode
函數的源碼分析:
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) { const hook = vnode.data?.hook hook?.prepatch?.(oldVnode, vnode) const elm = vnode.elm = oldVnode.elm! const oldCh = oldVnode.children as VNode[] const ch = vnode.children as VNode[] // oldVnode與vnode徹底相等並無須要更新的內容則直接返回,不作處理 if (oldVnode === vnode) return if (vnode.data !== undefined) { for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) vnode.data.hook?.update?.(oldVnode, vnode) } // vnode.text爲undefined表示vnode虛擬節點沒有文本內容 if (isUndef(vnode.text)) { // oldCh與ch都不爲undefined表示oldVnode與vnode都有虛擬子節點children if (isDef(oldCh) && isDef(ch)) { // oldCh !== ch 利用算法去更新子節點 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) } else if (isDef(ch)) { // 將oldVnode的文本節點設置爲'' if (isDef(oldVnode.text)) api.setTextContent(elm, '') // 調用addVnodes方法將vnode的虛擬子節點循環插入到elm節點的子列表下 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) // oldCh不爲undefined表示oldVnode有虛擬子節點children } else if (isDef(oldCh)) { // vnode沒有children則直接刪除oldVnode的children removeVnodes(elm, oldCh, 0, oldCh.length - 1) // oldVnode.text有值而vnode.text沒有值 } else if (isDef(oldVnode.text)) { // 將oldVnode的文本節點設置爲'' api.setTextContent(elm, '') } // oldVnode與vnode文本節點內容不一樣 } else if (oldVnode.text !== vnode.text) { // isDef(oldCh)-->oldCh !== undefined 代表oldVnode虛擬節點下有虛擬子節點 if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1) } // oldCh虛擬節點下沒有虛擬子節點則直接更新文本內容 api.setTextContent(elm, vnode.text!) } hook?.postpatch?.(oldVnode, vnode) }
當新舊 vnode 都有子節點時,diff
算法定義了四個指針來處理子節點,四個指針分別是:oldStartVnode(舊前vnode)
/newStartVnode(新前vnode)
/oldEndVnode(舊後vnode)
/newEndVnode(新後vnode)
。進入循環體內後,新舊 vnode 的子節點兩兩比較,這裏提供了一套比較規則,以下圖:
若是上面四個規則都不知足,將 oldVnode 的子節點從舊的前索引 oldStartIdx
到舊的後索引 oldEndIdx
作一個 key 與對應位置序號的映射 oldKeyToIdx
,經過新 vnode 的 key 去找 oldKeyToIdx
中是否有對應的索引值,若沒有,代表 oldVnode
沒有對應的舊節點,是一個新增的節點,進行插入操做;如有,代表 oldVnode
有對應的舊節點,不是一個新增節點,進行移動操做。下面貼一下源碼解析:
// 舊vnode的子節點的前索引oldStartIdx到後索引oldEndIdx的key與對應位置序號的映射關係 function createKeyToOldIdx(children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap { const map: KeyToIndexMap = {} for (let i = beginIdx; i <= endIdx; ++i) { const key = children[i]?.key if (key !== undefined) { map[key] = i } } /** * 例如:map = { A: 1, B: 2 } */ return map }
function updateChildren(parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue) { let oldStartIdx = 0 // 舊的前索引 let newStartIdx = 0 // 新的前索引 let oldEndIdx = oldCh.length - 1 // 舊的後索引 let newEndIdx = newCh.length - 1 // 新的後索引 let oldStartVnode = oldCh[0] // 舊的前vnode let newStartVnode = newCh[0] // 新的前vnode let oldEndVnode = oldCh[oldEndIdx] // 舊的後vnode let newEndVnode = newCh[newEndIdx] // 新的後vnode let oldKeyToIdx: KeyToIndexMap | undefined let idxInOld: number let elmToMove: VNode let before: any // 當舊的前索引 <= 舊的後索引 && 新的前索引 <= 新的後索引時執行循環語句 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 爲何oldStartVnode == null? // 由於虛擬節點進行移動操做後要將原來的虛擬節點置爲undefined了 // oldCh[idxInOld] = undefined as any if (oldStartVnode == null) { // oldStartVnode爲null就過濾掉當前節點,取oldCh[++oldStartIdx]節點(舊的前索引的下一個索引的節點) oldStartVnode = oldCh[++oldStartIdx] } else if (oldEndVnode == null) { // oldEndVnode爲null就過濾掉當前節點,取oldCh[--oldEndIdx]節點(舊的後索引的上一個索引的節點) oldEndVnode = oldCh[--oldEndIdx] } else if (newStartVnode == null) { // newStartVnode爲null就過濾掉當前節點,取newCh[++newStartIdx]節點(新的前索引的下一個索引的節點) newStartVnode = newCh[++newStartIdx] } else if (newEndVnode == null) { // newEndVnode爲null就過濾掉當前節點,取newCh[--newEndIdx]節點(新的後索引的上一個索引的節點) newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { /** * ① 舊的前vnode(oldStartVnode) 與 新的前vnode(newStartVnode) 比較是不是同一個虛擬節點 * 舊的虛擬子節點 新的虛擬子節點 * h('li', { key: 'A' }, 'A') h('li', { key: 'A' }, 'A') * h('li', { key: 'B' }, 'B') h('li', { key: 'B' }, 'B') */ // 若是判斷是同一個虛擬節點則調用patchVnode函數 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) // oldCh[++oldStartIdx]取舊的前索引節點的下一個虛擬節點(例子中key爲B的節點),賦值給oldStartVnode oldStartVnode = oldCh[++oldStartIdx] // oldCh[++oldStartIdx]取新的前索引節點的下一個虛擬節點(例子中key爲B的節點),賦值給newStartVnode newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { /** * 若是舊的前vnode(例子中key爲B的虛擬節點) 與 新的前vnode(例子中key爲B的虛擬節點) * 不是同一個虛擬節點則進行方案②比較 * ② 舊的後vnode(oldEndVnode) 與 新的後vnode(newEndVnode) 比較是不是同一個虛擬節點 * 舊的虛擬子節點 新的虛擬子節點 * h('li', { key: 'C' }, 'C') h('li', { key: 'A' }, 'A') * h('li', { key: 'B' }, 'B') h('li', { key: 'B' }, 'B') */ // 若是判斷是同一個虛擬節點則調用patchVnode函數 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) // oldCh[--oldEndIdx]取舊的後索引節點的上一個虛擬節點(例子中key爲C的虛擬節點),賦值給oldEndVnode oldEndVnode = oldCh[--oldEndIdx] // newCh[--newEndIdx]取新的後索引節點的上一個虛擬節點(例子中key爲A的虛擬節點),賦值給newEndVnode newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { /** * 若是舊的後vnode 與 新的後vnode 不是同一個虛擬節點則進行方案③比較 * ③ 舊的前vnode(oldStartVnode) 與 新的後vnode(newEndVnode) 比較是不是同一個虛擬節點 * 舊的虛擬子節點 新的虛擬子節點 * h('li', { key: 'C' }, 'C') h('li', { key: 'A' }, 'A') * h('li', { key: 'B' }, 'B') h('li', { key: 'B' }, 'B') * h('li', { key: 'C' }, 'C') */ // 若是判斷是同一個虛擬節點則調用patchVnode函數 patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) // 將舊的前vnode(至關於例子中key爲C的虛擬節點)插入到當前舊的後vnode的下一個兄弟節點的前面 // 若是oldEndVnode是最末尾的虛擬節點,則node.nextSibling會返回null, // 則新的虛擬節點直接插入到最末尾,等同於appenChild api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!)) // oldCh[++oldStartIdx]取舊的前索引虛擬節點的下一個虛擬節點(例子中key爲B的虛擬節點),賦值給oldStartVnode oldStartVnode = oldCh[++oldStartIdx] // newCh[--newEndIdx]取新的後索引虛擬節點的上一個虛擬節點(例子中key爲B的虛擬節點),賦值給newEndVnode newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left /** * 若是舊的前vnode 與 新的後vnode 不是同一個虛擬節點則進行方案④比較 * ④ 舊的後vnode(oldEndVnode) 與 新的前vnode(newStartVnode) 比較是不是同一個虛擬節點 * 舊的虛擬子節點 新的虛擬子節點 * h('li', { key: 'C' }, 'C') h('li', { key: 'B' }, 'B') * h('li', { key: 'B' }, 'B') h('li', { key: 'A' }, 'A') * h('li', { key: 'C' }, 'C') */ // 若是判斷是同一個虛擬節點則調用patchVnode函數 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) // 將舊的後vnode(例子中key爲B)插入到當前舊的前vnode(例子中key爲C)的前面 api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!) // oldCh[--oldEndIdx]取舊的後索引節點的上一個虛擬節點(例子中key爲C的虛擬節點),賦值給oldEndVnode oldEndVnode = oldCh[--oldEndIdx] // newCh[++newStartIdx]取新的前索引節點的下一個虛擬節點(例子中key爲A的虛擬節點),賦值給newStartVnode newStartVnode = newCh[++newStartIdx] } else { // 不知足以上四種狀況 if (oldKeyToIdx === undefined) { // oldKeyToIdx保存舊的children中各個節點的key與對應位置序號的映射關係 oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) } // 從oldKeyToIdx中獲取當前newStartVnode節點key對應的序號 idxInOld = oldKeyToIdx[newStartVnode.key as string] if (isUndef(idxInOld)) { // isUndef(idxInOld) --> idxInOld === undefined /** * idxInOld = undefined 要插入節點 * 舊的虛擬子節點中沒有idxInOld對應的節點,而新的虛擬子節點中有, * 因此newStartVnode是須要插入的虛擬節點 * 舊的虛擬子節點 新的虛擬子節點 * h('li', { key: 'A' }, 'A') h('li', { key: 'C' }, 'C') * h('li', { key: 'B' }, 'B') */ // 根據newStartVnode(例子中key爲C的虛擬節點)建立真實DOM節點createElm(), // 將建立的DOM節點插入到oldStartVnode.elm(例子中key爲A的節點)的前面 api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!) } else { /** * idxInOld != undefined 要移動節點 * 舊的虛擬子節點中有idxInOld對應的節點,因此oldCh[idxInOld]是須要移動的虛擬節點 * 舊的虛擬子節點 新的虛擬子節點 * h('li', { key: 'A' }, 'A') h('div', { key: 'B' }, 'B') * h('li', { key: 'B' }, 'B') h('li', { key: 'D' }, 'D') */ elmToMove = oldCh[idxInOld] // elmToMove保存要移動的虛擬節點 // 判斷elmToMove與newStartVnode在key相同的狀況下sel屬性是否相同 if (elmToMove.sel !== newStartVnode.sel) { // sel屬性不相同代表不是同一個虛擬節點, // 根據newStartVnode虛擬節點建立真實DOM節點並插入到oldStartVnode.elm(舊的key爲A的節點)以前 api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!) } else { // key與sel相同表示是同一個虛擬節點,調用patchVnode函數 patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) // 處理完被移動的虛擬節點oldCh[idxInOld]要設置爲undefined,方便下次循環處理時過濾掉已經處理的節點 oldCh[idxInOld] = undefined as any // 將elmToMove.elm(例子中舊的key爲B的節點)插入到oldStartVnode.elm(例子中key爲A的節點)的前面 api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!) } } // 取newCh[++newStartIdx]虛擬節點(例子中key爲D的虛擬節點)賦值給newStartVnode newStartVnode = newCh[++newStartIdx] } } /** * 循環結束後舊的前索引 <= 舊的後索引 || 新的前索引 <= 新的後索引, * 表示還有部分虛擬節點(例子中key爲C的虛擬節點)沒處理 * 舊的虛擬子節點 新的虛擬子節點 * 狀況一: * h('li', { key: 'A' }, 'A') h('li', { key: 'A' }, 'A') * h('li', { key: 'B' }, 'B') h('li', { key: 'B' }, 'B') * h('li', { key: 'D' }, 'D') h('li', { key: 'C' }, 'C') * h('li', { key: 'D' }, 'D') * 狀況二: * h('li', { key: 'A' }, 'A') h('li', { key: 'A' }, 'A') * h('li', { key: 'B' }, 'B') h('li', { key: 'B' }, 'B') * h('li', { key: 'C' }, 'C') */ if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { // 處理例子中狀況一 if (oldStartIdx > oldEndIdx) { // 待插入的節點以before節點爲參照,newCh[newEndIdx]是例子中新的子節點中key爲C的虛擬節點, // 因此before = newCh[newEndIdx + 1]是key爲D的虛擬節點 before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm // 例子中如今newStartIdx,newEndIdx都爲2 addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else { // 處理例子中狀況二,刪除舊的前索引到舊的後索引中間的節點(例子中刪除舊的key爲C的虛擬節點) removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } } }