在上一篇文章中,咱們分析了在編譯過程靜態節點的提高。而且,在文章的結尾也說了,下一篇文章將會介紹 patch
過程。javascript
提及「Vue3」的 patch
過程,其中最爲津津樂道的就是靶向更新。靶向更新,顧名思義,即更新的過程是帶有目標性的、直接性的。而,這也是和靜態節點提高同樣,是「Vue3」針對 VNode
更新性能問題的一大優化。vue
那麼,今天,咱們就來揭祕「Vue3」compile 和 runtime 結合的 patch
過程 到底是如何實現的!java
提及「Vue3」的 patch
,老生常談的就是 patchFlag
。因此,對於 shapeFlag
我想你們可能有點蒙,這是啥?node
ShapeFlag
顧名思義,是對具備形狀的元素進行標記,例如普通元素、函數組件、插槽、keep alive
組件等等。它的做用是幫助 Rutime 時的 render
的處理,能夠根據不一樣 ShapeFlag
的枚舉值來進行不一樣的 patch
操做。數組
在「Vue3」源碼中 ShapeFlag
和 patchFlag
同樣被定義爲枚舉類型,每個枚舉值以及意義會是這樣:
緩存
瞭解過「Vue2.x」源碼的同窗應該知道第一次 patch
的觸發,就是在組件建立的過程。只不過此時,oldVNode
爲 null
,因此會表現爲掛載的行爲。所以,在認知 pathc
的過程以前,不可或缺地是咱們須要知道組件是怎麼建立的?微信
既然說 patch
的第一次觸發會是組件的建立過程,那麼在「Vue3」中組件的建立過程會是怎麼樣的?它會經歷這麼三個過程:svg
在以前,咱們講過 compile
編譯過程會將咱們的 template
轉化爲可執行代碼,即 render
函數。而,compiler
生成的 render
函數會綁定在當前組件實例的 render
屬性上。例如,此時有這樣的 template
模板:函數
<div><div>hi vue3</div><div>{{msg}}</div></div>
它通過 compile
編譯處理後生成的 render
函數會是這樣:源碼分析
const _Vue = Vue const _hoisted_1 = _createVNode("div", null, "hi vue3", -1 /* HOISTED */) function render(_ctx, _cache) { with (_ctx) { const { createVNode: _createVNode, toDisplayString: _toDisplayString, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue return (_openBlock(), _createBlock(_Fragment, null, [ _createVNode("div", null, [ _hoisted_1, _createVNode("div", null, _toDisplayString(msg), 1 /* TEXT */) ]) ])) } }
這個 render
函數真正執行的時機是在安裝全局的渲染函數對應 effect
的時候,即 setupRenderEffect
。而渲染 effect
會在組件建立時和更新時觸發。
這個時候,可能又有同窗會問什麼是 effect
?effect
並非「Vue3」的新概念,它的本質是「Vue2.x」源碼中的 watcher
,一樣地,effect
也會負責依賴收集和派發更新。
有興趣瞭解「Vue3」依賴收集和派發更新過程的同窗能夠看一下這篇文章 4k+ 字分析 Vue 3.0 響應式原理(依賴收集和派發更新)
而 setupRenderEffect
函數對應的僞代碼會是這樣:
function setupRenderEffect() { instance.update = effect(function componentEffect() { // 組件未掛載 if (!instance.isMounted) { // 建立組件對應的 VNode tree const subTree = (instance.subTree = renderComponentRoot(instance)) ... instance.isMounted = true } else { // 更新組件 ... } }
能夠看到,組件的建立會命中 renderComponentRoot(instance)
的邏輯,此時 renderComponentRoot(instance)
會調用 instance
上的 render
函數,而後爲當前組件實例構造整個 VNode Tree
,即這裏的 subTree
。renderComponentRoot
函數對應的僞代碼會是這樣:
function renderComponentRoot(instance) { const { ... render, ShapeFlags, ... } = instance if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { ... result = normalizeVNode( render!.call( proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx ) ) ... } }
能夠看到,在 renderComponentRoot
中,若是當前 ShapeFlags
爲 STATEFUL_COMPONENT
時會命中調用 render
的邏輯。這裏的 render
函數,就是上面咱們所說的 compile
編譯後生成的可執行代碼。它最終會返回一個 VNode Tree
,它看起來會是這樣:
{ ... children: (2) [{…}, {…}], ... dynamicChildren: (2) [{…}, {…}], ... el: null, key: null, patchFlag: 64, ... shapeFlag: 16, ... type: Symbol(Fragment), ... }
以靶向更新爲例,瞭解過的同窗應該知道,它的實現離不開 VNode Tree
上的 dynamicChildren
屬性,dynamicChildren
則是用來承接整個 VNode Tree
中的全部動態節點, 而標記動態節點的過程又是在 compile
編譯的 transform
階段,能夠說是環環相扣,因此,這也是咱們常說的「Vue3」Runtime
和 Compile
的巧妙結合。
顯然在「Vue2.x」是不具有構建 VNode
的 dynamicChildren
屬性的條件。那麼,「Vue3」又是如何生成的 dynamicChildren
?
Block VNode
是「Vue3」針對靶向更新而提出的概念,它的本質是動態節點對應的 VNode
。而,VNode
上的 dynamicChildren
屬性則是衍生於 Block VNode
,所以,它也就是充當着靶向更新中的靶的角色。
這裏,咱們再回到前面所提到的 compiler
編譯時生成 render
函數,它返回的結果:
(_openBlock(), _createBlock(_Fragment, null, [ _createVNode("div", null, [ _hoisted_1, _createVNode("div", null, _toDisplayString(msg), 1 /* TEXT */) ]) ]))
須要注意的是openBlock
必須寫在createBlock
以前,由於在Block Tree
中的Children
老是會在createBlock
以前執行。
能夠看到有兩個和 Block
相關的函數:_openBlock()
和 _createBlock()
。實際上,它們分別對應着源碼中的 openBlock()
和 createBlock()
函數。那麼,咱們分別來認識一下這二者:
openBlock
會爲當前 Vnode
初始化一個數組 currentBlock
來存放 Block
。openBlock
函數的定義十分簡單,會是這樣:
function openBlock(disableTracking = false) { blockStack.push((currentBlock = disableTracking ? null : [])); }
openBlock
函數會有一個形參 disableTracking
,它是用來判斷是否初始化 currentBlock
。那麼,在什麼狀況下不須要建立 currentBlock
?
當存在 v-for
造成的 VNode
時,它的 render
函數中的 openBlock()
函數形參 disableTracking
就是 true
。由於,它不須要靶向更新,來優化更新過程,即它在 patch
時會經歷完整的 diff
過程。
換個角理解,爲何這麼設計?靶向更新的本質是爲了從一顆存在動態、靜態節點的 VNode Tree
中篩選出動態的節點造成 Block Tree
,即 dynamicChildren
,而後在 patch
時實現精準、快速的更新。因此,顯然 v-for
造成的 VNode Tree
它不須要靶向更新。
這裏,你們可能還會有一個疑問,爲何建立好的Block VNode
又被push
到了blockStack
中?它又有什麼做用?有興趣的同窗能夠去試一下v-if
場景,它最終會構造一個Block Tree
,有興趣的同窗能夠看一下這篇文章 Vue3 Compiler 優化細節,如何手寫高性能渲染函數
createBlock
則負責建立 Block VNode
,它會調用 createVNode
方法來依次建立 Block VNode
。createBlock
函數的定義:
function createBlock(type, props, children, patchFlag, dynamicProps) { const vnode = createVNode(type, props, children, patchFlag, dynamicProps, true); // 構造 `Block Tree` vnode.dynamicChildren = currentBlock || EMPTY_ARR; closeBlock(); if (shouldTrack > 0 && currentBlock) { currentBlock.push(vnode); } return vnode; }
能夠看到在 createBlock
中仍然會調用 createVNode
建立 VNode
。而 createVNode
函數本質上調用的是源碼中的 _createVNode
函數,它的類型定義看起來會是這樣:
function _createVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props: (Data & VNodeProps) | null = null, children: unknown = null, patchFlag: number = 0, dynamicProps: string[] | null = null, isBlockNode = false ): VNode {}
當咱們調用 _createVNode()
建立 Block VNode
時,須要傳入的 isBlockNode
爲 true
,它用來標識當前 VNode
是否爲 Block VNode
,從而避免 Block VNode
依賴本身的狀況發生,即就不會將當前 VNode
加入到 currentBlock
中。其對應的僞代碼會是這樣:
function _createVNode() { ... if ( shouldTrack > 0 && !isBlockNode && currentBlock && patchFlag !== PatchFlags.HYDRATE_EVENTS && (patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) ) { currentBlock.push(vnode) } ... }
因此,只有知足上面的 if
語句中的全部條件的 VNode
,才能做爲 Block Node
,它們對應的具體含義會是這樣:
v-once
指令下的 VNode
。Block Node
。Block Node
,對於 v-for
場景下,curretBlock
爲 null
,它不須要靶向更新。32
事件監聽,只有事件監聽狀況時事件監聽會被緩存。Block Node
,這是爲了保證下一個 VNode
的正常卸載。至於,再深一層次探索爲何?有興趣的同窗能夠自行去了解。
講完 VNode
的建立過程,我想你們都會意識到一點,若是使用手寫 render
函數的形式開發,咱們就須要對 createBlock
、openBlock
等函數的概念有必定的認知。由於,只有這樣,咱們寫出的 render
函數才能充分利用好靶向更新過程,實現的應用更新性能也是最好的。
前面,咱們也說起了 patch
是組件建立和更新的最後一步,有時候它也會被稱爲 diff
。在
「Vue2.x」中它的 patch
過程會是這樣:
VNode
間的比較,判斷這兩個新舊 VNode
是否屬於同一個引用,是則不進行後續比較,不是則對比每一級的 VNode
。VNode
的首尾,循環條件爲頭指針索引小於尾指針索引。VNode
的當前匹配成功的真實 DOM
移動到對應新 VNode
匹配成功的位置。VNode
中的真實 DOM
節點插入到舊 VNode
的對應位置中,即,此時是建立舊 VNode
中不存在的 DOM
節點。VNode
的 children
不存在爲止。粗略一看,就能明白「Vue2.x」patch
是一個硬比較的過程。因此,這也是它的缺陷所在,沒法合理地處理大型應用狀況下的 VNode
更新。
雖然「Vue3」的 patch
沒有像 compile
同樣會從新命名一些例如 baseCompile
、transform
階段性的函數。可是,其內部的處理相對於「Vue2.x」變得更爲智能。
它會利用 compile
階段的 type
和 patchFlag
來處理不一樣狀況下的更新,這也能夠理解爲是一種分而治之的策略。其對應的僞代碼會是這樣:
function patch(...) { if (n1 && !isSameVNodeType(n1, n2)) { ... } if (n2.patchFlag === PatchFlags.BAIL) { ... } const { type, ref, shapeFlag } = n2 switch (type) { case Text: processText(n1, n2, container, anchor) break case Comment: processCommentNode(n1, n2, container, anchor) break case Static: if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG) } else if (__DEV__) { patchStaticNode(n1, n2, container, isSVG) } break case Fragment: processFragment(...) break default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement(...) } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent(...) }else if (shapeFlag & ShapeFlags.TELEPORT) { ;(type as typeof TeleportImpl).process(...) } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ;(type as typeof SuspenseImpl).process(...) } else if (__DEV__) { warn('Invalid VNode type:', type, `(${typeof type})`) } }
能夠看到,除開文本、靜態、文檔碎片、註釋等 VNode
會根據 type
處理。默認狀況下,都是根據 shapeFlag
來處理諸如組件、普通元素、Teleport
、Suspense
組件等。因此,這也是爲何文章開頭會介紹 shapeFlag
的緣由。
而且,從 render
階段建立 Block VNode
到 patch
階段根據特定 shapeFlag
的不一樣處理,在必定程度上,shapeFlag
具備和 patchFlag
同樣的價值!
這裏取其中一種狀況,當 ShapeFlag
爲 ELEMENT
時,咱們來分析一下 processElement
是如何處理 VNode
的 patch
的。
一樣地 processElement
會處理掛載的狀況,即 oldVNode
爲 null
的時候。processElement
函數的定義:
const processElement = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) => { isSVG = isSVG || (n2.type as string) === 'svg' if (n1 == null) { mountElement( n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else { patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized) } }
其實,我的認爲oldVNode
改成n1
、newVNode
改成n2
,這命名是否有點倉促?
能夠看到,processElement
在處理更新的狀況時,實際上會調用 patchElement
函數。
patchElement
會處理咱們所熟悉的 props
、生命週期、自定義事件指令等。這裏,咱們不會一一分析每一種狀況會發生什麼。咱們就以文章開頭提的靶向更新爲例,它是如何處理的?
其實,對於靶向更新的處理非常簡單,即若是此時 n2
(newVNode
) 的 dynamicChildren
存在時,直接"梭哈",一把更新 dynamicChildren
,不須要處理其餘 VNode
。它對應的僞代碼會是這樣:
function patchElement(...) { ... if (dynamicChildren) { patchBlockChildren( n1.dynamicChildren!, dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG ) ... } ... }
因此,若是 n2
的 dynamicChildren
存在時,則會調用 patchBlockChildren
方法。而,patchBlockChildren
方法其實是基於 patch
方法的一層封裝。
patchBlockChildren
會遍歷 newChildren
,即 dynamicChildren
來處理每個同級別的 oldVNode
和 newVNode
,以及它們做爲參數來調用 patch
函數。以此類推,不斷重複上述過程。
const patchBlockChildren: PatchBlockChildrenFn = ( oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, isSVG ) => { for (let i = 0; i < newChildren.length; i++) { const oldVNode = oldChildren[i] const newVNode = newChildren[i] const container = oldVNode.type === Fragment || !isSameVNodeType(oldVNode, newVNode) || oldVNode.shapeFlag & ShapeFlags.COMPONENT || oldVNode.shapeFlag & ShapeFlags.TELEPORT ? hostParentNode(oldVNode.el!)! : fallbackContainer patch( oldVNode, newVNode, container, null, parentComponent, parentSuspense, isSVG, true ) } }
你們應該會注意到,此時還會獲取當前 VNode
須要掛載的容器,由於 dynamicChildren
有時候會是跨層級的,並非此時的 VNode
就是它的 parent
。具體會分爲兩種狀況:
1. oldVNode 的父節點做爲容器
oldVNode
的類型爲文檔碎片時。oldVNode
和 newVNode
不是同一個節點時。shapeFlag
爲 teleport
或 component
時。2. 初始調用 patch 的容器
patch
方法傳入的根 VNode
的掛載點做爲容器。具體每一種狀況爲何須要這樣處理,講起來又將是 長篇大論,預計會放在下一篇文章中和你們見面。
原本初衷是想化繁爲簡,沒想到最後仍是寫了 3k+ 的字。由於,「Vue3」將 compile
和 runtime
結合運用實現了諸多優化。因此,已經不可能出現如「Vue2.x」同樣分析 patch
只須要關注 runtime
,不須要關注在這以前的 compile
作了一些奠基基調的處理。所以,文章總會不可避免地有點晦澀,這裏建議想加深印象的同窗能夠結合實際栗子單步調式一番。
從零到一,帶你完全搞懂 vite 中的 HMR 原理(源碼分析)
經過閱讀,若是你以爲有收穫的話,能夠愛心三連擊!!!
微信公衆號: Code center