文章來(lái)源于公眾號(hào):Code center ,作者五柳
前言
「靜態(tài)節(jié)點(diǎn)提升」是「Vue3」針對(duì) VNode
更新過(guò)程性能問(wèn)題而提出的一個(gè)優(yōu)化點(diǎn)。眾所周知,在大型應(yīng)用場(chǎng)景下,「Vue2.x」 的 patchVNode
過(guò)程,即 diff
過(guò)程是非常緩慢的,這是一個(gè)十分令人頭疼的問(wèn)題。
雖然,對(duì)于面試常問(wèn)的 diff
過(guò)程在一定程度上是減少了對(duì) DOM
的直接操作。但是,「這個(gè)減少是有一定成本的」。因?yàn)?,如果是?fù)雜應(yīng)用,那么就會(huì)存在父子關(guān)系非常復(fù)雜的 VNode
,而這也就是 diff
的痛點(diǎn),它會(huì)不斷地遞歸調(diào)用 patchVNode
,不斷堆疊而成的幾毫秒,最終就會(huì)造成 VNode
更新緩慢。
也因此,這也是為什么我們所看到的大型應(yīng)用諸如阿里云之類(lèi)的采用的是基于「React」的技術(shù)棧的原因之一。所以,「Vue3」也是痛改前非,重寫(xiě)了整個(gè) Compiler
過(guò)程,提出了靜態(tài)提升、靶向更新等優(yōu)化點(diǎn),來(lái)提高 patchVNode
過(guò)程。
那么,回到今天的正題,我們從源碼角度看看在整個(gè)編譯過(guò)程「Vue3」靜態(tài)節(jié)點(diǎn)提升究竟是「何許人也」?
什么是 patchFlag
由于,在 compile
過(guò)程的 transfrom
階段會(huì)提及 AST Element 上的 patchFlag
屬性。所以,在正式認(rèn)識(shí) complie
之前,我們先搞清楚一個(gè)概念,什么是 patchFlag
?
patchFlag
是 complier
時(shí)的 transform
階段解析 AST Element 打上的「優(yōu)化標(biāo)識(shí)」。并且,顧名思義 patchFlag
,patch
一詞表示著它會(huì)為 runtime
時(shí)的 patchVNode
提供依據(jù),從而實(shí)現(xiàn)靶向更新 VNode
的效果。因此,這樣一來(lái)一往,也就是耳熟能詳?shù)?Vue3 巧妙結(jié)合 runtime
與 compiler
實(shí)現(xiàn)靶向更新和靜態(tài)提升。
而在源碼中 patchFlag
被定義為一個(gè)「數(shù)字枚舉類(lèi)型」,每一個(gè)枚舉值對(duì)應(yīng)的標(biāo)識(shí)意義會(huì)是這樣:
并且,值得一提的是整體上 patchFlag
的分為兩大類(lèi):
- 當(dāng)
patchFlag
的值「大于」 0 時(shí),代表所對(duì)應(yīng)的元素在patchVNode
時(shí)或render
時(shí)是可以被優(yōu)化生成或更新的。 - 當(dāng)
patchFlag
的值「小于」 0 時(shí),代表所對(duì)應(yīng)的元素在patchVNode
時(shí),是需要被full diff
,即進(jìn)行遞歸遍歷VNode tree
的比較更新過(guò)程。
其實(shí),還有兩類(lèi)特殊的
flag
:shapeFlag
和slogFlag
,這里我就不對(duì)此展開(kāi),有興趣的同學(xué)可以自行去了解。
Compile 編譯過(guò)程
對(duì)比 Vue2.x 編譯過(guò)程
了解過(guò)「Vue2.x」源碼的同學(xué),我想應(yīng)該都知道在「Vue2.x」中的 Compile
過(guò)程會(huì)是這樣:
parse
編譯模板生成原始 AST。optimize
優(yōu)化原始 AST,標(biāo)記 AST Element 為靜態(tài)根節(jié)點(diǎn)或靜態(tài)節(jié)點(diǎn)。generate
根據(jù)優(yōu)化后的 AST,生成可執(zhí)行代碼,例如_c
、_l
之類(lèi)的。
而在「Vue3」中,整體的 Compile
過(guò)程仍然是三個(gè)階段,但是不同于「Vue2.x」的是,第二個(gè)階段換成了正常編譯器都會(huì)存在的階段 transform
。所以,它看起來(lái)會(huì)是這樣:
在源碼中,它對(duì)應(yīng)的偽代碼會(huì)是這樣:
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
...
const ast = isString(template) ? baseParse(template, options) : template
...
transform(
ast,
extend({}, options, {....})
)
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
那么,我想這個(gè)時(shí)候大家可能會(huì)問(wèn)為什么會(huì)是 transform
?它的職責(zé)是什么?
通過(guò)簡(jiǎn)單的對(duì)比「Vue2.x」編譯過(guò)程的第二階段的 optimize
,很明顯,transform
并不是「無(wú)米之炊」,它仍然有著「優(yōu)化」原始 AST 的作用,而具體職責(zé)會(huì)表現(xiàn)在:
- 對(duì)所有 AST Element 新增
codegen
屬性來(lái)幫助generate
更準(zhǔn)確地生成「最優(yōu)」的可執(zhí)行代碼。 - 對(duì)靜態(tài) AST Element 新增
hoists
屬性來(lái)實(shí)現(xiàn)靜態(tài)節(jié)點(diǎn)的「單獨(dú)創(chuàng)建」。 - ...
此外,transform
還標(biāo)識(shí)了諸如 isBlock
、helpers
等屬性,來(lái)生成最優(yōu)的可執(zhí)行代碼,這里我們就不細(xì)談,有興趣的同學(xué)可以自行了解。
baseParse 構(gòu)建原始抽象語(yǔ)法樹(shù)(AST)
baseParse
顧名思義起著「解析」的作用,它的表現(xiàn)和「Vue2.x」的 parse
相同,都是解析模板 tempalte
生成「原始 AST」。
假設(shè),此時(shí)我們有一個(gè)這樣的模板 template
:
<div><div>hi vue3</div><div>{{msg}}</div></div>
那么,它在經(jīng)過(guò) baseParse
處理后生成的 AST 看起來(lái)會(huì)是這樣:
{
cached: 0,
children: [{…}],
codegenNode: undefined,
components: [],
directives: [],
helpers: [],
hoists: [],
imports: [],
loc: {start: {…}, end: {…}, source: "<div><div>hi vue3</div><div>{{msg}}</div></div>"},
temps: 0,
type: 0
}
如果,了解過(guò)「Vue2.x」編譯過(guò)程的同學(xué)應(yīng)該對(duì)于上面這顆 AST
的大部分屬性不會(huì)陌生。AST
的本質(zhì)是通過(guò)用對(duì)象來(lái)描述「DSL」(特殊領(lǐng)域語(yǔ)言),例如:
children
中存放的就是最外層div
的后代。loc
則用來(lái)描述這個(gè) AST Element 在整個(gè)字符串(template
)中的位置信息。type
則是用于描述這個(gè)元素的類(lèi)型(例如 5 為插值、2 為文本)等等。
并且,可以看到的是不同于「Vue2.x」的 AST,這里我們多了諸如 helpers
、codegenNode
、hoists
等屬性。而,這些屬性會(huì)在 transform
階段進(jìn)行相應(yīng)地賦值,進(jìn)而幫助 generate
階段生成「更優(yōu)的」可執(zhí)行代碼。
transfrom 優(yōu)化原始抽象語(yǔ)法樹(shù)(AST)
對(duì)于 transform
階段,如果了解過(guò)「編譯器」的工作流程的同學(xué)應(yīng)該知道,一個(gè)完整的編譯器的工作流程會(huì)是這樣:
- 首先,
parse
解析原始代碼字符串,生成抽象語(yǔ)法樹(shù) AST。 - 其次,
transform
轉(zhuǎn)化抽象語(yǔ)法樹(shù),讓它變成更貼近目標(biāo)「DSL」的結(jié)構(gòu)。 - 最后,
codegen
根據(jù)轉(zhuǎn)化后的抽象語(yǔ)法樹(shù)生成目標(biāo)「DSL」的可執(zhí)行代碼。
而在「Vue3」采用 Monorepo
的方式管理項(xiàng)目后,compile
對(duì)應(yīng)的能力就是一個(gè)編譯器。所以,transform
也是整個(gè)編譯過(guò)程的重中之重。換句話說(shuō),如果沒(méi)有 transform
對(duì) AST 做諸多層面的轉(zhuǎn)化,「Vue」仍然會(huì)掛在 diff
這個(gè)「飽受詬病」的過(guò)程。
相比之下,「Vue2.x」的編譯階段沒(méi)有完整的
transform
,只是optimize
優(yōu)化了一下 AST,可以想象在「Vue」設(shè)計(jì)之初尤大也沒(méi)想到它以后會(huì)「這么地流行」!
那么,我們來(lái)看看 transform
函數(shù)源碼中的定義:
function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseNode(root, context)
if (options.hoistStatic) {
hoistStatic(root, context)
}
if (!options.ssr) {
createRootCodegen(root, context)
}
// finalize meta information
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
}
可以說(shuō),transform
函數(shù)做了什么,在它的定義中是「一覽無(wú)余」。這里我們提一下它對(duì)靜態(tài)提升其決定性作用的兩件事:
- 將原始 AST 中的靜態(tài)節(jié)點(diǎn)對(duì)應(yīng)的 AST Element 賦值給根 AST 的
hoists
屬性。 - 獲取原始 AST 需要的 helpers 對(duì)應(yīng)的鍵名,用于
generate
階段的生成可執(zhí)行代碼的獲取對(duì)應(yīng)函數(shù),例如createTextVNode
、createStaticVNode
、renderList
等等。
并且,在 traverseNode
函數(shù)中會(huì)對(duì) AST Element 應(yīng)用具體的 transform
函數(shù),大致可以分為兩類(lèi):
- 靜態(tài)節(jié)點(diǎn)
transform
應(yīng)用,即節(jié)點(diǎn)不含有插值、指令、props、動(dòng)態(tài)樣式的綁定等。 - 動(dòng)態(tài)節(jié)點(diǎn)
transform
應(yīng)用,即節(jié)點(diǎn)含有插值、指令、props、動(dòng)態(tài)樣式的綁定等。
那么,我們就來(lái)看看對(duì)于靜態(tài)節(jié)點(diǎn) transform
是如何應(yīng)用的?
靜態(tài)節(jié)點(diǎn) transform
應(yīng)用
這里,對(duì)于上面我們說(shuō)到的這個(gè)栗子,靜態(tài)節(jié)點(diǎn)就是這個(gè)部分:
<div>hi vue3</div>
而它在沒(méi)有進(jìn)行 transform
應(yīng)用之前,它對(duì)應(yīng)的 AST 會(huì)是這樣:
{
children: [{
content: "hi vue3"
loc: {start: {…}, end: {…}, source: "hi vue3"}
type: 2
}],
codegenNode: undefined,
isSelfClosing: false,
loc: {start: {…}, end: {…}, source: "<div>hi vue3</div>"},
ns: 0,
props: [],
tag: "div",
tagType: 0,
type: 1
}
可以看出,此時(shí)它的 codegenNode
是 undefined
。而在源碼中各類(lèi) transform
函數(shù)被定義為 plugin
,它會(huì)根據(jù) baseParse
生成的 AST 「遞歸應(yīng)用」對(duì)應(yīng)的 plugin
。然后,創(chuàng)建對(duì)應(yīng) AST Element 的 codegen
對(duì)象。
所以,此時(shí)我們會(huì)命中 transformElement
和 transformText
兩個(gè) plugin
的邏輯。
「transformText」
transformText
顧名思義,它和「文本」相關(guān)。很顯然,此時(shí)的 AST Element 所屬的類(lèi)型就是 Text
。那么,我們先來(lái)看一下 transformText
函數(shù)對(duì)應(yīng)的偽代碼:
export const transformText: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ROOT ||
node.type === NodeTypes.ELEMENT ||
node.type === NodeTypes.FOR ||
node.type === NodeTypes.IF_BRANCH
) {
return () => {
const children = node.children
let currentContainer: CompoundExpressionNode | undefined = undefined
let hasText = false
for (let i = 0; i < children.length; i++) { // {1}
const child = children[i]
if (isText(child)) {
hasText = true
...
}
}
if (
!hasText ||
(children.length === 1 &&
(node.type === NodeTypes.ROOT ||
(node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT)))
) { // {2}
return
}
...
}
}
}
可以看到,這里我們會(huì)命中 「{2}」 的邏輯,即如果對(duì)于「節(jié)點(diǎn)含有單一文本」 transformText
并不需要進(jìn)行額外的處理,即該節(jié)點(diǎn)仍然在這里仍然保留和「Vue2.x」版本一樣的處理方式。
而 transfromText
真正發(fā)揮作用的場(chǎng)景是當(dāng)模板中存在這樣的情況:
<div>ab {a} </div>
此時(shí) transformText
需要將兩者放在一個(gè)「單獨(dú)的」 AST Element 下,在源碼中它被稱為「Compound Expression」,即「組合的表達(dá)式」。這種組合的目的是為了 patchVNode
這類(lèi) VNode
時(shí)做到「更好地定位和實(shí)現(xiàn) DOM
的更新」。反之,如果是一個(gè)文本節(jié)點(diǎn)和插值動(dòng)態(tài)節(jié)點(diǎn)的話,在 patchVNode
階段同樣的操作需要進(jìn)行兩次,例如對(duì)于同一個(gè) DOM
節(jié)點(diǎn)操作兩次。
「transformElement」
transformElement
是一個(gè)所有 AST Element 都會(huì)被執(zhí)行的一個(gè) plugin
,它的核心是為 AST Element 生成最基礎(chǔ)的 codegen
屬性。例如標(biāo)識(shí)出對(duì)應(yīng) patchFlag
,從而為生成 VNode
提供依據(jù),例如 dynamicChildren
。
而對(duì)于靜態(tài)節(jié)點(diǎn),同樣是起到一個(gè)初始化它的 codegenNode
屬性的作用。并且,從上面介紹的 patchFlag
的類(lèi)型,我們可以知道它的 patchFlag
為默認(rèn)值 0
。所以,它的 codegenNode
屬性值看起來(lái)會(huì)是這樣:
{
children: {
content: "hi vue3"
loc: {start: {…}, end: {…}, source: "hi vue3"}
type: 2
},
directives: undefined,
disableTracking: false,
dynamicProps: undefined,
isBlock: false,
loc: {start: {…}, end: {…}, source: "<div>hi vue3</div>"},
patchFlag: undefined,
props: undefined,
tag: ""div"",
type: 13
}
generate 生成可執(zhí)行代碼
generate
是 compile
階段的最后一步,它的作用是將 transform
轉(zhuǎn)換后的 AST 生成對(duì)應(yīng)的「可執(zhí)行代碼」,從而在之后 Runtime 的 Render 階段時(shí),就可以通過(guò)可執(zhí)行代碼生成對(duì)應(yīng)的 VNode Tree,然后最終映射為真實(shí)的 DOM Tree 在頁(yè)面上。
同樣地,這一階段在「Vue2.x」也是由 generate
函數(shù)完成,它會(huì)生成是諸如 _l
、_c
之類(lèi)的函數(shù),這本質(zhì)上是對(duì) _createElement
函數(shù)的封裝。而相比較「Vue2.x」版本的 generate
,「Vue3」改變了很多,其 generate
函數(shù)對(duì)應(yīng)的偽代碼會(huì)是這樣:
export function generate(
ast: RootNode,
options: CodegenOptions & {
onContextCreated?: (context: CodegenContext) => void
} = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
if (options.onContextCreated) options.onContextCreated(context)
const {
mode,
push,
prefixIdentifiers,
indent,
deindent,
newline,
scopeId,
ssr
} = context
...
genFunctionPreamble(ast, context)
...
if (!ssr) {
...
push(`function render(_ctx, _cache${optimizeSources}) {`)
}
....
return {
ast,
code: context.code,
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined
}
}
所以,接下來(lái),我們就來(lái)「一睹」帶有靜態(tài)節(jié)點(diǎn)對(duì)應(yīng)的 AST 生成的可執(zhí)行代碼的過(guò)程會(huì)是怎樣。
CodegenContext 代碼生成上下文
從上面 generate
函數(shù)的偽代碼可以看到,在函數(shù)的開(kāi)始調(diào)用了 createCodegenContext
為當(dāng)前 AST 生成了一個(gè) context
。在整個(gè) generate
函數(shù)的執(zhí)行過(guò)程「都依托」于一個(gè) CodegenContext
「生成代碼上下文」(對(duì)象)的能力,它是通過(guò) createCodegenContext
函數(shù)生成。而 CodegenContext
的接口定義會(huì)是這樣:
interface CodegenContext
extends Omit {
source: string
code: string
line: number
column: number
offset: number
indentLevel: number
pure: boolean
map?: SourceMapGenerator
helper(key: symbol): string
push(code: string, node?: CodegenNode): void
indent(): void
deindent(withoutNewLine?: boolean): void
newline(): void
}
可以看到 CodegenContext
對(duì)象中有諸如 push
、indent
、newline
之類(lèi)的方法。而它們的作用是在根據(jù) AST 來(lái)生成代碼時(shí)用來(lái)「實(shí)現(xiàn)換行」、「添加代碼」、「縮進(jìn)」等功能。從而,最終形成一個(gè)個(gè)可執(zhí)行代碼,即我們所認(rèn)知的 render
函數(shù),并且,它會(huì)作為 CodegenContext
的 code
屬性的值返回。
下面,我們就來(lái)看下靜態(tài)節(jié)點(diǎn)的可執(zhí)行代碼生成的核心,它被稱為 Preamble
前導(dǎo)。
genFunctionPreamble 生成前準(zhǔn)備
整個(gè)靜態(tài)提升的可執(zhí)行代碼生成就是在 genFunctionPreamble
函數(shù)部分完成的。并且,大家仔細(xì)「斟酌」一番靜態(tài)提升的字眼,靜態(tài)二字我們可以不看,但是「提升二字」,直抒本意地表達(dá)出它(靜態(tài)節(jié)點(diǎn))被「提高了」。
為什么說(shuō)是提高了?因?yàn)樵谠创a中的體現(xiàn),確實(shí)是被提高了。在前面的 generate
函數(shù),我們可以看到 genFunctionPreamble
是先于 render
函數(shù)加入context.code
中,所以,在 Runtime 時(shí)的 Render 階段,它會(huì)先于 render
函數(shù)執(zhí)行。
geneFunctionPreamble
函數(shù)(偽代碼):
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const {
ssr,
prefixIdentifiers,
push,
newline,
runtimeModuleName,
runtimeGlobalName
} = context
...
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
if (ast.helpers.length > 0) {
...
if (ast.hoists.length) {
const staticHelpers = [
CREATE_VNODE,
CREATE_COMMENT,
CREATE_TEXT,
CREATE_STATIC
]
.filter(helper => ast.helpers.includes(helper))
.map(aliasHelper)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
}
}
...
genHoists(ast.hoists, context)
newline()
push(`return `)
}
可以看到,這里會(huì)對(duì)前面我們?cè)?transform
函數(shù)提及的 hoists
屬性的長(zhǎng)度進(jìn)行判斷。顯然,對(duì)于前面說(shuō)的這個(gè)栗子,它的 ast.hoists.length
長(zhǎng)度是大于 0 的。所以,這里就會(huì)根據(jù) hoists
中的 AST 生成對(duì)應(yīng)的可執(zhí)行代碼。因此,到這里,生成的可執(zhí)行代碼會(huì)是這樣:
const _Vue = Vue
const { createVNode: _createVNode } = _Vue
// 靜態(tài)提升部分
const _hoisted_1 = _createVNode("div", null, "hi vue3", -1 /* HOISTED */)
// render 函數(shù)會(huì)在這下面
小結(jié)
靜態(tài)節(jié)點(diǎn)提升在整個(gè) compile
編譯階段體現(xiàn),從最初的 baseCompile
到 transform
轉(zhuǎn)化原始 AST、再到 generate
的優(yōu)先 render
函數(shù)處理生成可執(zhí)行代碼,最后交給 Runtime 時(shí)的 Render 執(zhí)行,這種設(shè)計(jì)可以說(shuō)是非常精妙!所以,這樣一來(lái),就完成了我們經(jīng)??吹皆谝恍┪恼绿峒暗摹竀ue3」對(duì)于靜態(tài)節(jié)點(diǎn)在整個(gè)生命周期中它只會(huì)執(zhí)行「一次創(chuàng)建」的源碼實(shí)現(xiàn),這在一定程度上降低了性能上的開(kāi)銷(xiāo)。
以上就是W3Cschool編程獅
關(guān)于Vue 3.0 diff 新特性 - 靜態(tài)節(jié)點(diǎn)提升的相關(guān)介紹了,希望對(duì)大家有所幫助。