在 Vue3 源碼解析系列的第一篇文章中,筆者帶領(lǐng)大家一起走了一遍一個(gè) Vue 對(duì)象實(shí)例化的流程,在一起看 @vue/compiler-core 編譯模塊的時(shí)候,首次出現(xiàn)了代碼生成器 —— generate 模塊。為了幫助大家回顧,我們?cè)賮?lái)看一遍 compile 編譯過(guò)程中發(fā)生了什么。
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
const onError = options.onError || defaultOnError
const isModuleMode = options.mode === 'module'
const prefixIdentifiers =
!__BROWSER__ && (options.prefixIdentifiers === true || isModuleMode)
// 生成 AST 抽象語(yǔ)法樹(shù)
const ast = isString(template) ? baseParse(template, options) : template
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
prefixIdentifiers
)
// 對(duì) AST 抽象語(yǔ)法樹(shù)執(zhí)行轉(zhuǎn)換
transform(
ast,
extend({}, options, {})
)
// 返回代碼生成器生成的代碼字符串
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
在筆者給出的編譯模塊的簡(jiǎn)化源碼中,可以看到我們之前幾篇文章提及到的生成 AST 抽象語(yǔ)法樹(shù),以及節(jié)點(diǎn)轉(zhuǎn)換器的轉(zhuǎn)換節(jié)點(diǎn)的注釋?zhuān)裉煳覀円v的就是最后一行代碼中 generate 函數(shù)做了什么事情。
代碼生成器是什么
代碼生成器是什么?它有什么作用?在回答這些問(wèn)題以前,我們還是要從編譯流程中說(shuō)起,在生成一個(gè) Vue 對(duì)象的編譯過(guò)程執(zhí)行結(jié)束時(shí),我們會(huì)從編譯的結(jié)果中拿到一個(gè)名叫 code 的 string 類(lèi)型的變量。而這個(gè)變量就是我們今天通篇會(huì)提及的代碼字符串,Vue 會(huì)用這個(gè)生成的代碼字符串,配合 Function 類(lèi)的構(gòu)造函數(shù)生成 render 渲染函數(shù),最終用生成的渲染函數(shù)完成對(duì)應(yīng)組件的渲染,在源碼中是如下這樣實(shí)現(xiàn)的。
function compileToFunction(
template: string | HTMLElement,
options?: CompilerOptions
): RenderFunction {
const key = template
// 執(zhí)行編譯函數(shù),并從結(jié)果中結(jié)構(gòu)出代碼字符串
const { code } = compile(
template,
extend(
{
hoistStatic: true,
onError: __DEV__ ? onError : undefined,
onWarn: __DEV__ ? e => onError(e, true) : NOOP
} as CompilerOptions,
options
)
)
// 通過(guò) Function 構(gòu)造方法,生成 render 函數(shù)
const render = (__GLOBAL__
? new Function(code)()
: new Function('Vue', code)(runtimeDom)) as RenderFunction
;(render as InternalRenderFunction)._rc = true
// 返回生成的 render 函數(shù),并緩存
return (compileCache[key] = render)
}
那么接下來(lái),筆者就帶大家直入代碼生成器模塊,從 generate 函數(shù)入手,查看生成器的工作方式。
代碼生成上下文
generate 函數(shù)位于 packages/compiler-core/src/codegen.ts 的位置,我們先看一下它的函數(shù)簽名。
export function generate(
ast: RootNode,
options: CodegenOptions & {
onContextCreated?: (context: CodegenContext) => void
} = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
/* 忽略后續(xù)邏輯 */
}
export interface CodegenResult {
code: string
preamble: string
ast: RootNode
map?: RawSourceMap
}
generate 函數(shù),接收兩個(gè)參數(shù),分別是經(jīng)過(guò)轉(zhuǎn)換器處理的 ast 抽象語(yǔ)法樹(shù),以及 options 代碼生成選項(xiàng)。最終返回一個(gè) CodegenResult 類(lèi)型的對(duì)象。
可以看到 CodegenResult 中包含了 code 代碼字符串、ast 抽象語(yǔ)法樹(shù)、可選的 sourceMap、以及代碼字符串的前置部分 preamble。
而 generate 的函數(shù),第一行就是生成一個(gè)上下文對(duì)象,這里為了語(yǔ)義上的更好理解,我們稱這個(gè) context 為代碼生成器的上下文對(duì)象,簡(jiǎn)稱生成器上下文。
生成器上下文中除了一些屬性外,會(huì)留意到它有 5 個(gè)工具函數(shù),這里重點(diǎn)看一下 push 函數(shù)。
講解 push 之前,沒(méi)看過(guò)代碼的朋友可能會(huì)有點(diǎn)迷惑,一個(gè)向數(shù)組內(nèi)添加元素的函數(shù)有什么好說(shuō)的呢?但是此 push 非彼 push,筆者先給大家看一下 push 的實(shí)現(xiàn)。
push(code, node) {
context.code += code
if (!__BROWSER__ && context.map) {
if (node) {
let name
/* 忽略邏輯 */
addMapping(node.loc.start, name)
}
advancePositionWithMutation(context, code)
if (node && node.loc !== locStub) {
addMapping(node.loc.end)
}
}
}
看完上方 push 的實(shí)現(xiàn),能夠發(fā)現(xiàn) push 并非是向數(shù)組中推送元素,而是拼接字符串,將傳入的字符串拼接入上下文中的 code 屬性中。并且會(huì)調(diào)用 addMapping 生成對(duì)應(yīng)的 sourceMap。這個(gè)函數(shù)是作用很重要,當(dāng)生成器處理完 ast 樹(shù)中的每個(gè)節(jié)點(diǎn)時(shí),都會(huì)調(diào)用 push,向之前已經(jīng)生成好的代碼字符串中去拼接新生成的字符串。直至最終,拿到完整的代碼字符串,并作為結(jié)果返回。
context 中除了 push,還有 indent、deindent、newline 這些處理字符串位置的函數(shù),分別的作用是縮進(jìn)、回退縮進(jìn)、插入新的一行。是用來(lái)輔助生成的代碼字符串,格式化結(jié)構(gòu)用的,讓生成的代碼字符串非常直觀,就像在 ide 中敲入的一樣。
而 context 中還有
執(zhí)行流程
當(dāng)生成器上下文創(chuàng)建好之后,generate 函數(shù)會(huì)接著向下執(zhí)行,接下來(lái)筆者就和大家繼續(xù)往下閱讀,分析生成器的執(zhí)行流程。在本節(jié)中我放入的代碼全部都是 generate 函數(shù)體內(nèi)的,所以為了更簡(jiǎn)短的篇幅,generate 的函數(shù)簽名就不會(huì)再重復(fù)放入了。
代碼字符串 前置內(nèi)容生成
const hasHelpers = ast.helpers.length > 0 // 是否存在 helpers 輔助函數(shù)
const useWithBlock = !prefixIdentifiers && mode !== 'module' // 使用 with 擴(kuò)展作用域
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
// 不在瀏覽器的環(huán)境且 mode 是 module
if (!__BROWSER__ && mode === 'module') {
// 使用 ES module 標(biāo)準(zhǔn)的 import 來(lái)導(dǎo)入 helper 的輔助函數(shù),處理生成代碼的前置部分
genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
} else {
// 否則生成的代碼前置部分是一個(gè)單一的 const { helpers... } = Vue 處理代碼前置部分
genFunctionPreamble(ast, preambleContext)
}
在創(chuàng)建完上下文,從上下文中解構(gòu)完一些對(duì)象后,會(huì)生成代碼字符串的前置部分,這里有個(gè)關(guān)鍵判斷是 mode 屬性,根據(jù) mode 屬性來(lái)判斷使用何種方式引入 helpers 輔助函數(shù)的聲明。
mode 有兩個(gè)選項(xiàng),'module' 或 'function'。當(dāng)傳入的參數(shù)是 module 時(shí),會(huì)通過(guò) ES module 的 import 來(lái)導(dǎo)入 ast 中的 helpers 輔助函數(shù),并用 export 默認(rèn)導(dǎo)出 render 函數(shù)。當(dāng)傳入的參數(shù)是 function 時(shí),就會(huì)生成一個(gè)單一的 const { helpers... } = Vue 聲明,并且 return 返回 render 函數(shù),而不是通過(guò) export 導(dǎo)出。下面的代碼框注中我放入了兩種模式生成的代碼前置部分的區(qū)別。
// mode === 'module' 生成的前置部分
'import { createVNode as _createVNode, resolveDirective as _resolveDirective } from "vue"
export '
// mode === 'function' 生成的前置部分
'const { createVNode: _createVNode, resolveDirective: _resolveDirective } = Vue
return '
要注意以上代碼僅僅是代碼前置部分,咱們還沒(méi)有開(kāi)始解析其他資源和節(jié)點(diǎn),所以僅僅是到了 export 或者 return 就戛然而止了。
在明白了前置部分的區(qū)別后,我們接著往下看代碼。
生成 render 函數(shù)簽名
接下來(lái)生成器會(huì)開(kāi)始生成 render 函數(shù)的函數(shù)體,首先從函數(shù)名、以及給 render 函數(shù)的傳參開(kāi)始。當(dāng)確定了函數(shù)簽名后,如果 mode 是 function 的情況,生成器會(huì)使用 with 來(lái)擴(kuò)展作用域,最后生成的模樣在第一篇編譯流程中也已經(jīng)展示過(guò)。
首先會(huì)根據(jù)是否是服務(wù)端渲染,ssr 的標(biāo)記來(lái)確定函數(shù)名 functionName 以及要傳入函數(shù)的參數(shù) args,并且在函數(shù)簽名部分會(huì)判斷是否是 TypeScript 的環(huán)境,如果是 TypeScript 的話,會(huì)給參數(shù)標(biāo)記為 any 類(lèi)型。
之后會(huì)判斷是通過(guò)箭頭函數(shù)還是函數(shù)聲明來(lái)創(chuàng)建函數(shù)。
在函數(shù)創(chuàng)建好后,函數(shù)體內(nèi)會(huì)判斷是否需要通過(guò) with 來(lái)擴(kuò)展作用域,并且此時(shí)如果有 helpers 輔助函數(shù),也會(huì)解構(gòu)在 with 的塊級(jí)作用域內(nèi),解構(gòu)以后也會(huì)重命名變量,防止與用戶的變量名沖突。
具體的代碼邏輯在下方。
// 生成后的函數(shù)名
const functionName = ssr ? `ssrRender` : `render`
// 函數(shù)的傳參
const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
/* 忽略邏輯 */
// 函數(shù)簽名,是 TypeScript 的話標(biāo)記為 any 類(lèi)型
const signature =
!__BROWSER__ && options.isTS
? args.map(arg => `${arg}: any`).join(',')
: args.join(', ')
/* 忽略邏輯 */
// 使用箭頭函數(shù)還是函數(shù)聲明來(lái)創(chuàng)建渲染函數(shù)
if (isSetupInlined || genScopeId) {
push(`(${signature}) => {`)
} else {
push(`function ${functionName}(${signature}) {`)
}
indent()
// 使用 with 擴(kuò)展作用域
if (useWithBlock) {
push(`with (_ctx) {`)
indent()
// 在 function mode 中,const 聲明應(yīng)該在代碼塊中,
// 并且應(yīng)該重命名解構(gòu)的變量,防止變量名和用戶的變量名沖突
if (hasHelpers) {
push(
`const { ${ast.helpers
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')} } = _Vue`
)
push(`\n`)
newline()
}
}
資源的分解聲明
在看到“資源的分解聲明”這個(gè)小標(biāo)題之前,我們先需要搞明白生成器把什么定義成資源。生成器將 AST 抽象語(yǔ)法樹(shù)中解析出的 components 組件,directives 指令,temps 臨時(shí)變量,以及上個(gè)月尤大又在 Vue3 中兼容了 Vue2 filters 過(guò)濾器這四樣類(lèi)型當(dāng)做資源。
在 render 函數(shù)中,該部分的處理會(huì)將上述資源都提前聲明出來(lái),將 AST 樹(shù)中解析出的資源 id 傳入每個(gè)資源對(duì)應(yīng)的處理函數(shù),并生成對(duì)應(yīng)的資源變量。
// 如果 ast 中有組件,解析組件
if (ast.components.length) {
genAssets(ast.components, 'component', context)
if (ast.directives.length || ast.temps > 0) {
newline()
}
}
/* 省略 指令和過(guò)濾器,邏輯與組件一致 */
if (ast.temps > 0) {
push(`let `)
for (let i = 0; i < ast.temps; i++) {
push(`${i > 0 ? `, ` : ``}_temp${i}`) // 通過(guò) let 聲明變量
}
}
在上面源碼中,我放入了兩個(gè)典型代表,components 以及 temps。舉個(gè)例子,給大家看看生成代碼后的結(jié)果。
components: [`Foo`, `bar-baz`, `barbaz`, `Qux__self`],
directives: [`my_dir_0`, `my_dir_1`],
temps: 3
假設(shè)在 AST 中有如下資源,4 個(gè)組件,ID 分別為 Foo、bar-baz、barbaz、Qux__self。2 個(gè)指令,ID 分別為 my_dir_0, my_dir_1,以及有 3 個(gè)臨時(shí)變量。這些資源被解析后生成如下所示的代碼字符串。
const _component_Foo = _resolveComponent("Foo")
const _component_bar_baz = _resolveComponent("bar-baz")
const _component_barbaz = _resolveComponent("barbaz")
const _component_Qux = _resolveComponent("Qux", true)
const _directive_my_dir_0 = _resolveDirective("my_dir_0")
const _directive_my_dir_1 = _resolveDirective("my_dir_1")
let _temp0, _temp1, _temp2
不必去糾結(jié) resolve 函數(shù)中做了什么事情,我們只需要知道代碼生成器會(huì)生成怎樣的代碼即可。
所以從結(jié)果去倒推 genAssets 函數(shù)做過(guò)的事情,就是根據(jù)資源類(lèi)型 + 資源 ID 當(dāng)做變量名,并將資源ID傳入類(lèi)型對(duì)應(yīng)的 resolve 函數(shù),并將結(jié)果賦值給聲明的變量。
而 temps 的處理在上方源碼已經(jīng)寫(xiě)的很清楚了。
返回結(jié)果
在生成 render 函數(shù)體,處理完資源后,生成器會(huì)開(kāi)始最關(guān)鍵一步——生成節(jié)點(diǎn)對(duì)應(yīng)的代碼字符串,在處理完所有節(jié)點(diǎn)后,會(huì)將生成的結(jié)果返回。由于節(jié)點(diǎn)的重要性,我們選擇將此部分放在后面單獨(dú)說(shuō)。至此代碼字符串生成完畢,最終會(huì)返回一個(gè) CodegenResult 類(lèi)型的對(duì)象。
生成節(jié)點(diǎn)
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
}
當(dāng)生成器判斷 ast 中有 codegenNode 的節(jié)點(diǎn)屬性后,會(huì)調(diào)用 genNode 來(lái)生成節(jié)點(diǎn)對(duì)應(yīng)的代碼字符串。接下來(lái)我們就來(lái)詳細(xì)看一下 genNode 函數(shù)。
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
// 如果是字符串,直接 push 入代碼字符串
if (isString(node)) {
context.push(node)
return
}
// 如果 node 是 symbol 類(lèi)型,傳入輔助函數(shù)生成的代碼字符串
if (isSymbol(node)) {
context.push(context.helper(node))
return
}
// 判斷 node 類(lèi)型
switch (node.type) {
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:
genNode(node.codegenNode!, context)
break
case NodeTypes.TEXT:
genText(node, context)
break
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context)
break
/* 忽略剩余 case 分支 */
}
}
genNode 函數(shù)會(huì)先判斷節(jié)點(diǎn)的類(lèi)型,對(duì)于字符串或 symbol 類(lèi)型的節(jié)點(diǎn),會(huì)直接拼接進(jìn)代碼字符串中,之后通過(guò)一個(gè) Switch-Case 條件分支判斷 node 節(jié)點(diǎn)的類(lèi)型。而由于判斷條件很多,這里會(huì)忽略大部分條件,只舉幾個(gè)典型的類(lèi)型來(lái)分析。
首先是第一個(gè) case,當(dāng)遇到 Element、IF 或 FOR 類(lèi)型的節(jié)點(diǎn)類(lèi)型時(shí),會(huì)遞歸的調(diào)用 genNode,繼續(xù)去生成這三種節(jié)點(diǎn)類(lèi)型的子節(jié)點(diǎn),這樣能夠保證遍歷的完整性。
而當(dāng)節(jié)點(diǎn)是一個(gè)文本類(lèi)型時(shí),會(huì)調(diào)用 genText 函數(shù),直接將文本通過(guò) JSON.stringify 序列化拼接進(jìn)代碼字符串中。
當(dāng)節(jié)點(diǎn)是一個(gè)簡(jiǎn)單表達(dá)式時(shí),會(huì)判斷該表達(dá)式是否是靜態(tài)的,如果是靜態(tài)的,則通過(guò) JSON 字符串序列化后拼入代碼字符串,否則直接拼接表達(dá)式對(duì)應(yīng)的 content。
通過(guò)這三個(gè)節(jié)點(diǎn)的分析,我們能知道其實(shí)生成器是根據(jù)不同節(jié)點(diǎn)的類(lèi)型,push 進(jìn)不同的代碼字符串,而對(duì)于存在子節(jié)點(diǎn)的節(jié)點(diǎn),又回去遞歸遍歷,確保每個(gè)節(jié)點(diǎn)都能生成對(duì)應(yīng)的代碼字符串。
處理靜態(tài)提升
在筆者講述生成器生成代碼前置部分時(shí),看源碼會(huì)發(fā)現(xiàn)根據(jù) mode 類(lèi)型,調(diào)用了 genModulePreamble 或 genFunctionPreamble 函數(shù)。而在這兩個(gè)函數(shù)中,都有一行相同的代碼:genHoists(ast.hoists, context)。
這個(gè)函數(shù)就是用來(lái)處理靜態(tài)提升的,在上一篇文章中,筆者給大家介紹了靜態(tài)提升,并舉了例子,說(shuō)明靜態(tài)提升會(huì)提前將靜態(tài)節(jié)點(diǎn)提取出來(lái),生成對(duì)應(yīng)的序列化字符串。而今天筆者準(zhǔn)備接上一篇文章的內(nèi)容,再跟大家一起探究生成器是怎么處理靜態(tài)提升的。
function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) {
if (!hoists.length) {
return
}
context.pure = true
const { push, newline, helper, scopeId, mode } = context
newline()
hoists.forEach((exp, i) => {
if (exp) {
push(`const _hoisted_${i + 1} = `)
genNode(exp, context)
newline()
}
})
context.pure = false
}
這里我直接放上了生成靜態(tài)提升的 genHoists 代碼,逐步分析邏輯。首先呢,函數(shù)接受 ast 樹(shù)中 hoists 的屬性的入?yún)?,是一組節(jié)點(diǎn)類(lèi)型的集合的數(shù)組,并接受生成器上下文,一共有兩個(gè)參數(shù)。
如果 hoists 數(shù)組中沒(méi)有元素,說(shuō)明不存在需要靜態(tài)提升的節(jié)點(diǎn),那直接返回即可。
否則就是存在需要提升的節(jié)點(diǎn),那么將上下文的 pure 標(biāo)記置為 true。
之后 forEach 遍歷 hoists 數(shù)組,并且根據(jù)數(shù)組的 index 生成靜態(tài)提升的變量名 _hoisted_${index + 1},之后調(diào)用 genNode 函數(shù),生成靜態(tài)提升節(jié)點(diǎn)的代碼字符串,賦值給之前聲明的變量 _hoisted_${index + 1}。
在遍歷完所有的需要提升的變量后,將 pure 標(biāo)記置為 false。
而這里 pure 標(biāo)記的作用,就是在某些節(jié)點(diǎn)類(lèi)型生成字符串前,添加 /*#__PURE__*/ 注釋前綴,表明該節(jié)點(diǎn)是靜態(tài)節(jié)點(diǎn)。
總結(jié)
在本文中,筆者帶領(lǐng)大家一起閱讀了生成器 generate 模塊的源碼,介紹了生成器的作用,以及介紹了生成器上下文,并且說(shuō)明是生成器上下文中的工具函數(shù)的用法。之后筆者對(duì) generate 函數(shù)的整體執(zhí)行流程進(jìn)行講解,從渲染函數(shù)的前置內(nèi)容,到函數(shù)簽名的生成,再到處理 ast 中的資源,到最終返回代碼字符串的結(jié)果,如果有人對(duì)結(jié)果感興趣,可以去看本系列的第一篇文章,在那篇文章中有 render 函數(shù)最終返回的示例。最后我們又著重講了生成節(jié)點(diǎn) genNode 函數(shù),以及為了呼應(yīng)上篇文章靜態(tài)提升篇,筆者又給大家講解了靜態(tài)節(jié)點(diǎn)的生成函數(shù),以及生成過(guò)程。
最后,如果這篇文章能夠幫助到你再深一點(diǎn)的理解 Vue3 的特性,希望能給本文點(diǎn)一個(gè)喜歡??。如果想繼續(xù)追蹤后續(xù)文章,也可以關(guān)注我的賬號(hào)或 follow 我的 github,再次謝謝各位可愛(ài)的看官老爺。