The transform method further transforms the AST object as analyzed above. This example is used as the template to generate codegenNode attributes on the root node of Type =0 and the element node object of type=1. Generate a new property object; Merge two adjacent text child nodes; Generate the render function string (renderString) by collecting the methods needed to create a VNode and assigning them to the root node (e.g. createVNode, createTextVNode, etc.). This article looks at the internal implementation of generate

Generate function entry

After calling transform method to transform AST object, regression to baseCompile method

// baseCompile returns at the end of the method
return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
)
Copy the code

Call generate and pass in the AST object (a further converted template object) and the options object. Take a look at the concrete internal implementation of the Generate method

export function generate(ast: RootNode, options: CodegenOptions & { onContextCreated? : (context: CodegenContext) =>void
  } = {}
) :CodegenResult {
    const context = createCodegenContext(ast, options)
    // ...
    const {
        mode,
        push,
        prefixIdentifiers,
        indent,
        deindent,
        newline,
        scopeId,
        ssr
    } = context
    const hasHelpers = ast.helpers.length > 0 // true
    constuseWithBlock = ! prefixIdentifiers && mode ! = ='module' // true
    constgenScopeId = ! __BROWSER__ && scopeId ! =null && mode === 'module' // false
    constisSetupInlined = ! __BROWSER__ && !! options.inline// false
    
    const preambleContext = isSetupInlined ? createCodegenContext(ast, options) : context
    if(! __BROWSER__ && mode ==='module') {
        // ...
    } else {
        genFunctionPreamble(ast, preambleContext) // start the root function
    }
    // ...
}
Copy the code

The createCodegenContext function is first called to generate a context object (similar to the Context object in the Transform method, which also provides methods for renderString generation, for example: Newline, indent, string concatenation, etc., and provide a variable to store the renderString, the context’s code property, which will be parsed later when using specific methods), The genFunctionPreamble method is called to generate the renderString header based on the browser environment. The parameters are the AST and context objects

Generate the beginning of the renderString

function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
    const {
        ssr,
        prefixIdentifiers,
        push,
        newline,
        runtimeModuleName,
        runtimeGlobalName
    } = context
    constVueBinding = ! __BROWSER__ && ssr ?`require(The ${JSON.stringify(runtimeModuleName)}) ` : runtimeGlobalName
    const aliasHelper = (s: symbol) = > `${helperNameMap[s]}: _${helperNameMap[s]}`
    if (ast.helpers.length > 0) {
        if(! __BROWSER__ && prefixIdentifiers) {/ *... * /}
        else {
            push(`const _Vue = ${VueBinding}\n`)
            // ...
        }
    }
    genHoists(ast.hoists, context) // ast. Hoists is an empty array. This method returns directly
    newline()
    push(`return `)}// The implementation of the push method in context
push(code, node) {
    context.code += code
    if(! __BROWSER__ && context.map) {/ *... * /}}// The concrete implementation of the newline method in context
newline() {
    newline(context.indentLevel/* initializes to 0 */)}function newline(n: number) {
    context.push('\n' + ` `.repeat(n))
}
Copy the code

First VueBinding variables, the value is equal to the context. RuntimeGlobalName, initialize the value is when creating the context object ‘Vue’, and calls the context of the push method and parameters for the ‘const _Vue = Vue \ n’, You can see that the push method is just adding strings on the code property of the context. Then call the newline method of the context to add the string, and call the push method inside the newline method to add ‘\n’ + “.repeat(n)(newline + n Spaces) to the code. Finally, the push method is called to add the ‘return ‘string to code. So far, the context’s code property has the value:

// renderString
const _Vue = Vue
return 
Copy the code

The return function that generates renderString

Return to the generate method and parse the internal implementation of the genFunctionPreamble function

// Generate method after executing genFunctionPreamble
 const functionName = ssr ? `ssrRender` : `render`
 const args = ssr ? ['_ctx'.'_push'.'_parent'.'_attrs'] : ['_ctx'.'_cache']
 // ...
 constsignature = ! __BROWSER__ && options.isTS ? args.map(arg= > `${arg}: any`).join(', ') : args.join(', ') // args.join(', ')
 // ...
 if (isSetupInlined || genScopeId) {
   // ...
 } else {
   push(`function ${functionName}(${signature}) {`)
 }
 indent()
 // ...
 
// Indent method of context
indent() {
  newline(++context.indentLevel)
}
Copy the code

