In the first article of the Vue3 source code parsing series, I took you through the process of instantiating a Vue object. When we looked at the @vue/ Compiler-core module, the code generator — generate module appeared for the first time. To help you review, let’s take a look again at what happens during the compile process.

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
) :CodegenResult {
  const onError = options.onError || defaultOnError
  const isModuleMode = options.mode === 'module'
  
  constprefixIdentifiers = ! __BROWSER__ && (options.prefixIdentifiers ===true || isModuleMode)

  // Generate the AST abstract syntax tree
  const ast = isString(template) ? baseParse(template, options) : template
  const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
    prefixIdentifiers
  )
  // Perform transformations on the AST abstract syntax tree
  transform(
    ast,
    extend({}, options, {})
  )

  // Returns the code string generated by the code generator
  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}

Copy the code

In my simplified source code for compiling the module, you can see the abstract syntax tree that generated the AST mentioned in our previous articles and the comments for the transformation node of the node converter, but today we’ll talk about what the generate function does in the last line of code.

What is a code generator

What is a code generator? What does it do? Before answering these questions, we’ll start with the compilation process. At the end of the compilation process that generates a Vue object, we get a string variable called code from the compiled result. This variable is the code string we will mention throughout today, Vue will use this generated code string, with the Function constructor to generate the render Function, and finally use the generated render Function to complete the corresponding component rendering, in the source code is as follows.

function compileToFunction(
  template: string| HTMLElement, options? : CompilerOptions) :RenderFunction {
  const key = template
 // Executes the compile function and constructs the code string from the result
  const { code } = compile(
    template,
    extend(
      {
        hoistStatic: true.onError: __DEV__ ? onError : undefined.onWarn: __DEV__ ? e= > onError(e, true) : NOOP
      } as CompilerOptions,
      options
    )
  )

  // Use the Function constructor to generate the render Function
  const render = (__GLOBAL__
    ? new Function(code)()
    : new Function('Vue', code)(runtimeDom)) asRenderFunction ; (renderas InternalRenderFunction)._rc = true

  // Return the generated render function and cache it
  return (compileCache[key] = render)
}

Copy the code

So let’s dive right into the code generator module and see how generators work, starting with the generate function.

Code generation context

The generate function is located in packages/ Compiler-core/SRC /codegen.ts. Let’s look at its function signature first.

export function generate(ast: RootNode, options: CodegenOptions & { onContextCreated? : (context: CodegenContext) =>void
  } = {}
) :CodegenResult {
    const context = createCodegenContext(ast, options)
    /* Ignore subsequent logic */
}

export interface CodegenResult {
  code: string
  preamble: string
  ast: RootNode map? : RawSourceMap }Copy the code

Generate function, which takes two arguments, an AST abstract syntax tree processed by the converter, and options code to generate options. An object of type CodegenResult is returned.

You can see that CodegenResult contains the code string, the AST abstract syntax tree, the optional sourceMap, and the preamble part of the code string.

The first line of the generate function is to generate a context object. For better semantic understanding, we call this context the code generator context object, or generator context for short.

In the generator context, in addition to some properties, you’ll notice that it has five utility functions, focusing here on the push function.

If you haven’t seen the code before pushing, what’s there to say about a function that adds elements to an array? But this push is not that push, so let me show you the implementation of push.

push(code, node) {
  context.code += code
  if(! __BROWSER__ && context.map) {if (node) {
      let name
      /* Ignore logic */
      addMapping(node.loc.start, name)
    }
    advancePositionWithMutation(context, code)
    if(node && node.loc ! == locStub) { addMapping(node.loc.end) } } }Copy the code

After looking at the implementation of push above, you can see that push is not pushing elements into an array, but concatenating strings into the code property in the context. AddMapping is called to generate the corresponding sourceMap. This function is important because when the generator finishes processing each node in the AST tree, it calls push to concatenate the newly generated string into the previously generated code string. Eventually, you get the complete code string and return it as a result.

In addition to push, there are indent, deindent, and newline functions that handle the position of a string, indenting, shrinking, and inserting a newline, respectively. Is used to assist the generated code string, formatting structure used, so that the generated code string is very intuitive, just like in the IDE type.

And the context has more

Execute the process

Once the generator context is created, the generate function is then executed downwards, so I’ll read on and analyze the generator execution flow. All of the code I put in this section is generated inside the function, so for brevity, the function signature of Generate won’t be added again.

Code string precontent generation

