Vue.js 3.0 核心源码解析 - 前百度、滴滴资深技术专家 - 拉勾教育

上一节课我们分析了 AST 节点转换的过程,也知道了 AST 节点转换的作用是通过语法分析,创建了语义和信息更加丰富的代码生成节点 codegenNode,便于后续生成代码。

那么这一节课,我们就来分析整个编译的过程的最后一步——代码生成的实现原理。

同样的,代码生成阶段由于要处理的场景很多,所以代码也非常多而复杂。为了方便你理解它的核心流程,我们还是通过这个示例来演示整个代码生成的过程:

  1. <div class="app">
  2. <hello v-if="flag"></hello>
  3. <div v-else>
  4. <p>hello {{ msg + test }}</p>
  5. <p>static</p>
  6. <p>static</p>
  7. </div>
  8. </div>

代码生成的结果是和编译配置相关的,你可以打开官方提供的模板导出工具平台,点击右上角的 Options 修改编译配置。为了让你理解核心的流程,这里我只分析一种配置方案,当然当你理解整个编译核心流程后,也可以修改这些配置分析其他的分支逻辑。

我们分析的编译配置是:mode 为 module,prefixIdentifiers 开启,hoistStatic 开启,其他配置均不开启。

为了让你有个大致印象,我们先来看一下上述例子生成代码的结果:

  1. import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
  2. const _hoisted_1 = { class: "app" }
  3. const _hoisted_2 = { key: 1 }
  4. const _hoisted_3 = _createVNode("p", null, "static", -1 )
  5. const _hoisted_4 = _createVNode("p", null, "static", -1 )
  6. export function render(_ctx, _cache) {
  7. const _component_hello = _resolveComponent("hello")
  8. return (_openBlock(), _createBlock("div", _hoisted_1, [
  9. (_ctx.flag)
  10. ? _createVNode(_component_hello, { key: 0 })
  11. : (_openBlock(), _createBlock("div", _hoisted_2, [
  12. _createVNode("p", null, "hello " + _toDisplayString(_ctx.msg + _ctx.test), 1 ),
  13. _hoisted_3,
  14. _hoisted_4
  15. ]))
  16. ]))
  17. }

示例的模板是如何转换生成这样的代码的?在 AST 转换后,会执行 generate 函数生成代码:

  1. return generate(ast, extend({}, options, {
  2. prefixIdentifiers
  3. }))

generate 函数的输入就是转换后的 AST 根节点,我们看一下它的实现:

  1. function generate(ast, options = {}) {
  2. const context = createCodegenContext(ast, options);
  3. const { mode, push, prefixIdentifiers, indent, deindent, newline, scopeId, ssr } = context;
  4. const hasHelpers = ast.helpers.length > 0;
  5. const useWithBlock = !prefixIdentifiers && mode !== 'module';
  6. const genScopeId = scopeId != null && mode === 'module';
  7. if ( mode === 'module') {
  8. genModulePreamble(ast, context, genScopeId);
  9. }
  10. else {
  11. genFunctionPreamble(ast, context);
  12. }
  13. if (!ssr) {
  14. push(`function render(_ctx, _cache) {`);
  15. }
  16. else {
  17. push(`function ssrRender(_ctx, _push, _parent, _attrs) {`);
  18. }
  19. indent();
  20. if (useWithBlock) {
  21. push(`with (_ctx) {`);
  22. indent();
  23. if (hasHelpers) {
  24. push(`const { ${ast.helpers
  25. .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
  26. .join(', ')} } = _Vue`);
  27. push(`\n`);
  28. newline();
  29. }
  30. }
  31. if (ast.components.length) {
  32. genAssets(ast.components, 'component', context);
  33. if (ast.directives.length || ast.temps > 0) {
  34. newline();
  35. }
  36. }
  37. if (ast.directives.length) {
  38. genAssets(ast.directives, 'directive', context);
  39. if (ast.temps > 0) {
  40. newline();
  41. }
  42. }
  43. if (ast.temps > 0) {
  44. push(`let `);
  45. for (let i = 0; i < ast.temps; i++) {
  46. push(`${i > 0 ? `, ` : ``}_temp${i}`);
  47. }
  48. }
  49. if (ast.components.length || ast.directives.length || ast.temps) {
  50. push(`\n`);
  51. newline();
  52. }
  53. if (!ssr) {
  54. push(`return `);
  55. }
  56. if (ast.codegenNode) {
  57. genNode(ast.codegenNode, context);
  58. }
  59. else {
  60. push(`null`);
  61. }
  62. if (useWithBlock) {
  63. deindent();
  64. push(`}`);
  65. }
  66. deindent();
  67. push(`}`);
  68. return {
  69. ast,
  70. code: context.code,
  71. map: context.map ? context.map.toJSON() : undefined
  72. };
  73. }

