Vue3 源碼解析(四):代碼生成器

在 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)用了 genModulePreamblegenFunctionPreamble 函數(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)的看官老爺。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容