您好,登錄后才能下訂單哦!
今天小編給大家分享一下Vue3.0靜態(tài)節(jié)點(diǎn)提升是什么的相關(guān)知識(shí)點(diǎn),內(nèi)容詳細(xì),邏輯清晰,相信大部分人都還太了解這方面的知識(shí),所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
「靜態(tài)節(jié)點(diǎn)提升」是「Vue3」針對(duì) VNode
更新過程性能問題而提出的一個(gè)優(yōu)化點(diǎn)。眾所周知,在大型應(yīng)用場(chǎng)景下,「Vue2.x」 的 patchVNode
過程,即 diff
過程是非常緩慢的,這是一個(gè)十分令人頭疼的問題。
雖然,對(duì)于面試常問的 diff
過程在一定程度上是減少了對(duì) DOM
的直接操作。但是,「這個(gè)減少是有一定成本的」。因?yàn)?,如果是?fù)雜應(yīng)用,那么就會(huì)存在父子關(guān)系非常復(fù)雜的 VNode
,而這也就是 diff
的痛點(diǎn),它會(huì)不斷地遞歸調(diào)用 patchVNode
,不斷堆疊而成的幾毫秒,最終就會(huì)造成 VNode
更新緩慢。
也因此,這也是為什么我們所看到的大型應(yīng)用諸如阿里云之類的采用的是基于「React」的技術(shù)棧的原因之一。所以,「Vue3」也是痛改前非,重寫了整個(gè) Compiler
過程,提出了靜態(tài)提升、靶向更新等優(yōu)化點(diǎn),來提高 patchVNode
過程。
那么,回到今天的正題,我們從源碼角度看看在整個(gè)編譯過程「Vue3」靜態(tài)節(jié)點(diǎn)提升究竟是「何許人也」?
由于,在 compile
過程的 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
的效果。因此,這樣一來一往,也就是耳熟能詳?shù)?Vue3 巧妙結(jié)合 runtime
與 compiler
實(shí)現(xiàn)靶向更新和靜態(tài)提升。
而在源碼中 patchFlag
被定義為一個(gè)「數(shù)字枚舉類型」
并且,值得一提的是整體上 patchFlag
的分為兩大類:
當(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
的比較更新過程。
其實(shí),還有兩類特殊的
flag
:shapeFlag
和slogFlag
,這里我就不對(duì)此展開,有興趣的同學(xué)可以自行去了解。
了解過「Vue2.x」源碼的同學(xué),我想應(yīng)該都知道在「Vue2.x」中的 Compile
過程會(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
之類的。
而在「Vue3」中,整體的 Compile
過程仍然是三個(gè)階段,但是不同于「Vue2.x」的是,第二個(gè)階段換成了正常編譯器都會(huì)存在的階段 transform
。
在源碼中,它對(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ì)問為什么會(huì)是 transform
?它的職責(zé)是什么?
通過簡(jiǎn)單的對(duì)比「Vue2.x」編譯過程的第二階段的 optimize
,很明顯,transform
并不是「無米之炊」,它仍然有著「優(yōu)化」原始 AST 的作用,而具體職責(zé)會(huì)表現(xiàn)在:
對(duì)所有 AST Element 新增 codegen
屬性來幫助 generate
更準(zhǔn)確地生成「最優(yōu)」的可執(zhí)行代碼。
對(duì)靜態(tài) AST Element 新增 hoists
屬性來實(shí)現(xiàn)靜態(tài)節(jié)點(diǎn)的「單獨(dú)創(chuàng)建」。
...
此外,transform
還標(biāo)識(shí)了諸如 isBlock
、helpers
等屬性,來生成最優(yōu)的可執(zhí)行代碼,這里我們就不細(xì)談,有興趣的同學(xué)可以自行了解。
baseParse
顧名思義起著「解析」的作用,它的表現(xiàn)和「Vue2.x」的 parse
相同,都是解析模板 tempalte
生成「原始 AST」。
假設(shè),此時(shí)我們有一個(gè)這樣的模板 template
:
<div><div>hi vue3</div><div>{{msg}}</div></div>
那么,它在經(jīng)過 baseParse
處理后生成的 AST 看起來會(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 }
如果,了解過「Vue2.x」編譯過程的同學(xué)應(yīng)該對(duì)于上面這顆 AST
的大部分屬性不會(huì)陌生。AST
的本質(zhì)是通過用對(duì)象來描述「DSL」(特殊領(lǐng)域語言),例如:
children
中存放的就是最外層 div
的后代。
loc
則用來描述這個(gè) AST Element 在整個(gè)字符串(template
)中的位置信息。
type
則是用于描述這個(gè)元素的類型(例如 5 為插值、2 為文本)等等。
并且,可以看到的是不同于「Vue2.x」的 AST,這里我們多了諸如 helpers
、codegenNode
、hoists
等屬性。而,這些屬性會(huì)在 transform
階段進(jìn)行相應(yīng)地賦值,進(jìn)而幫助 generate
階段生成「更優(yōu)的」可執(zhí)行代碼。
對(duì)于 transform
階段,如果了解過「編譯器」的工作流程的同學(xué)應(yīng)該知道,一個(gè)完整的編譯器的工作流程會(huì)是這樣:
首先,parse
解析原始代碼字符串,生成抽象語法樹 AST。
其次,transform
轉(zhuǎn)化抽象語法樹,讓它變成更貼近目標(biāo)「DSL」的結(jié)構(gòu)。
最后,codegen
根據(jù)轉(zhuǎn)化后的抽象語法樹生成目標(biāo)「DSL」的可執(zhí)行代碼。
而在「Vue3」采用 Monorepo
的方式管理項(xiàng)目后,compile
對(duì)應(yīng)的能力就是一個(gè)編譯器。所以,transform
也是整個(gè)編譯過程的重中之重。換句話說,如果沒有 transform
對(duì) AST 做諸多層面的轉(zhuǎn)化,「Vue」仍然會(huì)掛在 diff
這個(gè)「飽受詬病」的過程。
相比之下,「Vue2.x」的編譯階段沒有完整的
transform
,只是optimize
優(yōu)化了一下 AST,可以想象在「Vue」設(shè)計(jì)之初尤大也沒想到它以后會(huì)「這么地流行」!
那么,我們來看看 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ù)做了什么,在它的定義中是「一覽無余」。這里我們提一下它對(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ù),大致可以分為兩類:
靜態(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)樣式的綁定等。
那么,我們就來看看對(duì)于靜態(tài)節(jié)點(diǎn) transform
是如何應(yīng)用的?
transform
應(yīng)用這里,對(duì)于上面我們說到的這個(gè)栗子,靜態(tài)節(jié)點(diǎn)就是這個(gè)部分:
<div>hi vue3</div>
而它在沒有進(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
。而在源碼中各類 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 所屬的類型就是 Text
。那么,我們先來看一下 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
這類 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
的類型,我們可以知道它的 patchFlag
為默認(rèn)值 0
。所以,它的 codegenNode
屬性值看起來會(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
是 compile
階段的最后一步,它的作用是將 transform
轉(zhuǎn)換后的 AST 生成對(duì)應(yīng)的「可執(zhí)行代碼」,從而在之后 Runtime 的 Render 階段時(shí),就可以通過可執(zhí)行代碼生成對(duì)應(yīng)的 VNode Tree,然后最終映射為真實(shí)的 DOM Tree 在頁面上。
同樣地,這一階段在「Vue2.x」也是由 generate
函數(shù)完成,它會(huì)生成是諸如 _l
、_c
之類的函數(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 } }
所以,接下來,我們就來「一睹」帶有靜態(tài)節(jié)點(diǎn)對(duì)應(yīng)的 AST 生成的可執(zhí)行代碼的過程會(huì)是怎樣。
從上面 generate
函數(shù)的偽代碼可以看到,在函數(shù)的開始調(diào)用了 createCodegenContext
為當(dāng)前 AST 生成了一個(gè) context
。在整個(gè) generate
函數(shù)的執(zhí)行過程「都依托」于一個(gè) CodegenContext
「生成代碼上下文」(對(duì)象)的能力,它是通過 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
之類的方法。而它們的作用是在根據(jù) AST 來生成代碼時(shí)用來「實(shí)現(xiàn)換行」、「添加代碼」、「縮進(jìn)」等功能。從而,最終形成一個(gè)個(gè)可執(zhí)行代碼,即我們所認(rèn)知的 render
函數(shù),并且,它會(huì)作為 CodegenContext
的 code
屬性的值返回。
下面,我們就來看下靜態(tài)節(jié)點(diǎn)的可執(zhí)行代碼生成的核心,它被稱為 Preamble
前導(dǎo)。
整個(gè)靜態(tài)提升的可執(zhí)行代碼生成就是在 genFunctionPreamble
函數(shù)部分完成的。并且,大家仔細(xì)「斟酌」一番靜態(tài)提升的字眼,靜態(tài)二字我們可以不看,但是「提升二字」,直抒本意地表達(dá)出它(靜態(tài)節(jié)點(diǎn))被「提高了」。
為什么說是提高了?因?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ì)于前面說的這個(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ì)在這下面
以上就是“Vue3.0靜態(tài)節(jié)點(diǎn)提升是什么”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會(huì)為大家更新不同的知識(shí),如果還想學(xué)習(xí)更多的知識(shí),請(qǐng)關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。