摘要:就近復(fù)用為了盡可能不發(fā)生的移動,會就近復(fù)用相同的節(jié)點(diǎn),復(fù)用的依據(jù)是判斷是否是同類型的元素方法在中,主要是方法。例如元素的之類的詳細(xì)了解請查看模塊模塊判斷是否是相同的虛擬節(jié)點(diǎn)判斷是否是相同的虛擬節(jié)點(diǎn)方法最后返回一個方法。
patch 方法 前言
在開始解析這塊源碼的時候,先給大家補(bǔ)一個知識點(diǎn)。關(guān)于 兩顆 Virtual Dom 樹對比的策略
diff 策略同級對比
對比的時候,只針對同級的對比,減少算法復(fù)雜度。
就近復(fù)用
為了盡可能不發(fā)生 DOM 的移動,會就近復(fù)用相同的 DOM 節(jié)點(diǎn),復(fù)用的依據(jù)是判斷是否是同類型的 dom 元素
在 ./src/snabbdom.ts 中,主要是 init 方法。
init 方法主要是傳入 modules ,domApi , 然后返回一個 patch 方法
注冊鉤子// 鉤子 , const hooks: (keyof Module)[] = [ "create", "update", "remove", "destroy", "pre", "post" ];
這里主要是注冊一系列的鉤子,在不同的階段觸發(fā),細(xì)節(jié)可看 鉤子
將各個模塊的鉤子方法,掛到統(tǒng)一的鉤子上這里主要是將每個 modules 下的 hook 方法提取出來存到 cbs 里面
初始化的時候,將每個 modules 下的相應(yīng)的鉤子都追加都一個數(shù)組里面。create、update....
在進(jìn)行 patch 的各個階段,觸發(fā)對應(yīng)的鉤子去處理對應(yīng)的事情
這種方式比較方便擴(kuò)展。新增鉤子的時候,不需要更改到主要的流程
// 循環(huán) hooks , 將每個 modules 下的 hook 方法提取出來存到 cbs 里面 // 返回結(jié)果 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).push(hook); } } }
這些模塊的鉤子,主要用在更新節(jié)點(diǎn)的時候,會在不同的生命周期里面去觸發(fā)對應(yīng)的鉤子,從而更新這些模塊。sameVnode例如元素的 attr、props、class 之類的!
詳細(xì)了解請查看模塊:模塊
判斷是否是相同的虛擬節(jié)點(diǎn)
/** * 判斷是否是相同的虛擬節(jié)點(diǎn) */ function sameVnode(vnode1: VNode, vnode2: VNode): boolean { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; }patch
init 方法最后返回一個 patch 方法 。
patch 方法主要的邏輯如下 :
觸發(fā) pre 鉤子
如果老節(jié)點(diǎn)非 vnode, 則新創(chuàng)建空的 vnode
新舊節(jié)點(diǎn)為 sameVnode 的話,則調(diào)用 patchVnode 更新 vnode , 否則創(chuàng)建新節(jié)點(diǎn)
觸發(fā)收集到的新元素 insert 鉤子
觸發(fā) post 鉤子
/** * 修補(bǔ)節(jié)點(diǎn) */ return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node; // 用于收集所有插入的元素 const insertedVnodeQueue: VNodeQueue = []; // 先調(diào)用 pre 回調(diào) for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); // 如果老節(jié)點(diǎn)非 vnode , 則創(chuàng)建一個空的 vnode if (!isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode); } // 如果是同個節(jié)點(diǎn),則進(jìn)行修補(bǔ) if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { // 不同 Vnode 節(jié)點(diǎn)則新建 elm = oldVnode.elm as Node; parent = api.parentNode(elm); createElm(vnode, insertedVnodeQueue); // 插入新節(jié)點(diǎn),刪除老節(jié)點(diǎn) if (parent !== null) { api.insertBefore( parent, vnode.elm as Node, api.nextSibling(elm) ); removeVnodes(parent, [oldVnode], 0, 0); } } // 遍歷所有收集到的插入節(jié)點(diǎn),調(diào)用插入的鉤子, for (i = 0; i < insertedVnodeQueue.length; ++i) { (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks) .insert as any)(insertedVnodeQueue[i]); } // 調(diào)用post的鉤子 for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); return vnode; };
整體的流程大體上是這樣子,接下來我們來關(guān)注更多的細(xì)節(jié)!
patchVnode 方法首先我們研究 patchVnode 了解相同節(jié)點(diǎn)是如何更新的
patchVnode 方法主要的邏輯如下 :
觸發(fā) prepatch 鉤子
觸發(fā) update 鉤子, 這里主要為了更新對應(yīng)的 module 內(nèi)容
非文本節(jié)點(diǎn)的情況 , 調(diào)用 updateChildren 更新所有子節(jié)點(diǎn)
文本節(jié)點(diǎn)的情況 , 直接 api.setTextContent(elm, vnode.text as string);
這里在對比的時候,就會直接更新元素內(nèi)容了。并不會等到對比完才更新 DOM 元素
具體代碼細(xì)節(jié):
/** * 更新節(jié)點(diǎn) */ function patchVnode( oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue ) { let i: any, hook: any; // 調(diào)用 prepatch 回調(diào) 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; // 調(diào)用 cbs 中的所有模塊的update回調(diào) 更新對應(yīng)的實(shí)際內(nèi)容。 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)) { // 新老子節(jié)點(diǎn)都存在的情況,更新 子節(jié)點(diǎn) if (oldCh !== ch) updateChildren( elm, oldCh as Array, ch as Array , insertedVnodeQueue ); } else if (isDef(ch)) { // 老節(jié)點(diǎn)不存在子節(jié)點(diǎn),情況下,新建元素 if (isDef(oldVnode.text)) api.setTextContent(elm, ""); addVnodes( elm, null, ch as Array , 0, (ch as Array ).length - 1, insertedVnodeQueue ); } else if (isDef(oldCh)) { // 新節(jié)點(diǎn)不存在子節(jié)點(diǎn),情況下,刪除元素 removeVnodes( elm, oldCh as Array , 0, (oldCh as Array ).length - 1 ); } else if (isDef(oldVnode.text)) { // 如果老節(jié)點(diǎn)存在文本節(jié)點(diǎn),而新節(jié)點(diǎn)不存在,所以清空 api.setTextContent(elm, ""); } } else if (oldVnode.text !== vnode.text) { // 子節(jié)點(diǎn)文本不一樣的情況下,更新文本 api.setTextContent(elm, vnode.text as string); } // 調(diào)用 postpatch if (isDef(hook) && isDef((i = hook.postpatch))) { i(oldVnode, vnode); } }
一開始,看到這種寫法總有點(diǎn)不習(xí)慣,不過后面看著就習(xí)慣了。updateChildren 方法if (isDef((i = data.hook)) && isDef((i = i.init))) {i(vnode);}
約等于
if(data.hook.init){data.hook.init(vnode)}
patchVnode 里面最重要的方法,也是整個 diff 里面的最核心方法
updateChildren 主要的邏輯如下:
優(yōu)先處理特殊場景,先對比兩端。也就是
舊 vnode 頭 vs 新 vnode 頭
舊 vnode 尾 vs 新 vnode 尾
舊 vnode 頭 vs 新 vnode 尾
舊 vnode 尾 vs 新 vnode 頭
首尾不一樣的情況,尋找 key 相同的節(jié)點(diǎn),找不到則新建元素
如果找到 key,但是,元素選擇器變化了,也新建元素
如果找到 key,并且元素選擇沒變, 則移動元素
兩個列表對比完之后,清理多余的元素,新增添加的元素
不提供 key 的情況下,如果只是順序改變的情況,例如第一個移動到末尾。這個時候,會導(dǎo)致其實(shí)更新了后面的所有元素
具體代碼細(xì)節(jié):
/** * 更新子節(jié)點(diǎn) */ function updateChildren( parentElm: Node, oldCh: ArrayaddVnodes 方法, newCh: Array , 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) { // 移動索引,因?yàn)楣?jié)點(diǎn)處理過了會置空,所以這里向右移 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 // 最左側(cè) 對比 最右側(cè) patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); // 移動元素到右側(cè)指針的后面 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 // 最右側(cè)對比最左側(cè) patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); // 移動元素到左側(cè)指針的后面 api.insertBefore( parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node ); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { // 首尾都不一樣的情況,尋找相同 key 的節(jié)點(diǎn),所以使用的時候加上key可以調(diào)高效率 if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx( oldCh, oldStartIdx, oldEndIdx ); } idxInOld = oldKeyToIdx[newStartVnode.key as string]; if (isUndef(idxInOld)) { // New element // 如果找不到 key 對應(yīng)的元素,就新建元素 api.insertBefore( parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node ); newStartVnode = newCh[++newStartIdx]; } else { // 如果找到 key 對應(yīng)的元素,就移動元素 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]; } } } // 新老數(shù)組其中一個到達(dá)末尾 if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { if (oldStartIdx > oldEndIdx) { // 如果老數(shù)組先到達(dá)末尾,說明新數(shù)組還有更多的元素,這些元素都是新增的,說以一次性插入 before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm; addVnodes( parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue ); } else { // 如果新數(shù)組先到達(dá)末尾,說明新數(shù)組比老數(shù)組少了一些元素,所以一次性刪除 removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } } }
addVnodes 就比較簡單了,主要功能就是添加 Vnodes 到 真實(shí) DOM 中
/** * 添加 Vnodes 到 真實(shí) DOM 中 */ function addVnodes( parentElm: Node, before: Node | null, vnodes: ArrayremoveVnodes 方法, 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 的主要邏輯如下:
循環(huán)觸發(fā) destroy 鉤子,遞歸觸發(fā)子節(jié)點(diǎn)的鉤子
觸發(fā) remove 鉤子,利用 createRmCb , 在所有監(jiān)聽器執(zhí)行后,才調(diào)用 api.removeChild,刪除真正的 DOM 節(jié)點(diǎn)
/** * 創(chuàng)建一個刪除的回調(diào),多次調(diào)用這個回調(diào),直到監(jiān)聽器都沒了,就刪除元素 */ 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: ArraycreateElm 方法, 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; // 所有監(jiān)聽刪除 rm = createRmCb(ch.elm as Node, listeners); for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); // 如果有鉤子則調(diào)用鉤子后再調(diào)刪除回調(diào),如果沒,則直接調(diào)用回調(diào) 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 轉(zhuǎn)換成真正的 DOM 元素
主要邏輯如下:
觸發(fā) init 鉤子
處理注釋節(jié)點(diǎn)
創(chuàng)建元素并設(shè)置 id , class
觸發(fā)模塊 create 鉤子 。
處理子節(jié)點(diǎn)
處理文本節(jié)點(diǎn)
觸發(fā) vnodeData 的 create 鉤子
/** * VNode ==> 真實(shí)DOM */ function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node { let i: any, data = vnode.data; if (data !== undefined) { // 如果存在 data.hook.init ,則調(diào)用該鉤子 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; // 根據(jù) tag 創(chuàng)建元素 const elm = (vnode.elm = isDef(data) && isDef((i = (data as VNodeData).ns)) ? api.createElementNS(i, tag) : api.createElement(tag)); // 設(shè)置 id if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot)); // 設(shè)置 className if (dotIdx > 0) elm.setAttribute("class",sel.slice(dot + 1).replace(/./g, " ")); // 執(zhí)行所有模塊的 create 鉤子,創(chuàng)建對應(yīng)的內(nèi)容 for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); // 如果存在 children ,則創(chuàng)建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)) { // 追加文本節(jié)點(diǎn) api.appendChild(elm, api.createTextNode(vnode.text)); } // 執(zhí)行 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 不存在的情況, 即為文本節(jié)點(diǎn) vnode.elm = api.createTextNode(vnode.text as string); } return vnode.elm; }其他
想了解在各個生命周期都有哪些鉤子,請查看:鉤子
想了解在各個生命周期里面如何更新具體的模塊請查看:模塊
snabbdom源碼解析系列snabbdom源碼解析(一) 準(zhǔn)備工作
snabbdom源碼解析(二) h函數(shù)
snabbdom源碼解析(三) vnode對象
snabbdom源碼解析(四) patch 方法
snabbdom源碼解析(五) 鉤子
snabbdom源碼解析(六) 模塊
snabbdom源碼解析(七) 事件處理
個人博客地址
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/108829.html
摘要:閱讀源碼的時候,想了解虛擬結(jié)構(gòu)的實(shí)現(xiàn),發(fā)現(xiàn)在的地方。然而慢慢的人們發(fā)現(xiàn),在我們的代碼中布滿了一系列操作的代碼。源碼解析系列源碼解析一準(zhǔn)備工作源碼解析二函數(shù)源碼解析三對象源碼解析四方法源碼解析五鉤子源碼解析六模塊源碼解析七事件處理個人博客地址 前言 虛擬 DOM 結(jié)構(gòu)概念隨著 react 的誕生而火起來,之后 vue2.0 也加入了虛擬 DOM 的概念。 閱讀 vue 源碼的時候,想了解...
摘要:元素從父節(jié)點(diǎn)刪除時觸發(fā),和略有不同,只影響到被移除節(jié)點(diǎn)中最頂層的節(jié)點(diǎn)在方法的最后調(diào)用,也就是完成后觸發(fā)源碼解析系列源碼解析一準(zhǔn)備工作源碼解析二函數(shù)源碼解析三對象源碼解析四方法源碼解析五鉤子源碼解析六模塊源碼解析七事件處理個人博客地址 文件路徑 : ./src/hooks.ts 這個文件主要是定義了 Virtual Dom 在實(shí)現(xiàn)過程中,在其執(zhí)行過程中的一系列鉤子。方便外部做一些處理 /...
介紹 這里是 typescript 的語法,定義了一系列的重載方法。h 函數(shù)主要根據(jù)傳進(jìn)來的參數(shù),返回一個 vnode 對象 代碼 代碼位置 : ./src/h.ts /** * 根據(jù)選擇器 ,數(shù)據(jù) ,創(chuàng)建 vnode */ export function h(sel: string): VNode; export function h(sel: string, data: VNodeData...
摘要:這種解決方式也是相當(dāng)優(yōu)雅,值得學(xué)習(xí)源碼解析系列源碼解析一準(zhǔn)備工作源碼解析二函數(shù)源碼解析三對象源碼解析四方法源碼解析五鉤子源碼解析六模塊源碼解析七事件處理個人博客地址 事件處理 我們在使用 vue 的時候,相信你一定也會對事件的處理比較感興趣。 我們通過 @click 的時候,到底是發(fā)生了什么呢! 雖然我們用 @click綁定在模板上,不過事件嚴(yán)格綁定在 vnode 上的 。 event...
摘要:對象是一個對象,用來表示相應(yīng)的結(jié)構(gòu)代碼位置定義類型定義類型選擇器數(shù)據(jù),主要包括屬性樣式數(shù)據(jù)綁定時間等子節(jié)點(diǎn)關(guān)聯(lián)的原生節(jié)點(diǎn)文本唯一值,為了優(yōu)化性能定義的類型定義綁定的數(shù)據(jù)類型屬性能直接用訪問的屬性樣式類樣式數(shù)據(jù)綁定的事件鉤子創(chuàng)建對象根據(jù)傳入的 vnode 對象 vnode 是一個對象,用來表示相應(yīng)的 dom 結(jié)構(gòu) 代碼位置 :./src/vnode.ts 定義 vnode 類型 /** ...
閱讀 2268·2021-11-22 13:52
閱讀 3950·2021-11-10 11:36
閱讀 1460·2021-09-24 09:47
閱讀 1115·2019-08-29 13:54
閱讀 3391·2019-08-29 13:46
閱讀 1970·2019-08-29 12:16
閱讀 2140·2019-08-26 13:26
閱讀 3491·2019-08-23 17:10