在開始解析這塊源碼的時候,先給你們補一個知識點。關於 兩顆 Virtual Dom 樹對比的策略javascript
在 ./src/snabbdom.ts
中,主要是 init 方法。html
init
方法主要是傳入 modules
,domApi
, 而後返回一個 patch
方法vue
// 鉤子 , const hooks: (keyof Module)[] = [ 'create', 'update', 'remove', 'destroy', 'pre', 'post' ];
這裏主要是註冊一系列的鉤子,在不一樣的階段觸發,細節可看 鉤子java
這裏主要是將每一個 modules 下的 hook 方法提取出來存到 cbs 裏面node
// 循環 hooks , 將每一個 modules 下的 hook 方法提取出來存到 cbs 裏面 // 返回結果 eg : cbs['create'] = [modules[0]['create'],modules[1]['create'],...]; for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { const hook = modules[j][hooks[i]]; if (hook !== undefined) { (cbs[hooks[i]] as Array<any>).push(hook); } } }
這些模塊的鉤子,主要用在更新節點的時候,會在不一樣的生命週期裏面去觸發對應的鉤子,從而更新這些模塊。例如元素的
attr、props、class
之類的!git詳細瞭解請查看模塊:模塊github
判斷是不是相同的虛擬節點算法
/** * 判斷是不是相同的虛擬節點 */ function sameVnode(vnode1: VNode, vnode2: VNode): boolean { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; }
init
方法最後返回一個 patch
方法 。segmentfault
patch
方法主要的邏輯以下 :api
pre
鉤子vnode,
則新建立空的 vnode
sameVnode
的話,則調用 patchVnode
更新 vnode
, 不然建立新節點insert
鉤子post
鉤子/** * 修補節點 */ return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node; // 用於收集全部插入的元素 const insertedVnodeQueue: VNodeQueue = []; // 先調用 pre 回調 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); // 若是老節點非 vnode , 則建立一個空的 vnode if (!isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode); } // 若是是同個節點,則進行修補 if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { // 不一樣 Vnode 節點則新建 elm = oldVnode.elm as Node; parent = api.parentNode(elm); createElm(vnode, insertedVnodeQueue); // 插入新節點,刪除老節點 if (parent !== null) { api.insertBefore( parent, vnode.elm as Node, api.nextSibling(elm) ); removeVnodes(parent, [oldVnode], 0, 0); } } // 遍歷全部收集到的插入節點,調用插入的鉤子, for (i = 0; i < insertedVnodeQueue.length; ++i) { (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks) .insert as any)(insertedVnodeQueue[i]); } // 調用post的鉤子 for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); return vnode; };
總體的流程大致上是這樣子,接下來咱們來關注更多的細節!
首先咱們研究 patchVnode
瞭解相同節點是如何更新的
patchVnode 方法主要的邏輯以下 :
prepatch
鉤子update
鉤子, 這裏主要爲了更新對應的 module
內容api.setTextContent(elm, vnode.text as string);
這裏在對比的時候,就會直接更新元素內容了。並不會等到對比完才更新 DOM 元素
具體代碼細節:
/** * 更新節點 */ function patchVnode( oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue ) { let i: any, hook: any; // 調用 prepatch 回調 if ( isDef((i = vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch)) ) { i(oldVnode, vnode); } const elm = (vnode.elm = oldVnode.elm as Node); let oldCh = oldVnode.children; let ch = vnode.children; if (oldVnode === vnode) return; // 調用 cbs 中的全部模塊的update回調 更新對應的實際內容。 if (vnode.data !== undefined) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); i = vnode.data.hook; if (isDef(i) && isDef((i = i.update))) i(oldVnode, vnode); } if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { // 新老子節點都存在的狀況,更新 子節點 if (oldCh !== ch) updateChildren( elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue ); } else if (isDef(ch)) { // 老節點不存在子節點,狀況下,新建元素 if (isDef(oldVnode.text)) api.setTextContent(elm, ''); addVnodes( elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue ); } else if (isDef(oldCh)) { // 新節點不存在子節點,狀況下,刪除元素 removeVnodes( elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1 ); } else if (isDef(oldVnode.text)) { // 若是老節點存在文本節點,而新節點不存在,因此清空 api.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) { // 子節點文本不同的狀況下,更新文本 api.setTextContent(elm, vnode.text as string); } // 調用 postpatch if (isDef(hook) && isDef((i = hook.postpatch))) { i(oldVnode, vnode); } }
一開始,看到這種寫法總有點不習慣,不事後面看着就習慣了。
if (isDef((i = data.hook)) && isDef((i = i.init))) {i(vnode);}
約等於
if(data.hook.init){data.hook.init(vnode)}
patchVnode
裏面最重要的方法,也是整個 diff
裏面的最核心方法
updateChildren
主要的邏輯以下:
優先處理特殊場景,先對比兩端。也就是
不提供 key 的狀況下,若是隻是順序改變的狀況,例如第一個移動到末尾。這個時候,會致使其實更新了後面的全部元素
具體代碼細節:
/** * 更新子節點 */ function updateChildren( parentElm: Node, oldCh: Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue ) { let oldStartIdx = 0, newStartIdx = 0; let oldEndIdx = oldCh.length - 1; let oldStartVnode = oldCh[0]; let oldEndVnode = oldCh[oldEndIdx]; let newEndIdx = newCh.length - 1; let newStartVnode = newCh[0]; let newEndVnode = newCh[newEndIdx]; let oldKeyToIdx: any; let idxInOld: number; let elmToMove: VNode; let before: any; while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode == null) { // 移動索引,由於節點處理過了會置空,因此這裏向右移 oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left } else if (oldEndVnode == null) { // 原理同上 oldEndVnode = oldCh[--oldEndIdx]; } else if (newStartVnode == null) { // 原理同上 newStartVnode = newCh[++newStartIdx]; } else if (newEndVnode == null) { // 原理同上 newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newStartVnode)) { // 從左對比 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (sameVnode(oldEndVnode, newEndVnode)) { // 從右對比 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // 最左側 對比 最右側 patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); // 移動元素到右側指針的後面 api.insertBefore( parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node) ); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // 最右側對比最左側 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); // 移動元素到左側指針的後面 api.insertBefore( parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node ); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { // 首尾都不同的狀況,尋找相同 key 的節點,因此使用的時候加上key能夠調高效率 if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx( oldCh, oldStartIdx, oldEndIdx ); } idxInOld = oldKeyToIdx[newStartVnode.key as string]; if (isUndef(idxInOld)) { // New element // 若是找不到 key 對應的元素,就新建元素 api.insertBefore( parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node ); newStartVnode = newCh[++newStartIdx]; } else { // 若是找到 key 對應的元素,就移動元素 elmToMove = oldCh[idxInOld]; if (elmToMove.sel !== newStartVnode.sel) { api.insertBefore( parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node ); } else { patchVnode( elmToMove, newStartVnode, insertedVnodeQueue ); oldCh[idxInOld] = undefined as any; api.insertBefore( parentElm, elmToMove.elm as Node, oldStartVnode.elm as Node ); } newStartVnode = newCh[++newStartIdx]; } } } // 新老數組其中一個到達末尾 if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { if (oldStartIdx > oldEndIdx) { // 若是老數組先到達末尾,說明新數組還有更多的元素,這些元素都是新增的,說以一次性插入 before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; addVnodes( parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue ); } else { // 若是新數組先到達末尾,說明新數組比老數組少了一些元素,因此一次性刪除 removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } } }
addVnodes
就比較簡單了,主要功能就是添加 Vnodes
到 真實 DOM 中
/** * 添加 Vnodes 到 真實 DOM 中 */ function addVnodes( parentElm: Node, before: Node | null, vnodes: Array<VNode>, startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue ) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx]; if (ch != null) { api.insertBefore( parentElm, createElm(ch, insertedVnodeQueue), before ); } } }
刪除 VNodes 的主要邏輯以下:
createRmCb
, 在全部監聽器執行後,才調用 api.removeChild
,刪除真正的 DOM 節點/** * 建立一個刪除的回調,屢次調用這個回調,直到監聽器都沒了,就刪除元素 */ function createRmCb(childElm: Node, listeners: number) { return function rmCb() { if (--listeners === 0) { const parent = api.parentNode(childElm); api.removeChild(parent, childElm); } }; }
/** * 刪除 VNodes */ function removeVnodes( parentElm: Node, vnodes: Array<VNode>, startIdx: number, endIdx: number ): void { for (; startIdx <= endIdx; ++startIdx) { let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx]; if (ch != null) { if (isDef(ch.sel)) { invokeDestroyHook(ch); listeners = cbs.remove.length + 1; // 全部監聽刪除 rm = createRmCb(ch.elm as Node, listeners); for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); // 若是有鉤子則調用鉤子後再調刪除回調,若是沒,則直接調用回調 if ( isDef((i = ch.data)) && isDef((i = i.hook)) && isDef((i = i.remove)) ) { i(ch, rm); } else { rm(); } } else { // Text node api.removeChild(parentElm, ch.elm as Node); } } } }
將 vnode 轉換成真正的 DOM 元素
主要邏輯以下:
/** * VNode ==> 真實DOM */ function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node { let i: any, data = vnode.data; if (data !== undefined) { // 若是存在 data.hook.init ,則調用該鉤子 if (isDef((i = data.hook)) && isDef((i = i.init))) { i(vnode); data = vnode.data; } } let children = vnode.children, sel = vnode.sel; // ! 來表明註釋 if (sel === '!') { if (isUndef(vnode.text)) { vnode.text = ''; } vnode.elm = api.createComment(vnode.text as string); } else if (sel !== undefined) { // Parse selector // 解析選擇器 const hashIdx = sel.indexOf('#'); const dotIdx = sel.indexOf('.', hashIdx); const hash = hashIdx > 0 ? hashIdx : sel.length; const dot = dotIdx > 0 ? dotIdx : sel.length; const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; // 根據 tag 建立元素 const elm = (vnode.elm = isDef(data) && isDef((i = (data as VNodeData).ns)) ? api.createElementNS(i, tag) : api.createElement(tag)); // 設置 id if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot)); // 設置 className if (dotIdx > 0) elm.setAttribute('class',sel.slice(dot + 1).replace(/\./g, ' ')); // 執行全部模塊的 create 鉤子,建立對應的內容 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) { api.appendChild( elm, createElm(ch as VNode, insertedVnodeQueue) ); } } } else if (is.primitive(vnode.text)) { // 追加文本節點 api.appendChild(elm, api.createTextNode(vnode.text)); } // 執行 vnode.data.hook 中的 create 鉤子 i = (vnode.data as VNodeData).hook; // Reuse variable if (isDef(i)) { if (i.create) i.create(emptyNode, vnode); if (i.insert) insertedVnodeQueue.push(vnode); } } else { // sel 不存在的狀況, 即爲文本節點 vnode.elm = api.createTextNode(vnode.text as string); } return vnode.elm; }
想了解在各個生命週期都有哪些鉤子,請查看:鉤子
想了解在各個生命週期裏面如何更新具體的模塊請查看:模塊