const hasHelpers = ast.helpers.length > 0 // Helpers exist
constuseWithBlock = ! prefixIdentifiers && mode ! = ='module' // Use with to extend the scope
constgenScopeId = ! __BROWSER__ && scopeId ! =null && mode === 'module'

// Not in the browser environment and mode is module
if(! __BROWSER__ && mode ==='module') {
  // Use ES Module standard import to import helper functions to handle the pre-generated code
  genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
} else {
  // Otherwise the generated code is preceded by a single const {helpers... } = Vue handles the pre-code portion
  genFunctionPreamble(ast, preambleContext)
}
Copy the code

After the context is created and some objects are deconstructed from the context, the code string is generated in the beginning, and the key judgment is the mode attribute, which determines how to introduce the declaration of helpers function.

Mode has two options, ‘module’ or ‘function’. When the parameter is module, the helpers function in ast will be imported by ES Module import, and the render function will be exported by default. Const {helpers… Const {helpers………………….. } = Vue declaration, and return returns the render function instead of exporting it. In the code box below I have included the differences between the pre-code generated by the two modes.

Import {createVNode as _createVNode, ResolveDirective as _resolveDirective} from "vue" export '// mode === 'function' const {createVNode: _createVNode, resolveDirective: _resolveDirective } = Vue return 'Copy the code

Note that the above code is just the front part of the code, we have not started parsing other resources and nodes, so it stops abruptly when we reach export or return.

Now that we understand the difference in the front section, we move on to the code.

Generate the render function signature

The generator will then start generating the body of the render function, starting with the function name and passing arguments to the render function. When the function signature is determined, the generator uses with to extend the scope if mode is function, and the resulting look was shown in the first compilation flow.

The functionName and args parameter to be passed to the function are determined based on whether it is a server-side render, SSR marker, and the function signature section determines whether it is a TypeScript environment. The parameter is marked as type any.

It then decides whether to create the function through the arrow function or the function declaration.

After the function is created, the function body will determine whether it needs to use with to extend the scope, and if there is helpers function at this time, it will also deconstruct the variable in the block-level scope of with, and rename the variable after the deconstruction to prevent conflict with the variable name of the user.

The code logic is below.

// The generated function name
const functionName = ssr ? `ssrRender` : `render`
// Pass the parameter to the function
const args = ssr ? ['_ctx'.'_push'.'_parent'.'_attrs'] : ['_ctx'.'_cache']
/* Ignore logic */

// Function signatures, which are marked as any in TypeScript
constsignature = ! __BROWSER__ && options.isTS ? args.map(arg= > `${arg}: any`).join(', ')
    : args.join(', ')

/* Ignore logic */

// use arrow functions or function declarations to create render functions
if (isSetupInlined || genScopeId) {
  push(` (${signature}) = > {`)}else {
  push(`function ${functionName}(${signature}) {`)
}
indent()

// Use with to extend the scope
if (useWithBlock) {
  push(`with (_ctx) {`)
  indent()
  // In function mode, const should be declared in the code block,
  // Destruct variables should also be renamed to prevent conflicts between variable names and user variable names
  if (hasHelpers) {
    push(
      `const { ${ast.helpers
        .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
        .join(', ')} } = _Vue`
    )
    push(`\n`)
    newline()
  }
}

Copy the code

Decomposition declaration of a resource

Before we look at the subheading “Resource decomposition declaration,” we need to understand what the generator defines as a resource. The generator uses the COMPONENTS parses from the AST abstract syntax tree, directives, temps temp variables, and last month JUDAH incorporated Vue2 filter filters as resources in Vue3.

In the Render function, this part of the processing declares all of the above resources in advance, passes the resource ID parsed from the AST tree to the corresponding handler for each resource, and generates the corresponding resource variable.

// If there are components in the AST, parse them
if (ast.components.length) {
  genAssets(ast.components, 'component', context)
  if (ast.directives.length || ast.temps > 0) {
    newline()
  }
}
/* Omit directives and filters, logic consistent with component */
if (ast.temps > 0) {
  push(`let `)
  for (let i = 0; i < ast.temps; i++) {
    push(`${i > 0 ? `, ` : ` `}_temp${i}`) // Declare variables with let}}Copy the code

In the source code above, I put two typical representatives, Components and Temps. As an example, let me show you the result of generating code.

components: [`Foo`.`bar-baz`.`barbaz`.`Qux__self`].directives: [`my_dir_0`.`my_dir_1`].temps: 3
Copy the code

Suppose there are four components in the AST with ids Foo, bar-baz, barbaz, and Qux__self. Two instructions with ids my_dir_0 and my_DIR_1, and three temporary variables. These resources are parsed to generate the code strings shown below.

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
Copy the code

Instead of worrying about what the resolve function does, we just need to know what the code generator will generate.

So what the genAssets function does from the result is pass the resource ID as the variable name based on the resource type + resource ID, pass the resource ID to the resolve function corresponding to the type, and assign the result to the declared variable.

And temps processing in the upper source code has been written very clearly.

Returns the result

After generating the body of the Render function and processing the resources, the generator begins the crucial step of generating the code strings for the nodes, and returns the resulting results after processing all the nodes. Because of the importance of nodes, we have chosen to leave this section behind. At this point the code string is generated, and an object of type CodegenResult is eventually returned.

Generate the nodes

if (ast.codegenNode) {
  genNode(ast.codegenNode, context)
}
Copy the code

When the generator determines that the AST has a codegenNode node attribute, genNode is called to generate the code string corresponding to the node. Let’s take a closer look at the genNode function.

function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
  // If it is a string, push it directly into the code string
  if (isString(node)) {
    context.push(node)
    return
  }
  // If node is of type symbol, pass in the code string generated by the helper function
  if (isSymbol(node)) {
    context.push(context.helper(node))
    return
  }
  // Determine the node type
  switch (node.type) {
    case NodeTypes.ELEMENT:
    case NodeTypes.IF:
    caseNodeTypes.FOR: genNode(node.codegenNode! , context)break
    case NodeTypes.TEXT:
      genText(node, context)
      break
    case NodeTypes.SIMPLE_EXPRESSION:
      genExpression(node, context)
      break
    /* Ignore the remaining case branches */}}Copy the code

The genNode function determines the node type, concatenated directly into the code string for a string or symbol type, and then determines the node type through a switch-case conditional branch. As there are many judgment conditions, most of them will be ignored here and only a few typical types will be selected for analysis.

In the first case, genNode is recursively called when it encounters node types of Element, IF, or FOR, and continues to generate child nodes of these three node types to ensure the integrity of the traversal.

When the node is a text type, genText is called to concatenate the text directly into the code string via json.stringify serialization.

If the node is a simple expression, it will determine whether the expression is static. If it is static, the code string will be serialized through the JSON string, otherwise the corresponding content of the expression will be directly concatenated.

Through the analysis of these three nodes, we can see that the generator actually pushes in different code strings based on the type of nodes, and goes back recursively for nodes with child nodes to make sure that each node generates the corresponding code string.

Handling static lifting

When we look at the generator to generate the code precursors, we’ll see that the genModulePreamble or genFunctionPreamble functions are called, depending on the mode type. In both functions, there is the same line of code: genHoists(ast.hoists, context).

This function is used to handle static promotion. In the last article, I introduced static promotion and gave an example of how static nodes are extracted ahead of time to generate the corresponding serialized string. Today I’m going to pick up where I left off and explore how generators handle static promotion.

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
}
Copy the code

Here I put the genHoists code directly to generate static promotions, analyzing the logic step by step. First, the function takes an input of the Hoists property in the AST tree, which is an array of a collection of node types, and a generator context with two arguments.

If there are no elements in the Hoists array, there are no nodes that need to be promoted statically, so simply return.

Otherwise, there are nodes that need to be promoted, so set the pure tag of the context to true.

Then forEach iterates through the Hoists array and generates the static promoted variable name _HOisted_ ${index + 1} according to the index of the array. After that, genNode is called to generate the code string of the static promoted node. Assign to the previously declared variable _hoisted_${index + 1}.

After iterating through all the variables that need to be promoted, set the pure flag to false.

In this case, the pure tag prefixes the string generated by some node types with /*#__PURE__*/ comments to indicate that the node is static.

conclusion

In this article, I took you through the source code of the generator generate module, introduced the role of the generator, introduced the generator context, and explained the use of the tool function in the generator context. We then walk you through the entire process of executing Generate, from rendering the function’s precursors, generating the function signature, processing the resources in the AST, and finally returning the code string as a result. For those of you interested in the results, see the first article in this series. In that article there was an example of the render function finally returning. Finally, we focus on the generation of node genNode function, and in order to echo the previous article static promotion, and to explain the static node generation function, and the generation process.

If this article helps you understand the features of Vue3 a little more deeply, I hope to give this article a like ❤️. If you want to continue to follow the following articles, you can also follow my account, thank you again for reading so far.