First, functionName is ‘render’ and args is [‘_ctx’, ‘_cache’] (not SSR). Join (‘, ‘), signature = ‘_ctx, _cache’, then execute push to add code string, then call context.indent, You can see that the indent method is implemented by calling newline and indenting ++context.indentLevel with a space (where 1 is passed). The code property now has the value:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 
Copy the code

Generates the body of the return function in renderString

The function body of the renderString return function is formed

// helperNameMap object key(Symbol type),value(Vue3 exposed method name to create the node)
export const helperNameMap: any = {
  [TO_DISPLAY_STRING]: `toDisplayString`,
  [CREATE_VNODE]: `createVNode`,
  [CREATE_TEXT]: `createTextVNode`,
  [FRAGMENT]: `Fragment`,
  [OPEN_BLOCK]: `openBlock`,
  [CREATE_BLOCK]: `createBlock`.// ...
}

// Generate generates the body of the return function in renderString
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
    if (hasHelpers) {
      push(
        `const { ${ast.helpers
          .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
          .join(', ')} } = _Vue`
      )
      push(`\n`)
      newline()
    }
}

// ... add components/directives/temps code
if(! ssr) { push(`return `)}if (ast.codegenNode) {
    genNode(ast.codegenNode, context)
} else {
    // ...
}
// ...
Copy the code

