上文解析了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
节点。
🍏🍎🍐🍊🍋🍌🍉🍇🍓🫐🍈🍒🍑🥭🍍🥥🥝🍅🍆🥑🥦