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.
🍏 🍎 🍐 🍊 🍋 🍌 🍉 🍇 🍓 🫐 🍈 🍒 🍑 🥭 🍍 🥥 🥝 🍅 🍆 🥑 🥦