First call the method push(‘with (_ctx) {‘) to accumulate the code string, then indent to wrap and indent two Spaces. Helpers array ([Symbol(toDisplayString), Symbol(createVNode), Symbol(createTextVNode), Symbol(Fragment), Symbol(openBlock), Symbol(createBlock)]) through the map loop replaces the helperNameMap object with a new element [‘toDisplayString: _toDisplayString’, ‘createVNode: _createVNode’, ‘createTextVNode: _createTextVNode’, ‘Fragment: _Fragment’, ‘openBlock: _openBlock’, ‘createBlock: _createBlock’], then join(‘, ‘) to form a new string, add the push method to the code string, finally push(‘\n’) newline, newline() after indenting two Spaces. Then push(‘return ‘). The code string is as follows:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 with (_ctx) {
  const { toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
  return 
Copy the code

The genNode method resolves the codegenNode property of the root object

CodegenNode is called to parse the ast.codegenNode property

function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
  if (isString(node)) {
    context.push(node)
    return
  }
  if (isSymbol(node)) {
    context.push(context.helper(node))
    return
  }
  switch (node.type) {
      case NodeTypes.VNODE_CALL: / / 13
          genVNodeCall(node, context)
          break
      case NodeTypes.TEXT_CALL: / / 12
          genNode(node.codegenNode, context)
          break
      case NodeTypes.JS_CALL_EXPRESSION: / / 14
          genCallExpression(node, context)
          break
      case NodeTypes.COMPOUND_EXPRESSION: / / 8
          genCompoundExpression(node, context)
          break
      case NodeTypes.INTERPOLATION: / / 5
          genInterpolation(node, context)
          break
      case NodeTypes.TEXT: / / 2
          genText(node, context)
          break
      case NodeTypes.ELEMENT: / / 1
          // ...genNode(node.codegenNode! , context)break
      case NodeTypes.JS_OBJECT_EXPRESSION: / / 15
          genObjectExpression(node, context)
          break
      case NodeTypes.SIMPLE_EXPRESSION: / / 4
          genExpression(node, context)
          break}}Copy the code

First, codegenNode of type 0 is an object of type 13, so the execution of genVNodeCall is hit

function genVNodeCall(node: VNodeCall, context: CodegenContext) {
  const { push, helper, pure/* Create context to define -false */ } = context
  const {
    tag,
    props,
    children,
    patchFlag,
    dynamicProps,
    directives,
    isBlock, // codegenNode -true for type 13
    disableTracking // false
  } = node
  if (directives) {}
  if (isBlock) {
    push(` (${helper(OPEN_BLOCK)}(${disableTracking ? `true` : ` `}), `)}if (pure) {}
  push(helper(isBlock ? CREATE_BLOCK : CREATE_VNODE) + ` (`, node)
  genNodeList(
    genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
    context
  )
  push(`) `)
  if (isBlock) {
    push(`) `)}if (directives) {
    push(`, `)
    genNode(directives, context)
    push(`) `)}}// The context helper method defines the value of the - '_' + helperNameMap object mapping
helper(key) {
  return ` _${helperNameMap[key]}`
}
Copy the code

Push (‘(_openBlock(), ‘)(‘_openBlock’ is the return value of helper(OPEN_BLOCK)), then call context.push with ‘_createBlock(‘, code string:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 with (_ctx) {
  const { toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
  return (_openBlock(), _createBlock(
Copy the code

Then call the genNodeList method to parse the tag, props, children, patchFlag, and dynamicProps elements of node step by step and add the code string

[tag, props, children, patchFlag, dynamicProps] [tag, props, children, patchFlag, dynamicProps] = null (property values) is out of circulation, and then execute the args. Slice (0, + 1) I will finally there is no attributes are rejecting, perform the args. The map (arg = > arg | | ‘null’), will not exist among set to null. In other words, start from the last item in the array composed of all attributes, remove all non-existent attribute values, and set the non-existent intermediate attribute values to NULL. The first argument to genNodeList is [Symbol(Fragment), null, [{…}, {…}], 64]

The genNodeList method resolves the tag, children and other attributes in the codegenNode object of root in turn

The following details the internal implementation of the genNodeList function

function genNodeList(
  nodes: (string | symbol | CodegenNode | TemplateChildNode[])[],
  context: CodegenContext,
  multilines: boolean = false,
  comma: boolean = true
) {
  const { push, newline } = context
  for (let i = 0; i < nodes.length; i++) {
    const node = nodes[i]
    if (isString(node)) {
      push(node)
    } else if (isArray(node)) {
      genNodeListAsArray(node, context)
    } else {
      genNode(node, context)
    }
    if (i < nodes.length - 1) {
      if (multilines/* false */) {
        comma && push(', ')
        newline()
      } else {
        comma && push(', ')}}}}Copy the code

Inside this function, a for loop loops through an array of Nodes, matching criteria and parsing each element accordingly. This example is used as the template

The first tag is a Symbol(Fragment), so call the genNode method (described above), hit the Symbol determination, execute context.push(context.helper(node)), Add a “_Fragment” string to code, and then push(‘, ‘) to add a comma space (essentially separating the input arguments to the _createBlock method). The second is “null”, hit isString, is a string, so push(‘null’) directly, add null on code, then push(‘, ‘). The third is children, because children is an array, so we do the genNodeListAsArray method.

In this case, the code string is:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 with (_ctx) {
  const { toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
  return (_openBlock(), _createBlock(_Fragment, null.Copy the code

GenNodeListAsArray parses the children array element of root

Let’s take a closer look at the internal implementation of the genNodeListAsArray method

function genNodeListAsArray(nodes: (string | CodegenNode | TemplateChildNode[])[], context: CodegenContext) {
  const multilines =
    nodes.length > 3| | ((! __BROWSER__ || __DEV__) && nodes.some(n= >isArray(n) || ! isText(n))) context.push(` [`)
  multilines && context.indent()
  genNodeList(nodes, context, multilines)
  multilines && context.deindent()
  context.push(`] `)}Copy the code

Length > 3 or there is an element in children that is an array or not a text node. In this example, children of type=0 satisfies the condition, so multilines=true. We then execute context.push(‘[‘), code adds the ‘[‘ symbol, and indent() newline indentation.

GenNode resolves codegenNode for elements of type=12

Call the genNodeList method to parse each element of the Children array in turn. In this case, the first element of children of type=0 is a text node of type=12, which is an object so the genNode method is called. Returning to the genNode method, type=12 will continue to call the genNode method with the codegenNode attribute (in this case, the codegenNode of type=12 is the object of type=14), Type =14 hits the implementation of the genCallExpression method. Take a closer look at the internal implementation of the genCallExpression function

// genCallExpression method definition
function genCallExpression(node: CallExpression, context: CodegenContext) {
  const { push, helper, pure } = context
  const callee = isString(node.callee) ? node.callee : helper(node.callee)
  if (pure) {
    push(PURE_ANNOTATION)
  }
  push(callee + ` (`, node)
  genNodeList(node.arguments, context)
  push(`) `)}Copy the code

GenCallExpression resolves text nodes of type=14

In this case, it is Symbol(createTextVNode), so callee=_createTextVNode. Call push(callee + ‘(‘, node) to add the code string. The genNodeList method is then called to resolve the arguments attribute of the TEXT node (in this case, [{type: 8,…}, ‘1 /* TEXT */’]). In the genNodeList method, type=8 will hit genCompoundExpression. Take a look at the internal implementation of genCompoundExpression

// genCompoundExpression Method definition
function genCompoundExpression(node: CompoundExpressionNode, context: CodegenContext) {
  for (let i = 0; i < node.children! .length; i++) {constchild = node.children! [i]if (isString(child)) {
      context.push(child)
    } else {
      genNode(child, context)
    }
  }
}
Copy the code

The method internally loops through the array of Node.children passed in. If it is a string, push is called for string summation, otherwise genNode is parsed. In accordance with the children in this analytical objects of type = 8 for [} {type: 5,…, ‘+’ {} type: 2,…]. First objects of type=5 will hit the genInterpolation method

GenInterpolation parsing Type =5 Dynamic data node

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

The genInterpolation method first pushes the cumulated string “_toDisplayString(” and then calls the genNode function to parse the content property of type=5. In this case, it’s a “message” string, so push(‘messgae’), Finally push(‘)’) adds the closing parenthesis. Returning to the genCompoundExpression method, the second argument to children is the string ‘+’, so it adds directly.

GenText parses plain text nodes of type=2

The third argument is an object of type=2 that hits the genText method. The method internally executes context.push(json.stringify (Node.content), node), adding the value of the content property to the code string (in this case, the content of the type=2 object is “space”).

Going back to the genNodeList function implemented in the genCallExpression method, objects of type=8 perform push(‘, ‘) after execution (for reasons explained in detail above). The second element of the arguments array, “1 /* TEXT */”, is added as a string. Back to the genCallExpression method, which finally performs push(‘)’).

Return to genNodeListAsArray (parse the children array of codegenNode object of type=0) and use comma && push(‘,’). Newline ()(because multilines is true when genNodeListAsArray is called from genNodeList, as explained in detail above) adds “,” and newline indentation. In this case, the code string is:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 with (_ctx) {
  const { toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
  return (_openBlock(), _createBlock(_Fragment, null, [
   _createTextVNode(_toDisplayString(message) + "".1 /* TEXT */),
   
Copy the code

GenNode resolves codegenNode of type=1

Then start parsing objects of type=1. The genNode method is called recursively with node.codegenNode(type=13 object). Hit the genVNodeCall function (which also hits when parsing the codegenNode attribute of type=0). Since isBlock=false, push(‘_createVNode(‘, node) is performed. Tag, props, children, patchFlag, and dynamicProps are the same as before when type=0.

1, the first is the tag, this case is “\” button “\” string, so direct push (” \ “button” \ “), and then push (‘, ‘)

GenObjectExpression Parsing props property

2, then the props, because the button tag exists in the click event, so there is an object (type = 15). Hit the genObjectExpression function. Take a look at its internal implementation

// genObjectExpression Function definition
function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
  const { push, indent, deindent, newline } = context
  const { properties } = node
  if(! properties.length) {/ *... * /}
  const multilines /* false */ =
    properties.length > 1| | ((! __BROWSER__ || __DEV__) && properties.some(p= >p.value.type ! == NodeTypes.SIMPLE_EXPRESSION)) push(multilines ?` {` : `{ `)
  multilines/* false */ && indent()
  for (let i = 0; i < properties.length; i++) {
    const { key, value } = properties[i]
    // key
    genExpressionAsPropertyKey(key, context)
    push(` : `)
    // value
    genNode(value, context)
    if (i < properties.length - 1) {
      // will only reach this if it's multilines
      push(`, `)
      newline()
    }
  }
  multilines && deindent()
  push(multilines ? `} ` : ` }`)}/ / genExpressionAsPropertyKey definition
function genExpressionAsPropertyKey(node: ExpressionNode, context: CodegenContext) {
  const { push } = context
  if (node.type === NodeTypes.COMPOUND_EXPRESSION) {/ *... * /} 
  else if (node.isStatic) {
    // only quote keys if necessary
    const text = isSimpleIdentifier(node.content)
      ? node.content
      : JSON.stringify(node.content)
    push(text, node)
  } else {/ *... * /}}// isSimpleIdentifier
const nonIdentifierRE = /^\d|[^\$\w]/
export const isSimpleIdentifier = (name: string): boolean= >! nonIdentifierRE.test(name)Copy the code

Multilines =false(props = properties array length >1, value. Type! Lambda is equal to lambda is equal to 4. This example does not, so false), and push(‘{‘). The for loop through the Properties array, parsing each property (in this case, there is only one click method). Push the key value of each attribute object call genExpressionAsPropertyKey method (text node) (here the value of the text is in the node. The content made a layer optimization based on judgment, concrete is to use regular judge whether simple expressions, If not, call json.stringify to convert Node.content to a string)

Return to the genObjectExpression method, execute push(‘: ‘) after processing the key value of the attribute, then call genNode(value, context) method to process the value(in this case, the object of type=4), and match the genExpression method

// Internal definition of genExpression method
function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
  const { content, isStatic } = node
  context.push(isStatic ? JSON.stringify(content) : content, node)
}
Copy the code

Inside the genExpression method is the summation of the code string by calling context.push. The parameter is determined by the isStatic property. In this case, isStatic is false, so it is the Content (modifyMessage) string. Return to the genObjectExpression method and, after processing the value, determine that I < properties.lengs-1 (because there is only one attribute in this case) is not valid, and finally execute push(‘}’). Go back to the genNodeList method and do push(‘, ‘).

In this case, the child element of the Button element is an object of type=2, so execute the genNode method and hit genText function, which has been parsed before, adding the content property to the code string (in this case, “modify data” string). Go back to the genNodeList method and perform push(‘, ‘).

/* PROPS */”, push(‘, ‘), push(‘, ‘)

[\”onClick\”]” is still a string, so call push

The genNodeList method has been executed (tag, props, children, patchFlag, and dynamicProps properties have been resolved). Go back to the genVNodeCall method and perform push(‘)’). The children array of type=0 has been parsed back to the genNodeListAsArray function. Then call multilines && Context.deindent ()

// context.deindent method definition
deindent(withoutNewLine = false) {
  if (withoutNewLine) {
    --context.indentLevel
  } else {
    newline(--context.indentLevel)
  }
}
Copy the code

The deindent method basically does what it does by calling the push method to accumulate the string (\n + context.indentLevel-1 space), which is newline and indent n-1 Spaces. Finally execute context.push(‘]’) and add the end “]”. In this case, the code string is:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 with (_ctx) {
  const { toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
  return (_openBlock(), _createBlock(_Fragment, null, [
   _createTextVNode(_toDisplayString(message) + "".1 /* TEXT */),
   _createVNode("button", { onClick: modifyMessage }, "Modify data"."8 /* PROPS */"["onClick"]]Copy the code

After parsing the children property of type=0 (genNodeListAsArray), execute the push(‘, ‘) method. Then the patchFlag attribute of type=0 is parsed. In this case, it is the string “64 /* STABLE_FRAGMENT */”, so push directly adds the string. The tag, props, children, and patchFlag properties of type=0 have been resolved. Return to genVNodeCall and execute push(‘)’). At this point the codegenNode attribute of type=0 has been resolved. Return to the generate method

// Return portion of generate method
if (useWithBlock) {
    deindent()
    push(`} `)
}

deindent()
push(`} `)
// ...
return {
    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
}
Copy the code

Finally, because useWithBlock=true in this example, we call deindent to indent (– context.indentlevel space), push(‘}’) to add ‘}’, Call deindent() and push(‘}’) and add ‘}’. Generate returns an object {ast, code: concatenated renderString string,… }. The renderString generated for this template is as follows:

// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
 with (_ctx) {
  const { toDisplayString: _toDisplayString, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue
  return (_openBlock(), _createBlock(_Fragment, null, [
   _createTextVNode(_toDisplayString(message) + "".1 /* TEXT */),
   _createVNode("button", { onClick: modifyMessage }, "Modify data"."8 /* PROPS */"["onClick"]]),"64 /* STABLE_FRAGMENT */")}}Copy the code

conclusion

The whole method of generate is similar to the onion model. At the beginning, renderString (variable definition) was splicing together the beginning of renderString (variable definition) by using the running environment, the helpers array generated by transform parsing, etc. The codegenNode property of the root object is then parsed, followed by tag, props, children, and renderString concatenation. When parsed into the children array, each element of the array is parsed in turn, mainly by hitting different object resolution methods with switch-case in the genNode method (e.g. Static text, dynamic data, element node, etc.). When the child node is fully parsed, the upper node is returned to continue parsing patchFlag(patch identifier, which is needed when DOM generation is called by patch method in the future) and dynamicProps(dynamic properties) until the root object is fully parsed. So the context.code string is the generated renderString that returns {ast, code,… } object. The Render method in renderString is then executed to generate the VNode object, and the Patch method is called to generate the actual DOM node from the VNode.

🍏 🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🫐 🍈 🍒 🍑 🥭 🍍 🥥 🥝 🍅 🍆 🥑 🥦