generate 主要做五件事情:创建代码生成上下文,生成预设代码,生成渲染函数,生成资源声明代码,以及生成创建 VNode 树的表达式。接下来,我们就依次详细分析这几个流程。

创建代码生成上下文

首先,是通过执行 createCodegenContext 创建代码生成上下文,我们来看它的实现:

  1. function createCodegenContext(ast, { mode = 'function', prefixIdentifiers = mode === 'module', sourceMap = false, filename = `template.vue.html`, scopeId = null, optimizeBindings = false, runtimeGlobalName = `Vue`, runtimeModuleName = `vue`, ssr = false }) {
  2. const context = {
  3. mode,
  4. prefixIdentifiers,
  5. sourceMap,
  6. filename,
  7. scopeId,
  8. optimizeBindings,
  9. runtimeGlobalName,
  10. runtimeModuleName,
  11. ssr,
  12. source: ast.loc.source,
  13. code: ``,
  14. column: 1,
  15. line: 1,
  16. offset: 0,
  17. indentLevel: 0,
  18. pure: false,
  19. map: undefined,
  20. helper(key) {
  21. return `_${helperNameMap[key]}`
  22. },
  23. push(code) {
  24. context.code += code
  25. },
  26. indent() {
  27. newline(++context.indentLevel)
  28. },
  29. deindent(withoutNewLine = false) {
  30. if (withoutNewLine) {
  31. --context.indentLevel
  32. }
  33. else {
  34. newline(--context.indentLevel)
  35. }
  36. },
  37. newline() {
  38. newline(context.indentLevel)
  39. }
  40. }
  41. function newline(n) {
  42. context.push('\n' + ` `.repeat(n))
  43. }
  44. return context
  45. }

这个上下文对象 context 维护了 generate 过程的一些配置,比如 mode、prefixIdentifiers;也维护了 generate 过程的一些状态数据,比如当前生成的代码 code,当前生成代码的缩进 indentLevel 等。

此外,context 还包含了在 generate 过程中可能会调用的一些辅助函数,接下来我会介绍几个常用的方法,它们会在整个代码生成节点过程中经常被用到。

  • push(code),就是在当前的代码 context.code 后追加 code 来更新它的值。
  • indent(),它的作用就是增加代码的缩进,它会让上下文维护的代码缩进 context.indentLevel 加 1,内部会执行 newline 方法,添加一个换行符,以及两倍 indentLevel 对应的空格来表示缩进的长度。
  • deindent(),和 indent 相反,它会减少代码的缩进,让上下文维护的代码缩进 context.indentLevel 减 1,在内部会执行 newline 方法去添加一个换行符,并减少两倍 indentLevel 对应的空格的缩进长度。

上下文创建完毕后,接下来就到了真正的代码生成阶段,在分析的过程中我会结合示例讲解,让你更直观地理解整个代码的生成过程,我们先来看生成预设代码。

生成预设代码

