携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情 >>
版本:3.2.31
代码生成器 generate 在编译器的编译过程中负责将 JavaScript AST 转换成渲染函数,如下图所示:
代码生成也是编译器的最后一步,如下面的源码所示:
// packages/compiler-core/src/compile.ts
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
// 省略部分代码
// 1. 生成模板AST
const ast = isString(template) ? baseParse(template, options) : template
// 省略部分代码
// 2. 将 模板AST 转换成 JavaScript AST
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend(
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)
// 3. 将JavaScript AST 转换成渲染函数,generate函数会将渲染函数的代码以字符串的形式返回。
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
下面,我们从代码生成器的入口函数 generate 入手,来探究生成器的工作方式。
generate 函数签名
// packages/compiler-core/src/codegen.ts
export function generate(
ast: RootNode, // JavaScript AST
options: CodegenOptions & {
onContextCreated?: (context: CodegenContext) => void
} = {}
): CodegenResult {}
export interface CodegenResult {
code: string
preamble: string
ast: RootNode
map?: RawSourceMap
}
从上面的源码中可以看到,generate 函数接收两个参数:
- ast:经过 transform 转换器处理后的 JavaScript AST
- options:代码生成选项,如生成的代码模式 mode,是否生成 source map 等
函数最终返回一个 CodegenResult 类型的对象,其中包含了最终生成的渲染函数的代码字符串,代码字符串的前置部分 preamble、JavaScript AST 抽象语法树以及可选的 source map。
看完了 generate 函数的签名部分,我们开始进入函数的函数体部分。
生成器的执行流程
在深入分析生成器的执行流程前,我们先通过一个流程图来了解一下生成器的执行流程有哪些。如下图:
代码生成上下文
代码生成过程中的上下文对象,用来维护代码生成过程中程序的运行状态。在 generate 函数体中首先要做的事情就是创建一个上下文对象,如下面的源代码所示:
// packages/compiler-core/src/codegen.ts
export function generate(
ast: RootNode, // JavaScript AST
options: CodegenOptions & {
onContextCreated?: (context: CodegenContext) => void
} = {}
): CodegenResult {
// 创建生成器上下文对象,该上下文对象用来维护代码生成过程中程序的运行状态
const context = createCodegenContext(ast, options)
if (options.onContextCreated) options.onContextCreated(context)
// 解构上下文对象中的属性
const {
mode, // mode 的值有两个:function/module
push, // push 函数用来完成代码拼接
prefixIdentifiers,
indent, // indent 函数用来缩进代码
deindent, // deindent 函数用来取消缩进
newline, // newline 函数用来换行
scopeId,
ssr
} = context
// 省略部分代码
}
可以看到,代码生成上下文中包含mode、push、indent、deindent、newline 以及其它一些属性。
在代码生成上下文context的属性定义中,一共定义了5个方法,它们是 helper、push、indent、deIndent、newline。下面,我们来看看5个函数的实现。
helper 函数
// packages/compiler-core/src/codegen.ts
helper(key) {
return `_${helperNameMap[key]}`
},
helper 函数用来返回用于标识唯一值的 symbol 的字符串形式。
push 函数
// packages/compiler-core/src/codegen.ts
push(code, node) {
// 拼接代码字符串
context.code += code
// 生成对应的 sourceMap
if (!__BROWSER__ && context.map) {
if (node) {
let name
if (node.type === NodeTypes.SIMPLE_EXPRESSION && !node.isStatic) {
const content = node.content.replace(/^_ctx\./, '')
if (content !== node.content && isSimpleIdentifier(content)) {
name = content
}
}
addMapping(node.loc.start, name)
}
advancePositionWithMutation(context, code)
if (node && node.loc !== locStub) {
addMapping(node.loc.end)
}
}
},
可以看到,在 push 函数,通过字符串拼接的方式将字符串存储在上下文对象中的 code 属性中,从而完成代码的拼接。并且调用 addMapping 函数生成对应的 sourceMap。push 函数十分重要,在代码生成的过程中,编译器每处理一个JavaScript AST 节点时,都会调用 push 函数,向之前已经生成好的代码字符串中去拼接新生成的字符串。直到最后,拿到完整的代码字符串,并作为结果返回。
indent 函数
// packages/compiler-core/src/codegen.ts
indent() {
newline(++context.indentLevel)
},
indent 函数,用来缩进代码,即让 indentLevel 自增后,再调用 newline换行函数,如上面的源码所示。
deIndent 函数
// packages/compiler-core/src/codegen.ts
deindent(withoutNewLine = false) {
if (withoutNewLine) {
--context.indentLevel
} else {
newline(--context.indentLevel)
}
},
deIndent 函数,用来取消缩进,即让 indentLevel 自减后,再调 newline用换行函数
newline 函数
// packages/compiler-core/src/codegen.ts
newline() {
newline(context.indentLevel)
}
function newline(n: number) {
context.push('\n' + ` `.repeat(n))
}
newline 函数,用来换行,每次调用该函数时,都会在代码字符串后面追加换行符 \n。由于换行时,需要保留缩进,所以还要追加 currentIndent * 2 个空格符。
生成前置内容
我们在编写一个js文件时,通常需要引入某些模块的变量或方法,例如通过 import 语句引入 vue 的r响应式方法reactive,如下面的代码所示:
import { reactive } from 'vue'
这里的 import 语句就是我们在生成渲染函数时要生成的前置内容。
我们来看源码中关于生成前置内容的代码,如下面所示:
// 是否存在 helpers 辅助函数
const hasHelpers = ast.helpers.length > 0
// 使用 with 扩展作用域
const useWithBlock = !prefixIdentifiers && mode !== 'module'
// 不是浏览器环境且 mode 是 module,genScopeId 为 true时,要生成 scopeId
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
// 是否使用的是箭头函数创建渲染函数
const isSetupInlined = !__BROWSER__ && !!options.inline
// preambles
// in setup() inline mode, the preamble is generated in a sub context
// and returned separately.
const preambleContext = isSetupInlined
? createCodegenContext(ast, options)
: context
// 不是浏览器换并且 mode 是 module
if (!__BROWSER__ && mode === 'module') {
// 使用 ES module 标准的 import 来导入 helper 的辅助函数,处理生成代码的前置部分
// 例如:'import { createVNode as _createVNode, resolveDirective as _resolveDirective } from "vue"
// 生成 ES module 标准的 import 语句
genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
} else {
// 否则生成的代码的前置部分是一个单一的 const { helpers, ... } = Vue 或 const { helpers, ... } = require('Vue')
// 例如:'const { createVNode: _createVNode, resolveDirective: _resolveDirective } = Vue
// 生成 CommonJS 语法的变量导入语句
genFunctionPreamble(ast, preambleContext)
}
在这段代码中,有个关键的属性 mode,vue 根据该属性来判断使用何种方式引入 helpers 辅助函数的声明。
mode 属性的值有两个选项,'module' 和 'function'。如果 mode 的值传入的是 module,则调用 genModulePreamble 函数生成前置内容。如果 mode 的值传入的是 function,则调用genFunctionPreamble 生成前置内容。
我们分别来看看生成这两种前置内容的两个函数。
genModulePreamble 函数
// packages/compiler-core/src/codegen.ts
function genModulePreamble(
ast: RootNode,
context: CodegenContext,
genScopeId: boolean,
inline?: boolean
) {
const {
push,
newline,
optimizeImports,
runtimeModuleName,
ssrRuntimeModuleName
} = context
if (genScopeId && ast.hoists.length) {
ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
}
// generate import statements for helpers
// 生成 import 导入语句
if (ast.helpers.length) {
if (optimizeImports) {
// when bundled with webpack with code-split, calling an import binding
// as a function leads to it being wrapped with `Object(a.b)` or `(0,a.b)`,
// incurring both payload size increase and potential perf overhead.
// therefore we assign the imports to variables (which is a constant ~50b
// cost per-component instead of scaling with template size)
push(
`import { ${ast.helpers
.map(s => helperNameMap[s])
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
)
push(
`\n// Binding optimization for webpack code-split\nconst ${ast.helpers
.map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`)
.join(', ')}\n`
)
} else {
// 导入的变量需要重命名
push(
`import { ${ast.helpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
)
}
}
// 服务端渲染时的处理,生成 import 语句,通过 import 导入的变量要重命名
if (ast.ssrHelpers && ast.ssrHelpers.length) {
push(
`import { ${ast.ssrHelpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from "${ssrRuntimeModuleName}"\n`
)
}
if (ast.imports.length) {
// 生成 import 语句,import { ... } from 'xxx'
genImports(ast.imports, context)
newline()
}
genHoists(ast.hoists, context)
newline()
// 通过 export 的方式导出渲染函数
if (!inline) {
push(`export `)
}
}
可以看到,当 mode 的值为 module时,使用 ES module 标准的 import 语句来导入 ast 中的 helpers 辅助函数,并使用 export 将渲染函数作为默认导出。其生成的前置内容如下所示:
// mode === 'module' 生成的前置部分
'import { createVNode as _createVNode, resolveDirective as _resolveDirective } from "vue"
export '
genFunctionPreamble 函数
// packages/compiler-core/src/codegen.ts
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const {
ssr,
prefixIdentifiers,
push,
newline,
runtimeModuleName,
runtimeGlobalName,
ssrRuntimeModuleName
} = context
const VueBinding =
!__BROWSER__ && ssr
? `require(${JSON.stringify(runtimeModuleName)})`
: runtimeGlobalName
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
// Generate const declaration for helpers
// In prefix mode, we place the const declaration at top so it's done
// only once; But if we not prefixing, we place the declaration inside the
// with block so it doesn't incur the `in` check cost for every helper access.
// 通过
if (ast.helpers.length > 0) {
if (!__BROWSER__ && prefixIdentifiers) {
push(
`const { ${ast.helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`
)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = ${VueBinding}\n`)
// in "with" mode, helpers are declared inside the with block to avoid
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
if (ast.hoists.length) {
const staticHelpers = [ CREATE_VNODE, CREATE_ELEMENT_VNODE, CREATE_COMMENT, CREATE_TEXT, CREATE_STATIC ]
.filter(helper => ast.helpers.includes(helper))
.map(aliasHelper)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
}
}
}
// generate variables for ssr helpers
// 服务端导入模块,通过 require 的方式
if (!__BROWSER__ && ast.ssrHelpers && ast.ssrHelpers.length) {
// ssr guarantees prefixIdentifier: true
push(
`const { ${ast.ssrHelpers
.map(aliasHelper)
.join(', ')} } = require("${ssrRuntimeModuleName}")\n`
)
}
// 静态提升节点的生成
genHoists(ast.hoists, context)
// 换行
newline()
// 通过 return 的方式返回 渲染函数
push(`return `)
}
可以看到,当 mode 的值为 function 时,会生成一个单一的 const { helpers… } = Vue 声明,并且通过 return 的方式返回渲染函数。其生成的前置内容如下所示:
// mode === 'function' 生成的前置部分
'const { createVNode: _createVNode, resolveDirective: _resolveDirective } = Vue
return '
生成渲染函数的签名
接下来生成器开始生成渲染函数,如下面的代码所示;
// enter render function
// 如果是服务端渲染,渲染函数的名称是 ssrRender
// 如果是客户端渲染,渲染函数的名称是 render
const functionName = ssr ? `ssrRender` : `render`
// 渲染函数的参数的处理
const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
if (!__BROWSER__ && options.bindingMetadata && !options.inline) {
// binding optimization args
args.push('$props', '$setup', '$data', '$options')
}
// 函数签名,如果 TypeScript ,参数要标记为 any 类型
const signature =
!__BROWSER__ && options.isTS
// TS 语言,加上类型
? args.map(arg => `${arg}: any`).join(',')
// JS 语言
: args.join(', ')
// 使用 箭头函数还是 普通函数 来创建渲染函数
if (isSetupInlined) {
push(`(${signature}) => {`) // 使用箭头函数
} else {
push(`function ${functionName}(${signature}) {`) // 使用普通函数
}
// 缩进代码
indent()
// with() 函数处理
// with(obj)作用就是将后面的{}中的语句块中的缺省对象设置为obj
if (useWithBlock) {
push(`with (_ctx) {`)
// 缩进代码
indent()
// function mode const declarations should be inside with block
// also they should be renamed to avoid collision with user properties
// 在 function mode 中,const 生命应该在代码块中,
// 并且应该重命名结构的变量,防止变量名与用户的变量名冲突
// 解构变量
if (hasHelpers) {
push(
`const { ${ast.helpers
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')} } = _Vue`
)
push(`\n`)
// 换行
newline()
}
// 省略部分代码
// 扩展作用域结束
if (useWithBlock) {
deindent()
push(`}`)
}
}
- 首先是确定渲染函数的名称。如果是服务端渲染,渲染函数的名称是 ssrRender。如果是客户端渲染,则是 render。
- 然后是确定渲染函数的参数,根据是否是服务端渲染,参数会有所不同。函数签名部分会判断语言环境,如果是 TypeScript 环境的话,则需要给参数标记为 any 类型。
- 接下来通过 isSetupInlined 变量来判断是使用箭头函数还是普通函数来创建渲染函数。如果是普通函数,则通过 function 关键字来函数来声明函数。
- 在创建好函数签名后,在函数体内会判断是否需要使用 with() 函数来扩展作用域。如果此时有 helpers 辅助函数,则会将其结构在 with 的块级作用域内,结构后的变量会被重新命名,以防止与用户的变量名冲突。
资源的分解声明
在编译的过程,从 JavaScript AST 抽象语法树中解析出的 components 组件、directives 指令、filters 过滤器以及 temps 临时变量,会被生成器当作资源来处理。如下面的代码所示:
// generate asset resolution statements
// 如果 AST 中有组件,则解析组件
if (ast.components.length) {
genAssets(ast.components, 'component', context)
if (ast.directives.length || ast.temps > 0) {
newline()
}
}
// 如果 AST 中有指令,则解析指令
if (ast.directives.length) {
genAssets(ast.directives, 'directive', context)
if (ast.temps > 0) {
newline()
}
// 如果 AST 中有过滤器,则解析过滤器
if (__COMPAT__ && ast.filters && ast.filters.length) {
newline()
genAssets(ast.filters, 'filter', context)
newline()
}
// 如果有临时变量,则通过 let 声明
if (ast.temps > 0) {
push(`let `)
for (let i = 0; i < ast.temps; i++) {
push(`${i > 0 ? `, ` : ``}_temp${i}`)
}
}
if (ast.components.length || ast.directives.length || ast.temps) {
push(`\n`)
newline()
}
生成器会将 temps 临时变量生成使用 let 关键字声明的资源变量。将 components 组件、directives 指令、filters 过滤器这三种资源传入 genAssets 函数,生成对应的资源变量。
genAssets 函数
genAssets 函数的源码如下所示:
// packages/compiler-core/src/codegen.ts
function genAssets(
assets: string[],
type: 'component' | 'directive' | 'filter',
{ helper, push, newline, isTS }: CodegenContext
) {
// 通过资源的类型获取对应的资源解析器
const resolver = helper(
__COMPAT__ && type === 'filter'
? RESOLVE_FILTER
: type === 'component'
? RESOLVE_COMPONENT
: RESOLVE_DIRECTIVE
)
for (let i = 0; i < assets.length; i++) {
let id = assets[i]
// potential component implicit self-reference inferred from SFC filename
const maybeSelfReference = id.endsWith('__self')
if (maybeSelfReference) {
id = id.slice(0, -6)
}
// 解析对应的资源,将其拼接到代码字符串中
push(
`const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)}${
maybeSelfReference ? `, true` : ``
})${isTS ? `!` : ``}`
)
if (i < assets.length - 1) {
newline()
}
}
}
可以看到,genAssets 函数做的事情很简单,就是根据资源类型获取对应的resolve 函数,然后根据资源类型 + 资源ID 当作变量名,将资源ID传入该资源对应的 resolve 函数,并将解析后的结果赋值声明的变量,最后调用 push 函数完成字符串的拼接。
返回结果的方式
// generate the VNode tree expression
if (!ssr) {
push(`return `)
}
从上面的源码中可以看到,在渲染函数中,将通过 return 的方式返回节点转换成字符串后的代码。
生成节点代码字符串
接下来生成器要做的事情就是完成节点代码字符串的生成工作,如下面的源码所示:
// 生成 节点
if (ast.codegenNode) {
// genNode 函数用来完成代码生成的工作
genNode(ast.codegenNode, context)
} else {
push(`null`)
}
可以看到,当生成器判断 ast 中有 codegenNode 的节点属性后,会调用 genNode 函数来生成节点对应的代码字符串。
生成器的返回结果
return {
ast, // JavaScript AST
code: context.code, // 渲染函数的代码
preamble: isSetupInlined ? preambleContext.code : ``,
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined
}
在 generate 函数的最后,通过对象的形式返回了当前的 JavaScript AST 抽象语法树,经过生成器生成后的渲染函数的代码字符串及sourceMap 等。
上面我们介绍了生成器的执行流程,下面,我们来看看生成器是如何将 JavaScript AST 抽象语法树中的节点生成对应的代码字符串的。
genNode 生成节点代码字符串
在生成器的执行流程中,当生成器判断 ast 中有 codegenNode 的节点属性后,会调用 genNode 函数来生成节点对应的代码字符串。接下来,我们就来详细看下 genNode 函数。源码如下所示:
// packages/compiler-core/src/codegen.ts
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
// 如果当前节点是字符串,则直接将其拼接到字符字符串中
if (isString(node)) {
context.push(node)
return
}
// 如果 node 是 symbol 类型,则将其传入辅助函数helper中,再将其生成代码字符串
if (isSymbol(node)) {
context.push(context.helper(node))
return
}
// 判断节点类型,根据节点类型调用对应的生成函数
switch (node.type) {
case NodeTypes.ELEMENT:
case NodeTypes.IF:
case NodeTypes.FOR:
__DEV__ &&
assert(
node.codegenNode != null,
`Codegen node is missing for element/if/for node. ` +
`Apply appropriate transforms first.`
)
// 节点类型是 Element、if、for 类型,递归调用 genNode
// 继续生成这三种节点的子节点
genNode(node.codegenNode!, context)
break
// 文本类型
case NodeTypes.TEXT:
genText(node, context)
break
// 简单表达式类型
case NodeTypes.SIMPLE_EXPRESSION:
genExpression(node, context)
break
// 插值类型
case NodeTypes.INTERPOLATION:
genInterpolation(node, context)
break
// 文本调用类型
case NodeTypes.TEXT_CALL:
genNode(node.codegenNode, context)
break
//
case NodeTypes.COMPOUND_EXPRESSION:
genCompoundExpression(node, context)
break
case NodeTypes.COMMENT:
genComment(node, context)
break
case NodeTypes.VNODE_CALL:
genVNodeCall(node, context)
break
case NodeTypes.JS_CALL_EXPRESSION:
genCallExpression(node, context)
break
case NodeTypes.JS_OBJECT_EXPRESSION:
genObjectExpression(node, context)
break
case NodeTypes.JS_ARRAY_EXPRESSION:
genArrayExpression(node, context)
break
case NodeTypes.JS_FUNCTION_EXPRESSION:
genFunctionExpression(node, context)
break
case NodeTypes.JS_CONDITIONAL_EXPRESSION:
genConditionalExpression(node, context)
break
case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context)
break
case NodeTypes.JS_BLOCK_STATEMENT:
genNodeList(node.body, context, true, false)
break
// SSR only types
case NodeTypes.JS_TEMPLATE_LITERAL:
!__BROWSER__ && genTemplateLiteral(node, context)
break
case NodeTypes.JS_IF_STATEMENT:
!__BROWSER__ && genIfStatement(node, context)
break
case NodeTypes.JS_ASSIGNMENT_EXPRESSION:
!__BROWSER__ && genAssignmentExpression(node, context)
break
case NodeTypes.JS_SEQUENCE_EXPRESSION:
!__BROWSER__ && genSequenceExpression(node, context)
break
case NodeTypes.JS_RETURN_STATEMENT:
!__BROWSER__ && genReturnStatement(node, context)
break
/* istanbul ignore next */
case NodeTypes.IF_BRANCH:
// noop
break
default:
if (__DEV__) {
assert(false, `unhandled codegen node type: ${(node as any).type}`)
// make sure we exhaust all possible types
const exhaustiveCheck: never = node
return exhaustiveCheck
}
}
}
genNode 函数用来完成代码生成的工作。代码生成的原理很简单,只需要匹配各种类型的 JavaScript AST 节点,并调用对应的生成函数即可。
对于字符串或 symbol 类型的节点,会直接调用转换上下文对象context 的 push 方法将节点拼接进代码字符串中。
接着使用 switch 语句来匹配不同类型的节点,并调用与之对应的生成器函数。
当节点类型是 Element、IF、FOR 类型时,则递归调用 genNode 函数,继续去生成这三种类型节点的子节点,这样能够保证遍历的完整性。
当节点类型是文本类型时,则调用与其对应的 genText 函数,通过 JSON.stringify 将文本序列后后拼接进代码字符串中。genText 函数的源码如下所示:
// packages/compiler-core/src/codegen.ts
function genText(
node: TextNode | SimpleExpressionNode,
context: CodegenContext
) {
context.push(JSON.stringify(node.content), node)
}
当节点是一个简单表达式时,则调用 genExpression 函数,判断该表达式是否是静态的,如果是静态的,则通过 JSON.stringify 将表达式序列化后拼接进代码字符串中。否则直接拼接表达式对应的 content。genExpression 函数的源码如下所示:
// packages/compiler-core/src/codegen.ts
function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
const { content, isStatic } = node
context.push(isStatic ? JSON.stringify(content) : content, node)
}
当节点是一个插值时,则调用 genInterpolation 函数,在该函数中继续调用 genNode 函数来继续生成插值类型节点的子节点。genInterpolation 函数的源码如下所示:
// packages/compiler-core/src/codegen.ts
function genInterpolation(node: InterpolationNode, context: CodegenContext) {
const { push, helper, pure } = context
if (pure) push(PURE_ANNOTATION)
push(`${helper(TO_DISPLAY_STRING)}(`)
genNode(node.content, context)
push(`)`)
}
通过以上几种类型节点的分析,我们能知道生成器其实是根据不同节点的类型,调用与之对应的生成函数,通过调用转换上下文对象context 的 push 方法将节点字符串化后拼接到代码字符串中。对于存在子节点的节点,则调用 genNode 函数递归遍历,确保每个节点都能生成对应的代码字符串。
处理静态提升
生成器在生成前置内容时,无论是生成 mode 为 module 模式的前置内容还是生成 mode 为 function 模式的前置内容,在它们各自的生成函数中,都调用了 genHoists 函数来处理静态提升。下面我们就来看看生成器是怎么处理静态提升的。genHoists 函数的源码如下所示:
// packages/compiler-core/src/codegen.ts
function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) {
// 说明不存在需要静态提升的节点,那么直接返回,不做后续处理
if (!hoists.length) {
return
}
// 将上下文的 pure 设置为 true
context.pure = true
const { push, newline, helper, scopeId, mode } = context
const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function'
newline()
// generate inlined withScopeId helper
if (genScopeId) {
push(
`const _withScopeId = n => (${helper(
PUSH_SCOPE_ID
)}("${scopeId}"),n=n(),${helper(POP_SCOPE_ID)}(),n)`
)
newline()
}
// 遍历需要静态提升的节点,根据数组的index 生成静态提升的变量名
for (let i = 0; i < hoists.length; i++) {
const exp = hoists[i]
if (exp) {
const needScopeIdWrapper = genScopeId && exp.type === NodeTypes.VNODE_CALL
// 生成静态提升的变量名 const _hoisted_${i + 1}
push(
`const _hoisted_${i + 1} = ${
needScopeIdWrapper ? `${PURE_ANNOTATION} _withScopeId(() => ` : ``
}`
)
// 生成静态节点的代码字符串,赋值给静态提升的变量名 const _hoisted_${i + 1}
genNode(exp, context)
if (needScopeIdWrapper) {
push(`)`)
}
newline()
}
}
context.pure = false
}
- genHoists 函数接收两个参数,第一个参数是需要静态提升的节点集合hoists,第二个参数则是生成器上下文context。
- 在 genHoists 函数中,首先判断 hoists 数组是否有元素,如果没有,说明不存在需要静态提升的节点,那么直接返回,不做后续处理。
- 如果 hoists 数组有元素,说明存在需要静态提升的节点,那么将生成器上下文context的 pure 属性设置为true,然后从上下文context中取出相关的工具函数。
- 接着使用 forEach 遍历 hoists 数组,即遍历需要静态提升的节点,根据数组的 index 生成静态提升的变量名
_hoisted_${i + 1},然后调用 genNode 函数生成静态提升节点的代码字符串,并赋值给之前声明的变量名_hoisted_${i + 1}。 - 最后,在遍历完所有需要静态提升的节点后,经 context 的 pure 属性重置为 false 。而这里 pure 属性的作用,就是在某些节点类型生成字符串前,添加 /*#__PURE__*/ 注释前缀,表明该节点是静态节点。
总结
本文介绍了编译器编译过程中的代码生成器 generate 。代码生成本质上是字符串拼接的艺术,我们访问 JavaScript AST 中的节点,为每一种类型的节点生成相符的 JavaScript 代码。
接下来介绍了生成器的执行流程,首先是构建代码生成上下文,接下来是生成前置内容、生成渲染函数的签名、到返回结果的方式、再到生成节点代码字符串,到最后生成器的返回结果。
然后深入分析了生成器是如何根据不同节点的类型,调用与之对应的生成函数,将节点生成代码字符串后拼接到代码字符串中的。
最后介绍了生成器也是通过调用 genNode 函来处理需要静态提升的节点。