vue 源码 complier 之 codegen | 青训营

108 阅读1分钟

什么是 codegen

codegen 用于将 codegenNode 的结构转换为真实的 js 代码,例如:

  • 这是我们写的 vue 代码:
 <script setup>
 import { ref } from 'vue'
 const msg = ref('Hello World!')
 </script>
 <template>
   <h1>{{ msg }}</h1>
 </template>
  • 这是 codegen 最后生成的代码:
 /* Analyzed bindings: {
   "ref": "setup-const",
   "msg": "setup-ref"
 } */
 import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
 import { ref } from 'vue'
 ​
 const __sfc__ = {
   __name: 'App',
   setup(__props) {
     const msg = ref('Hello World!')
     return (_ctx, _cache) => {
       return (_openBlock(), _createElementBlock("h1", null, _toDisplayString(msg.value), 1 /* TEXT */))
     }
   }
 }
 ​
 __sfc__.__file = "src/App.vue"
 export default __sfc__

vue 为了防止函数名冲突,在方法前加了 _,为了看的更加清晰,我们可以进一步简化:

 /* Analyzed bindings: {
   "ref": "setup-const",
   "msg": "setup-ref"
 } */
 import { toDisplayString, openBlock, createElementBlock } from "vue"
 import { ref } from 'vue'
 ​
 const sfc = {
   name: 'App',
   setup(props) {
     const msg = ref('Hello World!')
     return (ctx, cache) => {
       return (openBlock(), createElementBlock("h1", null, toDisplayString(msg.value), 1))
     }
   }
 }
 ​
 sfc.file = "src/App.vue"
 export default sfc

我们可以看到最后默认导出了 sfc,这个可以被 @vitejs/plugin-vue 插件识别,进一步生成可执行文件

原理

compile 文件中,使用 generate 作为入口函数,并将其返回结果返回

 export function baseCompile(template, options) {
   // ...
   return generate(ast)
 }

查看 generate 函数的内容:

 export function generate(ast, options = {}) {
   const context = createCodegenContext(ast, options);
   const { push, mode } = context;
   if (mode === "module") {
     genModulePreamble(ast, context);
   } else {
     genFunctionPreamble(ast, context);
   }
   const functionName = "render";
   const args = ["_ctx"];
 ​
   const signature = args.join(", ");
   push(`function ${functionName}(${signature}) {`);
   // 这里需要生成具体的代码内容
   // 开始生成 vnode tree 的表达式
   push("return ")
   genNode(ast.codegenNode, context)
   push("}")
   
   return {
     code: context.code,
   };
 }

createCodegenContext

createCodegenContext 函数用于生产上下文:

其返回值是一个对象,内容包括:

  • code:最终生成可执行的代码
  • helper:用于生成代码的辅助函数
  • push:向 code 添加内容
  • newline:为 code 换行
 function createCodegenContext(ast, {
   runtimeModuleName = 'vue',
   runtimeGlobalName = 'vue',
   mode = 'function'
 }) {
   const context = {
     code: '',
     mode,
     runtimeModuleName,
     runtimeGlobalName,
     helper(key) {
       return `_${helperNameMap[key]}`
     },
     push(code) {
       context.code += code
     },
     newline() {
       // TODO: 缩进处理
       context.code += '\n'
     }
   }
   return context
 }

genModulePreamble

genModulePreamble 就是添加 import 语句的,就是最开始我们看到的:

 import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

函数代码:

代码很简单,就是生成了一堆 import 字符串

 function genModulePreamble(ast, context) {
   const { push, newline, runtimeModuleName } = context
   if(ast.helpers.length) {
     const code = `
       import {${ast.helpers.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`).join(', ')}} from ${JSON.stringify(runtimeModuleName)}
     `
     push(code)
   }
   newline()
   push(`export `)
 }

先看一下 helperNameMap 是什么吧:

 export const helperNameMap = {
   [TO_DISPLAY_STRING]: "toDisplayString",
   [CREATE_ELEMENT_VNODE]: "createElementVNode"
 };

genFunctionPreamble

genModulePreamble 类似,genFunctionPreamble 是用于添加函数内容的

 function genFunctionPreamble(ast, context) {
   const { runtimeGlobalName, push, newline } = context
   const VueBinging = runtimeGlobalName
   const aliasHelper = (s) => `${helperNameMap[s]} : _${helperNameMap[s]}`
   if(ast.helpers.length > 0) {
     push(
       `
         const { ${ast.helpers.map(aliasHelper).join(", ")}} = ${VueBinging} 
 ​
       `
     );
   }
   newline()
   push(`return `)
 }

genModulePreamble 不同的是,genFunctionPreamble 是直接从 vue 示例中获取代码,效果其实差不多

添加 render 逻辑

最开始的代码并没有直接出现 render 函数,但他是 createElementBlack 等函数内部的基本方法

 return (_openBlock(), _createElementBlock("h1", null, _toDisplayString(msg.value), 1 /* TEXT */))
 export function generate(ast, options = {}) {
   // ...
   const functionName = 'render'
   const args = ['_ctx']
   const signature = args.join(', ')
   push(`function ${functionName}(${signature}) {`)
   // 开始生成具体的代码内容
   push('return')
   genNode(ast.codegenNode, context)
   push('}')
   return {
     code: context.code
   }
 }

genNode

genNode 代码是通过读取之前 transofrm 转换成的 node,基于不同类型的 node 来生成对应的代码块,最终将代码块拼凑在一起就能形成完整代码了:

 function genNode(node, context) {
   switch(node.type) {
     case NodeTypes.INTERPOLATION:
       genInterpolation(node, context)
       break
     case NodeTypes.SIMPLE_EXPRESSION:
       genExpression(node, context)
       break
     case NodeTypes.ELEMENT:
       genElement(node, context)
       break
     case NodeTypes.COMPOUND_EXPRESSION:
       genCompoundExpression(node, context)
       break
     case NodeTypes.TEXT:
       genText(node, context)
       break
     default:
       break
   }
 }

genInterpolation

用来解析插值语法:

 function genInterpolation(node, context) {
   const { push, helper } = context
   push(`${helper(TO_DISPLAYING_STRING)}(`)
   genNode(node.content, context)
   push(')')
 }

genNode 结构:

 codegenNode: {
   type: 4,
   tag: "'div'",
   props: null,
   children: {
     type: 2, // NodeTypes.INTERPOLATION
     content: {
       type: 3, // SIMP
       content: '_ctx.msg',
     },
   },
 },