因为 mode 是 module,所以会执行 genModulePreamble 生成预设代码,我们来看它的实现:

  1. function genModulePreamble(ast, context, genScopeId) {
  2. const { push, newline, optimizeBindings, runtimeModuleName } = context
  3. if (ast.helpers.length) {
  4. if (optimizeBindings) {
  5. push(`import { ${ast.helpers
  6. .map(s => helperNameMap[s])
  7. .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`)
  8. push(`\n
  9. .map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`)
  10. .join(', ')}\n`)
  11. }
  12. else {
  13. push(`import { ${ast.helpers
  14. .map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
  15. .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`)
  16. }
  17. }
  18. genHoists(ast.hoists, context)
  19. newline()
  20. push(`export `)
  21. }

下面我们结合前面的示例来分析这个过程,此时 genScopeId 为 false,所以相关逻辑我们可以不看。ast.helpers 是在 transform 阶段通过 context.helper 方法添加的,它的值如下:

  1. [
  2. Symbol(resolveComponent),
  3. Symbol(createVNode),
  4. Symbol(createCommentVNode),
  5. Symbol(toDisplayString),
  6. Symbol(openBlock),
  7. Symbol(createBlock)
  8. ]

ast.helpers 存储了 Symbol 对象的数组,我们可以从 helperNameMap 中找到每个 Symbol 对象对应的字符串,helperNameMap 的定义如下:

  1. const helperNameMap = {
  2. [FRAGMENT]: `Fragment`,
  3. [TELEPORT]: `Teleport`,
  4. [SUSPENSE]: `Suspense`,
  5. [KEEP_ALIVE]: `KeepAlive`,
  6. [BASE_TRANSITION]: `BaseTransition`,
  7. [OPEN_BLOCK]: `openBlock`,
  8. [CREATE_BLOCK]: `createBlock`,
  9. [CREATE_VNODE]: `createVNode`,
  10. [CREATE_COMMENT]: `createCommentVNode`,
  11. [CREATE_TEXT]: `createTextVNode`,
  12. [CREATE_STATIC]: `createStaticVNode`,
  13. [RESOLVE_COMPONENT]: `resolveComponent`,
  14. [RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
  15. [RESOLVE_DIRECTIVE]: `resolveDirective`,
  16. [WITH_DIRECTIVES]: `withDirectives`,
  17. [RENDER_LIST]: `renderList`,
  18. [RENDER_SLOT]: `renderSlot`,
  19. [CREATE_SLOTS]: `createSlots`,
  20. [TO_DISPLAY_STRING]: `toDisplayString`,
  21. [MERGE_PROPS]: `mergeProps`,
  22. [TO_HANDLERS]: `toHandlers`,
  23. [CAMELIZE]: `camelize`,
  24. [SET_BLOCK_TRACKING]: `setBlockTracking`,
  25. [PUSH_SCOPE_ID]: `pushScopeId`,
  26. [POP_SCOPE_ID]: `popScopeId`,
  27. [WITH_SCOPE_ID]: `withScopeId`,
  28. [WITH_CTX]: `withCtx`
  29. }

由于 optimizeBindings 是 false,所以会执行如下代码:

  1. push(`import { ${ast.helpers
  2. .map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
  3. .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`)
  4. }

最终会生成这些代码,并更新到 context.code 中:

  1. import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

通过生成的代码,我们可以直观地感受到,这里就是从 Vue 中引入了一些辅助方法,那么为什么需要引入这些辅助方法呢,这就和 Vue.js 3.0 的设计有关了。

在 Vue.js 2.x 中,创建 VNode 的方法比如 $createElement、_c 这些都是挂载在组件的实例上,在生成渲染函数的时候,直接从组件实例 vm 中访问这些方法即可。

而到了 Vue.js 3.0,创建 VNode 的方法 createVNode 是直接通过模块的方式导出,其它方法比如 resolveComponent、openBlock ,都是类似的,所以我们首先需要生成这些 import 声明的预设代码。

我们接着往下看,ssrHelpers 是 undefined,imports 的数组长度为空,genScopeId 为 false,所以这些内部逻辑都不会执行,接着执行 genHoists 生成静态提升的相关代码,我们来看它的实现:

  1. function genHoists(hoists, context) {
  2. if (!hoists.length) {
  3. return
  4. }
  5. context.pure = true
  6. const { push, newline } = context
  7. newline()
  8. hoists.forEach((exp, i) => {
  9. if (exp) {
  10. push(`const _hoisted_${i + 1} = `)
  11. genNode(exp, context)
  12. newline()
  13. }
  14. })
  15. context.pure = false
  16. }

首先通过执行 newline 生成一个空行,然后遍历 hoists 数组,生成静态提升变量定义的方法。此时 hoists 的值是这样的:

  1. [
  2. {
  3. "type": 15,
  4. "properties": [
  5. {
  6. "type": 16,
  7. "key": {
  8. "type": 4,
  9. "isConstant": false,
  10. "content": "class",
  11. "isStatic": true
  12. },
  13. "value": {
  14. "type": 4,
  15. "isConstant": false,
  16. "content": "app",
  17. "isStatic": true
  18. }
  19. }
  20. ]
  21. },
  22. {
  23. "type": 15,
  24. "properties": [
  25. {
  26. "type": 16,
  27. "key": {
  28. "type": 4,
  29. "isConstant": false,
  30. "content": "key",
  31. "isStatic": true
  32. },
  33. "value": {
  34. "type": 4,
  35. "isConstant": false,
  36. "content": "1",
  37. "isStatic": false
  38. }
  39. }
  40. ]
  41. },
  42. {
  43. "type": 13,
  44. "tag": "\"p\"",
  45. "children": {
  46. "type": 2,
  47. "content": "static"
  48. },
  49. "patchFlag": "-1 /* HOISTED */",
  50. "isBlock": false,
  51. "disableTracking": false
  52. },
  53. {
  54. "type": 13,
  55. "tag": "\"p\"",
  56. "children": {
  57. "type": 2,
  58. "content": "static",
  59. },
  60. "patchFlag": "-1 /* HOISTED */",
  61. "isBlock": false,
  62. "disableTracking": false,
  63. }
  64. ]

这里,hoists 数组的长度为 4,前两个都是 JavaScript 对象表达式节点,后两个是 VNodeCall 节点,通过 genNode 我们可以把这些节点生成对应的代码,这个方法我们后续会详细说明,这里先略过。

然后通过遍历 hoists 我们生成如下代码:

  1. import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
  2. const _hoisted_1 = { class: "app" }
  3. const _hoisted_2 = { key: 1 }
  4. const _hoisted_3 = _createVNode("p", null, "static", -1 )
  5. const _hoisted_4 = _createVNode("p", null, "static", -1 )

可以看到,除了从 Vue 中导入辅助方法,我们还创建了静态提升的变量。

我们回到 genModulePreamble,接着会执行newline()push(export),非常好理解,也就是添加了一个空行和 export 字符串。

至此,预设代码生成完毕,我们就得到了这些代码:

  1. import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
  2. const _hoisted_1 = { class: "app" }
  3. const _hoisted_2 = { key: 1 }
  4. const _hoisted_3 = _createVNode("p", null, "static", -1 )
  5. const _hoisted_4 = _createVNode("p", null, "static", -1 )
  6. export

生成渲染函数

接下来,就是生成渲染函数了,我们回到 generate 函数:

  1. if (!ssr) {
  2. push(`function render(_ctx, _cache) {`);
  3. }
  4. else {
  5. push(`function ssrRender(_ctx, _push, _parent, _attrs) {`);
  6. }
  7. indent();

由于 ssr 为 false, 所以生成如下代码:

  1. import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
  2. const _hoisted_1 = { class: "app" }
  3. const _hoisted_2 = { key: 1 }
  4. const _hoisted_3 = _createVNode("p", null, "static", -1 )
  5. const _hoisted_4 = _createVNode("p", null, "static", -1 )
  6. export function render(_ctx, _cache) {

注意,这里代码的最后一行有 2 个空格的缩进

另外,由于 useWithBlock 为 false,所以我们也不需生成 with 相关的代码。而且,这里我们创建了 render 的函数声明,接下来的代码都是在生成 render 的函数体。

生成资源声明代码

在 render 函数体的内部,我们首先要生成资源声明代码:

  1. if (ast.components.length) {
  2. genAssets(ast.components, 'component', context);
  3. if (ast.directives.length || ast.temps > 0) {
  4. newline();
  5. }
  6. }
  7. if (ast.directives.length) {
  8. genAssets(ast.directives, 'directive', context);
  9. if (ast.temps > 0) {
  10. newline();
  11. }
  12. }
  13. if (ast.temps > 0) {
  14. push(`let `);
  15. for (let i = 0; i < ast.temps; i++) {
  16. push(`${i > 0 ? `, ` : ``}_temp${i}`);
  17. }
  18. }

在我们的示例中,directives 数组长度为 0,temps 的值是 0,所以自定义指令和临时变量代码生成的相关逻辑跳过,而这里 components 的值是["hello"]

接着就通过 genAssets 去生成自定义组件声明代码,我们来看一下它的实现:

  1. function genAssets(assets, type, { helper, push, newline }) {
  2. const resolver = helper(type === 'component' ? RESOLVE_COMPONENT : RESOLVE_DIRECTIVE)
  3. for (let i = 0; i < assets.length; i++) {
  4. const id = assets[i]
  5. push(`const ${toValidAssetId(id, type)} = ${resolver}(${JSON.stringify(id)})`)
  6. if (i < assets.length - 1) {
  7. newline()
  8. }
  9. }
  10. }

这里的 helper 函数就是从前面提到的 helperNameMap 中查找对应的字符串,对于 component,返回的就是 resolveComponent。

接着会遍历 assets 数组,生成自定义组件声明代码,在这个过程中,它们会把变量通过 toValidAssetId 进行一层包装:

  1. function toValidAssetId(name, type) {
  2. return `_${type}_${name.replace(/[^\w]/g, '_')}`;
  3. }

比如 hello 组件,执行 toValidAssetId 就变成了 _component_hello。

因此对于我们的示例而言,genAssets 后生成的代码是这样的:

  1. import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
  2. const _hoisted_1 = { class: "app" }
  3. const _hoisted_2 = { key: 1 }
  4. const _hoisted_3 = _createVNode("p", null, "static", -1 )
  5. const _hoisted_4 = _createVNode("p", null, "static", -1 )
  6. export function render(_ctx, _cache) {
  7. const _component_hello = _resolveComponent("hello")

这很好理解,通过 resolveComponent,我们就可以解析到注册的自定义组件对象,然后在后面创建组件 vnode 的时候当做参数传入。

回到 generate 函数,接下来会执行如下代码:

  1. if (ast.components.length || ast.directives.length || ast.temps) {
  2. push(`\n`);
  3. newline();
  4. }
  5. if (!ssr) {
  6. push(`return `);
  7. }

这里是指,如果生成了资源声明代码,则在尾部添加一个换行符,然后再生成一个空行,并且如果不是 ssr,则再添加一个 return 字符串,此时得到的代码结果如下:

  1. import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
  2. const _hoisted_1 = { class: "app" }
  3. const _hoisted_2 = { key: 1 }
  4. const _hoisted_3 = _createVNode("p", null, "static", -1 )
  5. const _hoisted_4 = _createVNode("p", null, "static", -1 )
  6. export function render(_ctx, _cache) {
  7. const _component_hello = _resolveComponent("hello")
  8. return

好的,我们就先分析到这里,下节课继续来看生成创建 VNode 树的表达式的过程。

本节课的相关代码在源代码中的位置如下:
packages/compiler-core/src/codegen.ts