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