【vue3原始碼】十三、認識Block
什麼是Block?
Block
是一種特殊的 vnode
,它和普通 vnode
相比,多出一個額外的 dynamicChildren
屬性,用來儲存動態節點。
什麼是動態節點?觀察下面這個 vnode
, children
中的第一個 vnode
的 children
是動態的,第二個 vnode
的 class
是動態的,這兩個 vnode
都是動態節點。動態節點都會有個 patchFlag
屬性,用來表示節點的什麼屬性時動態的。
const vnode = { type: 'div', children: [ { type: 'span', children: ctx.foo, patchFlag: PatchFlags.TEXT }, { type: 'span', children: 'foo', props: { class: normalizeClass(cls) }, patchFlag: PatchFlags.CLASS }, { type: 'span', children: 'foo' } ] }
作為 Block
,會將其所有子代動態節點收集到 dynamicChildren
中(子代的子代動態元素也會被收集到 dynamicChildren
中)。
const vnode = { type: 'div', children: [ { type: 'span', children: ctx.foo, patchFlag: PatchFlags.TEXT }, { type: 'span', children: 'foo', props: { class: normalizeClass(cls) }, patchFlag: PatchFlags.CLASS }, { type: 'span', children: 'foo' } ], dynamicChildren: [ { type: 'span', children: ctx.foo, patchFlag: PatchFlags.TEXT }, { type: 'span', children: 'foo', props: { class: normalizeClass(cls) }, patchFlag: PatchFlags.CLASS } ] }
哪些節點會作為Block?
模板中的根節點、帶有 v-for
、 v-if/v-else-if/v-else
的節點會被作為 Block
。如下示例:
dynamicChildren的收集
觀察 tempalte
被編譯後的程式碼,你會發現在建立 Block
之前會執行一個 openBlock
函式。
// 一個block棧用於儲存 export const blockStack: (VNode[] | null)[] = [] // 一個數組,用於儲存動態節點,最終會賦給dynamicChildren export let currentBlock: VNode[] | null = null export function openBlock(disableTracking = false) { blockStack.push((currentBlock = disableTracking ? null : [])) }
openBlock
中,如果 disableTracking
為 true
,會將 currentBlock
設定為 null
;否則建立一個新的陣列並賦值給 currentBlock
,並 push
到 blockStack
中。
再看 createBlock
, createBlock
呼叫一個 setupBlock
方法。
export function createBlock( type: VNodeTypes | ClassComponent, props?: Record<string, any> | null, children?: any, patchFlag?: number, dynamicProps?: string[] ): VNode { return setupBlock( createVNode( type, props, children, patchFlag, dynamicProps, true /* isBlock: prevent a block from tracking itself */ ) ) }
setupBlock
接收一個 vnode
引數。
function setupBlock(vnode: VNode) { // isBlockTreeEnabled > 0時,將currentBlock賦值給vnode.dynamicChildren // 否則置為null vnode.dynamicChildren = isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null // 關閉block closeBlock() // 父block收集子block // 如果isBlockTreeEnabled > 0,並且currentBlock不為null,將vnode放入currentBlock中 if (isBlockTreeEnabled > 0 && currentBlock) { currentBlock.push(vnode) } // 返回vnode return vnode }
closeBlock
:
export function closeBlock() { // 彈出棧頂block blockStack.pop() // 將currentBlock設定為父block currentBlock = blockStack[blockStack.length - 1] || null }
在理解 dynamicChildren
的收集過程之前,我們應該先清楚對於巢狀 vnode
的建立順序是從內向外執行的。如:
export default defineComponent({ render() { return createVNode('div', null, [ createVNode('ul', null, [ createVNode('li', null, [ createVNode('span', null, 'foo') ]) ]) ]) } })
vnode
的建立過程為: span
-> li
-> ul
-> div
。
在每次建立 Block
之前,都需要呼叫 openBlock
建立一個新陣列賦值給 currentBlock
,並放入 blockStack
棧頂。接著呼叫 createBlock
,在 createBlock
中會先建立 vnode
,並將 vnode
作為引數傳遞給 setupBlock
。
建立 vnode
時,如果滿足某些條件會將 vnode
收集到 currentBlock
中。
// 收集當前動態節點到currentBlock中 if ( isBlockTreeEnabled > 0 && // 避免收集自己 !isBlockNode && // 存在parent block currentBlock && // vnode.patchFlag需要大於0或shapeFlag中存在ShapeFlags.COMPONENT // patchFlag的存在表明該節點需要修補更新。 // 元件節點也應該總是打補丁,因為即使元件不需要更新,它也需要將例項持久化到下一個 vnode,以便以後可以正確解除安裝它 (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) && vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS ) { currentBlock.push(vnode) }
接著在 setupBlock
中,將 currentBlock
賦值給 vnode.dynamicChildren
屬性,然後呼叫 closeBlock
關閉 block
(彈出 blockStack
棧頂元素,並將 currentBlock
執行 blockStack
的最後一個元素,即剛彈出 block
的父 block
),接著將 vnode
收集到父 block
中。
示例
為了更清除 dynamicChildren
的收集流程,我們通過一個例子繼續進行分析。
<template> <div> <span v-for="item in data">{{ item }}</span> <ComA :count="count"></ComA> </div> </template> <script setup> import { ref, reactive } from 'vue' const data = reactive([1, 2, 3]) const count = ref(0) </script>
以上示例,經過編譯器編譯後生成的程式碼如下。SFC Playground
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, resolveComponent as _resolveComponent, createVNode as _createVNode } from "vue" import { ref, reactive } from 'vue' const __sfc__ = { __name: 'App', setup(__props) { const data = reactive([1, 2, 3]) const count = ref(0) return (_ctx, _cache) => { const _component_ComA = _resolveComponent("ComA") return (_openBlock(), _createElementBlock("div", null, [ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(data, (item) => { return (_openBlock(), _createElementBlock("span", null, _toDisplayString(item), 1 /* TEXT */)) }), 256 /* UNKEYED_FRAGMENT */)), _createVNode(_component_ComA, { count: count.value }, null, 8 /* PROPS */, ["count"]) ])) } } } __sfc__.__file = "App.vue" export default __sfc__
當渲染函式(這裡的渲染函式就是 setup
的返回值)被執行時,其執行流程如下:
- 執行
_openBlock()
建立一個新的陣列(稱其為div-block
),並push
到blockStack
棧頂 - 執行
_openBlock(true)
,由於引數為true
,所以不會建立新的陣列,而是將null
賦值給currentBlock
,並push
到blockStack
棧頂 - 執行
_renderList
,_renderList
會遍歷data
,並執行第二個renderItem
引數,即(item) => { ... }
。 - 首先
item
為1
,執行renderItem
,執行_openBlock()
建立一個新的陣列(稱其為span1-block
),並push
到blockStack
棧頂。此時blockStack
、currentBlock
狀態如下如:
- 接著執行
_createElementBlock("span", null, _toDisplayString(item), 1 /* TEXT */)
,在_createElementBlock
中會先呼叫createBaseVNode
建立vnode
,在建立vnode
時因為這是個block vnode
(isBlockNode
引數為true
),所以不會被收集到currentBlock
中 - 建立好
vnode
後,執行setupBlock
,將currentBlock
賦值給vnode.dynamicChildren
。 - 執行
closeBlock()
,彈出blcokStack
的棧頂元素,並將currentBlock
指向blcokStack
中的最後一個元素。如下圖所示:
- 由於此時
currentBlock
為null
,所以跳過currentBlock.push(vnode)
。 -
item = 2、item = 3
時,過程與4-7
步驟相同。當item = 3
時,block
建立完畢後的狀態如下:
- 此時,
list
渲染完畢,接著呼叫_createElementBlock(_Fragment)
。 - 執行
_createElementBlock
的過程中,因為isBlockNode
引數為true
且currentBlock
為null
,所以不會被currentBlock
收集 - 執行
setupBlock
,將EMPTY_ARR
(空陣列)賦值給vnode.dynamicChildren
,並呼叫closeBlock()
,彈出棧頂元素,使currentBlcok
指向最新的棧頂元素。由於此時currentBlock
不為null
,所以執行currentBlock.push(vnode)
- 執行
_createVNode(_component_ComA)
,建立vnode
過程中,因為vnode.patchFlag === PatchFlag.PROPS
,所以會將vnode
新增到currentBlock
中。
- 執行
_createElementBlock('div')
。先建立vnode
,因為isBlockNode
為true
,所以不會收集到currentBlock
中。 - 執行
setupBlock()
,將currentBlock
賦給vnode.dynamicChildren
。然後執行closeBlock()
,彈出棧頂元素,此時blockStack
長度為0,所以currentBlock
會指向null
最終生成的 vnode
:
{ type: "div", children: [ { type: Fragment, children: [{ type: "span", children: "1", patchFlag: PatchFlag.TEXT, dynamicChildren: [], }, { type: "span", children: "2", patchFlag: PatchFlag.TEXT, dynamicChildren: [], }, { type: "span", children: "3", patchFlag: PatchFlag.TEXT, dynamicChildren: [], }], patchFlag: PatchFlag.UNKEYED_FRAGMENT, dynamicChildren: [] }, { type: ComA, children: null, patchFlag: PatchFlag.PROPS, dynamicChildren: null } ] , patchFlag:0, dynamicChildren: [ { type: Fragment, children: [{ type: "span", children: "1", patchFlag: PatchFlag.TEXT, dynamicChildren: [], }, { type: "span", children: "2", patchFlag: PatchFlag.TEXT, dynamicChildren: [], }, { type: "span", children: "3", patchFlag: PatchFlag.TEXT, dynamicChildren: [], }], patchFlag: PatchFlag.UNKEYED_FRAGMENT, dynamicChildren: [] }, { type: ComA, children: null, patchFlag: PatchFlag.PROPS, dynamicChildren: null } ] }
Block的作用
如果你瞭解Diff過程,你應該知道在 Diff
過程中,即使 vnode
沒有發生變化,也會進行一次比較。而 Block
的出現減少了這種不必要的的比較,由於 Block
中的動態節點都會被收集到 dynamicChildren
中,所以 Block
間的 patch
可以直接比較 dynamicChildren
中的節點,減少了非動態節點之間的比較。
Block
之間進行 patch
時,會呼叫一個 patchBlockChildren
方法來對 dynamicChildren
進行 patch
。
const patchElement = ( n1: VNode, n2: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { // ... let { patchFlag, dynamicChildren, dirs } = n2 if (dynamicChildren) { patchBlockChildren( n1.dynamicChildren!, dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds ) if (__DEV__ && parentComponent && parentComponent.type.__hmrId) { traverseStaticChildren(n1, n2) } } else if (!optimized) { patchChildren( n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds, false ) } // ... }
patchElement
中如果新節點存在 dynamicChildren
,說明此時新節點是個 Block
,那麼會呼叫 patchBlockChildren
方法對 dynamicChildren
進行 patch
;否則如果 optimized
為 false
呼叫 patchChildren
, patchChildren
中可能會呼叫 patchKeyedChildren/patchUnkeyedChildren
進行 Diff
。
const patchBlockChildren: PatchBlockChildrenFn = ( oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, isSVG, slotScopeIds ) => { for (let i = 0; i < newChildren.length; i++) { const oldVNode = oldChildren[i] const newVNode = newChildren[i] // 確定父容器 const container = oldVNode.el && (oldVNode.type === Fragment || !isSameVNodeType(oldVNode, newVNode) || oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT)) ? hostParentNode(oldVNode.el)! : fallbackContainer patch( oldVNode, newVNode, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, true ) } }
總結
Block
是 vue3
中一種效能優化的手段。 Block
本質是一種特殊的 vnode
,它與普通 vnode
相比,多出了一個 dynamicChildren
屬性,這個屬性中儲存了所有 Block
子代的動態節點。 Block
進行 patch
可以直接對 dynamicChildren
中的動態節點進行 patch
,避免了靜態節點之間的比較。
Block
的建立過程:
- 每次建立
Block
節點之前,需要呼叫openBlcok
方法,建立一個新的陣列賦值給currentBlock
,並push
到blockStack
的棧頂。 - 在建立
vnode
的過程中如果滿足一些條件,會將動態節點放到currentBlock
中。 - 節點建立完成後,作為引數傳入
setupBlock
中。在setupBlock
中,將currentBlock
複製給vnode.dynamicChildren
,並呼叫closeBlcok
,彈出blockStack
棧頂元素,並使currentBlock
指向最新的棧頂元素。最後如果此時currentBlock
不為空,將vnode
收集到currentBlock
中。
- SegmentFault 2022 年社群週報 Vol.9
- 社群精選 | 不容錯過的9個冷門css屬性
- 2022最新版 Redis大廠面試題總結(附答案)
- 手寫一個mini版本的React狀態管理工具
- 【vue3原始碼】十三、認識Block
- 天翼雲全場景業務無縫替換至國產原生作業系統CTyunOS!
- JavaScript 設計模式 —— 代理模式
- MobTech簡訊驗證ApiCloud端SDK
- 以羊了個羊為例,淺談小程式抓包與響應報文修改
- 這幾種常見的 JVM 調優場景,你知道嗎?
- 聊聊如何利用管道模式來進行業務編排(下篇)
- 通用ORM的設計與實現
- 如此狂妄,自稱高效能佇列的Disruptor有啥來頭?
- 為什麼要學習GoF設計模式?
- 827. 最大人工島 : 簡單「並查集 列舉」運用題
- 介紹 Preact Signals
- 手把手教你如何使用 Timestream 實現物聯網時序資料儲存和分析
- 850. 矩形面積 II : 掃描線模板題
- Java 併發程式設計解析 | 基於JDK原始碼解析Java領域中的併發鎖,我們可以從中學習到什麼內容?
- 令人困惑的 Go time.AddDate