上文解析了transform方法对ast对象进一步转化,依本例为模版,在type=0的根节点和type=1的元素节点对象上生成codegenNode属性。生成新的属性对象;合并相邻两个文本子节点;将创建VNode需要的方法收集起来赋值到root根节点上(例如:createVNode、createTextVNode等),为generate方法生成render函数字符串(下文简称为renderString)做准备。本文来看下generate函数的具体内部实现
generate函数入口
在调用完transform方法转化ast对象之后,回归到baseCompile方法中
// baseCompile 方法最后return部分
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
调用generate函数,传参ast对象(进一步转化后的模版对象)、options对象。看下generate方法的具体内部实现
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
const useWithBlock = !prefixIdentifiers && mode !== 'module' // true
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module' // false
const isSetupInlined = !__BROWSER__ && !!options.inline // false
const preambleContext = isSetupInlined ? createCodegenContext(ast, options) : context
if (!__BROWSER__ && mode === 'module') {
// ...
} else {
genFunctionPreamble(ast, preambleContext) // 根函数开头
}
// ...
}
首先调用createCodegenContext函数生成一个context对象(类似于transform方法中的context对象,也是提供了一些方法,主要是服务于renderString的生成,例如:换行、缩进、字符串拼接等等,同时提供一个变量用于储存renderString,就是context的code属性,后续使用到具体方法时再详细解析),再根据所运行浏览器环境调用genFunctionPreamble方法生成renderString的头部,传参是ast和context对象,下面解析下genFunctionPreamble函数的内部实现
生成renderString开头部分
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const {
ssr,
prefixIdentifiers,
push,
newline,
runtimeModuleName,
runtimeGlobalName
} = context
const VueBinding = !__BROWSER__ && ssr ? `require(${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为空数组,此方法直接return
newline()
push(`return `)
}
// context对象中push方法的具体实现
push(code, node) {
context.code += code
if (!__BROWSER__ && context.map) {/* ... */}
}
// context对象中newline方法的具体实现
newline() {
newline(context.indentLevel/* 初始化为0 */)
}
function newline(n: number) {
context.push('\n' + ` `.repeat(n))
}
首先定义VueBinding变量,值等于context.runtimeGlobalName,在创建context对象时初始化该值为'Vue',再调用context的push方法,参数为'const _Vue = Vue\n',可以看到push方法就是在context的code属性上进行字符串的累加。之后调用context的newline方法进行字符串的添加,newline方法内部也是调用push方法,将'\n' + ' '.repeat(n)(换行符加n个空格) 累加到code中,最后调用push方法将'return '字符串累加到code中。至此,context的code属性的值为:
// renderString
const _Vue = Vue
return
生成renderString的return函数
回归到generate方法中,解析genFunctionPreamble函数后的内部实现
// generate方法中执行完genFunctionPreamble的后续
const functionName = ssr ? `ssrRender` : `render`
const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
// ...
const signature = !__BROWSER__ && options.isTS ? args.map(arg => `${arg}: any`).join(',') : args.join(', ') // args.join(', ')
// ...
if (isSetupInlined || genScopeId) {
// ...
} else {
push(`function ${functionName}(${signature}) {`)
}
indent()
// ...
// context的 indent方法
indent() {
newline(++context.indentLevel)
}
首先functionName为'render'、args为['_ctx', '_cache'] (非ssr)。signature为args.join(', '),所以signature为'_ctx, _cache',之后执行push方法累加code字符串,然后调用context.indent方法,可以看到indent方法的实现就是调用newline方法换行并缩进++context.indentLevel个空格(此处传参1), 现在code属性的值为:
// renderString
const _Vue = Vue
return function render(_ctx, _cache) {
生成renderString中return函数的函数体
后续解析下renderString中return函数的函数体内容形成
// helperNameMap对象key(Symbol类型),value(Vue3暴露的创建节点的方法名称)
export const helperNameMap: any = {
[TO_DISPLAY_STRING]: `toDisplayString`,
[CREATE_VNODE]: `createVNode`,
[CREATE_TEXT]: `createTextVNode`,
[FRAGMENT]: `Fragment`,
[OPEN_BLOCK]: `openBlock`,
[CREATE_BLOCK]: `createBlock`,
// ...
}
// generate方法中生成renderString中return函数的函数体
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 {
// ...
}
// ...
首先调push('with (_ctx) {')方法累计code字符串,然后indent换行并缩进两个空格。然后将ast.helpers数组([Symbol(toDisplayString), Symbol(createVNode), Symbol(createTextVNode), Symbol(Fragment), Symbol(openBlock), Symbol(createBlock)])通过map循环,根据helperNameMap对象的映射关系替换成新的元素['toDisplayString: _toDisplayString', 'createVNode: _createVNode', 'createTextVNode: _createTextVNode', 'Fragment: _Fragment', 'openBlock: _openBlock', 'createBlock: _createBlock'],然后通过join(', ')构成新的字符串,调push方法累加到code字符串中,最后push('\n')换行,调newline()换行之后缩进两个空格。然后push('return ')。此时code字符串如下:
// 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
genNode方法解析root对象codegenNode属性
那么接下来就是拼接renderString返回函数的内部返回内容(就是上面js代码中render方法的返回值),调用genNode方法开始解析ast.codegenNode属性,下面详细解析下genNode方法的内部实现
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
}
}
首先type为0的codegenNode是type为13的对象,所以命中genVNodeCall函数的执行
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const { push, helper, pure/* 创建context时定义 - false */ } = context
const {
tag,
props,
children,
patchFlag,
dynamicProps,
directives,
isBlock, // type为13的codegenNode - true
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(`)`)
}
}
// context的helper方法定义 - '_' + helperNameMap对象映射的value值
helper(key) {
return `_${helperNameMap[key]}`
}
首先执行push('(_openBlock(), ')('_openBlock'是helper(OPEN_BLOCK)的返回值),再调用context.push方法,参数为'_createBlock(',此时code字符串为:
// 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(
然后调用genNodeList方法对node的tag、props、children、patchFlag、dynamicProps元素逐步解析并累加code字符串
注意: 这里调用了
genNullableArgs方法对上面提到的tag等属性优化,大致实现是:[tag、props、children、patchFlag、dynamicProps]这个数组从最后一项开始循环判断,遇到第一个!=null(属性值存在)的则跳出循环,然后执行args.slice(0, i+1)将最后不存在的属性都剔除,再执行args.map(arg => arg || 'null'),将中间不存在的置为null。换言之将这个由各属性构成的数组从最后一项开始,将属性值不存在的都剔除,中间属性值不存在置为null。依本例解析传入genNodeList方法的第一个参数是[Symbol(Fragment), null, [{...}, {...}], 64]
genNodeList方法依次解析root的codegenNode对象中tag、children等属性
下面详细解析下genNodeList函数的内部实现
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(', ')
}
}
}
}
该函数内部是一个for循环贯穿,循环传入的nodes数组,依次匹配判断条件对每个元素做出相应的解析。依本例为模版
第一个
tag是Symbol(Fragment),因此调用genNode方法(此方法在上面有介绍),命中是Symbol类型的判断,执行context.push(context.helper(node)),向code中添加"_Fragment"字符串,然后执行push(', ')添加逗号空格(本质就是分隔_createBlock方法的入参)。
第二个是"null",命中isString,是字符串,所以直接push('null'),在code上累加null,后续push(', ')。
第三个是children,因为children是数组,所以执行genNodeListAsArray方法。
此时code字符串为:
// 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,
genNodeListAsArray解析root的children数组元素
接下来详细解析下genNodeListAsArray方法的内部实现
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(`]`)
}
该方法内部首先判断multilines的值,判断条件为children.length > 3 或者 children中存在是数组或者不是文本节点的元素,本例中type=0的children满足条件,所以multilines=true。然后执行context.push('['),code添加'['符号,再执行indent()方法换行缩进。
genNode解析type=12子元素的codegenNode
调用genNodeList方法依次解析children数组的每个元素。本例中type=0的children的第一个元素是type=12的文本节点,它是个对象所以调用genNode方法。回归到genNode方法中,type=12会继续调用genNode方法,参数是codegenNode属性(本例中type=12的codegenNode是type=14的对象),type=14会命中genCallExpression方法的执行。详细看下genCallExpression函数的内部实现
// genCallExpression 方法定义
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(`)`)
}
genCallExpression解析type=14的文本节点
该方法内部首先判断callee属性是否是字符串,本例为Symbol(createTextVNode),所以callee=_createTextVNode,调用push(callee + '(', node)方法,累加code字符串。之后调用genNodeList方法解析文本节点的arguments属性(本例中为[{type: 8, ...}, '1 /* TEXT */'])。在genNodeList方法中type=8会命中genCompoundExpression函数,看下genCompoundExpression函数的内部实现
// genCompoundExpression方法定义
function genCompoundExpression(
node: CompoundExpressionNode,
context: CodegenContext
) {
for (let i = 0; i < node.children!.length; i++) {
const child = node.children![i]
if (isString(child)) {
context.push(child)
} else {
genNode(child, context)
}
}
}
该方法内部会循环传入的node.children数组,如果是字符串则直接调用push方法进行字符串累加,否则调用genNode方法依次解析。依本例解析type=8的对象中children为[{type:5,...}, ' + ', {type:2,...}]。首先type=5的对象会命中genInterpolation方法
genInterpolation解析type=5动态数据节点
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(`)`)
}
在genInterpolation方法中首先push累计字符串"_toDisplayString(",然后调用genNode函数解析type=5的content属性,本例为"message"字符串,所以直接push('messgae'),最后push(')')添加右括号。回归到genCompoundExpression方法中,children的第二个参数是字符串' + ',所以直接累加。
genText解析type=2纯文本节点
第三个参数是type=2的对象,命中genText方法。该方法内部执行context.push(JSON.stringify(node.content), node),将content属性值累加进code字符串中(本例type=2对象的content为' '空格)。
回归到genCallExpression方法中执行的genNodeList函数,type=8的对象执行完后执行push(', ')(原因上面已经详细解析了)。然后解析arguments数组的第二个元素"1 /* TEXT */",因为是字符串所以直接累加。回归到genCallExpression方法中,最后执行push(')')。
回归到genNodeListAsArray方法中执行的genNodeList内部(解析的是type=0的codegenNode对象的children数组),此时type=12的已经解析完成了,执行comma && push(',');newline()(因为在genNodeListAsArray方法中调用genNodeList时,multilines为true,上述中已详细解析)添加",",并且换行缩进。此时code字符串为:
// 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 */),
genNode解析type=1的codegenNode
之后开始解析type=1的对象。依然是执行genNode方法,此时会递归调用genNode方法,参数为node.codegenNode(type=13对象)。命中genVNodeCall函数(此函数在解析type=0的codegenNode属性时也会命中)。因为isBlock=false,所以执行push('_createVNode(', node)。之后跟上述解析type=0时相同,依次解析tag、props、children、patchFlag、dynamicProps。
1、首先是tag,本例为"\"button"\"字符串,所以直接push("\"button"\"),然后push(', ')
genObjectExpression解析props属性
2、再是props,因为button标签中存在click事件,所以是存在的(type=15的对象)。命中genObjectExpression函数。看下它的内部实现
// genObjectExpression函数定义
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定义
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)
genObjectExpression方法内部首先判断multilines=false(判断props中properties数组的长度是否>1,或者是否存在value.type !== 4的元素。本例不符合,所以为false),然后push('{ ')。再for循环properties数组,解析每一个属性(本例中只有一个click方法)。每个属性的key值对象调用genExpressionAsPropertyKey方法push(text, node)(这里text的值是在node.content基础上做了一层优化判断,具体是利用正则判断是否是简单表达式,不是则调用JSON.stringify将node.content转成字符串)
回归到genObjectExpression方法中,处理完属性的key值后执行push(': '),再调用genNode(value, context)方法处理value(本例为type=4的对象),命中genExpression方法
// genExpression方法的内部定义
function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
const { content, isStatic } = node
context.push(isStatic ? JSON.stringify(content) : content, node)
}
genExpression方法内部就是调用context.push方法进行code字符串的累加,参数由isStatic属性决定,本例中isStatic为false,所以就是content(modifyMessage)字符串。回归到genObjectExpression方法中,处理完value之后,判断i < properties.length - 1(因为本例中只有一个属性)不成立,最后执行push(' }')。回到genNodeList方法中,执行push(', ')。
3、其次解析children,本例中button元素的子元素是type=2的对象,所以执行genNode方法,并命中genText函数,这个函数之前已经解析过了,就是将content属性累加到code字符串(本例为"修改数据"字符串),回归到genNodeList方法中,执行push(', ')。
4、再次解析patchFlag,本例为"8 /* PROPS */"字符串,所以直接push累加,然后push(', ')
5、最后解析dynamicProps属性,本例"[\"onClick\"]"仍然是字符串,所以调用push方法直接累加
至此genNodeList方法已经执行完成(tag、props、children、patchFlag、dynamicProps属性已经解析结束了)。回归到genVNodeCall方法中执行push(')')。回归到genNodeListAsArray函数中type=0的children数组已经解析完成了。之后调用multilines && context.deindent()
// context.deindent方法定义
deindent(withoutNewLine = false) {
if (withoutNewLine) {
--context.indentLevel
} else {
newline(--context.indentLevel)
}
}
deindent方法的大致作用就是调用push方法累加字符串(\n + context.indentLevel-1个空格),就是换行并缩进n-1个空格。最后执行context.push(']'),添加结尾"]"。此时code字符串为:
// 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 }, "修改数据", 8 /* PROPS */, ["onClick"])
]
解析完type=0的children属性后(genNodeListAsArray函数执行),执行push(', ')方法。然后解析type=0的patchFlag属性,本例为"64 /* STABLE_FRAGMENT */"字符串,所以push直接累加字符串。至此type=0的tag, props, children, patchFlag属性已经解析完成,回归到genVNodeCall方法中,执行push(')')。此时type=0的codegenNode属性已经解析结束了。回归到
generate方法中
// generate方法的返回部分
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
}
最后因为本例中useWithBlock=true,所以调用deindent方法换行缩进(--context.indentLevel个空格),push('}')累加'}',再调用deindent()和push('}')方法换行缩进并累加'}',最终generate函数返回一个对象{ ast, code: 拼接的renderString字符串, ... }。本例为模版生成的renderString如下:
// 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 }, "修改数据", 8 /* PROPS */, ["onClick"])
], 64 /* STABLE_FRAGMENT */)
}
}
总结
整个generate方法类似于一个拨洋葱模型,刚开始利用运行环境、transform解析生成的helpers数组(存放创建节点的函数名称)等拼接renderString的开头部分(变量定义),接着从root根对象的codegenNode属性开始解析,依次解析tag(标签名称)、props(属性)、children(子节点对象)并且进行renderString字符串的拼接。当解析到children数组时会将数组的每个元素依次解析,主要是利用genNode方法中的switch-case命中不同的对象解析方法(例如: 静态文本、动态数据、元素节点等),当子节点完全解析完成之后,回归到上层节点继续解析patchFlag(patch标识位,后续调用patch方法生成DOM时需要用到)和dynamicProps(动态属性),直到root根对象也全部解析结束,那么context.code字符串就是生成的renderString,最终返回{ast, code,...}对象。后续会执行renderString中的render方法,生成VNode对象,再调用patch方法将VNode生成真正的DOM节点。
🍏🍎🍐🍊🍋🍌🍉🍇🍓🫐🍈🍒🍑🥭🍍🥥🥝🍅🍆🥑🥦