代码生成器是模板编译的最后一步,它的作用是将AST转换成渲染函数中的内容(代码字符串),当渲染函数被执行后,可以生成一份VNode,虚拟DOM就是通过这个VNode来渲染视图。
那么,如何根据AST来生成代码字符串? 我们来详细了解一下。
一、代码字符串生成过程
生成代码字符串是一个递归的过程,从上向下依次处理每一个AST节点。在递归的过程中,每处理一个AST节点,就会生成一个与节点类型相对应的代码字符串。
假如现在有如下模板:
<div id="el">
<p>Hello {{name}}</p>
</div>
它转换成AST并经过优化器的优化后是下面的样子。
ast = {
'type': 1,
'tag': 'div',
'attrsList': [
{
'name':'id',
'value':'el',
}
],
'attrsMap': {
'id': 'el',
},
'static':false,
'parent': undefined,
'plain': false,
'children': [{
'type': 1,
'tag': 'p',
'plain': false,
'static':false,
'children': [
{
'type': 2,
'expression': '"Hello "+_s(name)',
'text': 'Hello {{name}}',
'static':false,
}
]
}]
}
然后我们来遍历上面的这个AST来生成对应的代码字符串。
1、生成根节点div,如下:
_c('div',{ attrs: { "id":"el"}})
2、生成div的子节点p,如下:
_c('div',{ attrs: { "id":"el"}}, [ _c('p')])
3、生成p节点下的文本,如下:
_c('div',{ attrs: { "id":"el"}}, [ _c('p',[_v("Hello "+_s(name)])])
到此,整个AST就遍历完毕了,我们将得到的代码再包装一下,如下:
`
with(this){
reurn _c(
'div',
{
attrs:{"id":"el"},
}
[
_c('p'),
[
_v("Hello "+_s(name))
]
])
}
`
观察生成后的代码字符串,我们会发现,这其实是一个嵌套的函数调用。函数_c的参数中执行了函数_v, 而函数_v的参数中又执行了函数_s。
其中,
- _c函数对应createElement方法,用来创建元素节点,它有三个参数,分别是:标签名、一个包含模板相关属性的数据对象、子节点列表;
- _v函数对应createTextVNode方法,用来创建文本节点,而_s函数则用来处理文本中的变量;
如果模板中有注释节点,则调用_e函数来处理。 _e函数对应createEmptyNode方法,用来创建注释节点。
二、代码字符串生成原理
不同类型节点的生成方式是不一样的,下面我们分别介绍如何生成每个类型的节点。
1、元素节点
生成元素节点,其实就是生成一个_c的函数调用字符串,相关代码如下:
export function genElement (el: ASTElement, state: CodegenState): string {
const data = el.plain ? undefined : genData(el, state)
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
return code
}
可以看出,生成元素节点,最关键的就是生成节点属性data和子节点列表children。
在生成节点属性data时,先判断plain属性是否为true,若为true则表示节点没有属性,将data赋值为undefined;如果不为true则调用genData函数获取节点属性data数据。
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','
// key
if (el.key) {
data += `key:${el.key},`
}
// ref
if (el.ref) {
data += `ref:${el.ref},`
}
if (el.refInFor) {
data += `refInFor:true,`
}
// pre
if (el.pre) {
data += `pre:true,`
}
// 篇幅所限,省略其他情况的判断
data = data.replace(/,$/, '') + '}'
return data
}
可以看到,源码中genData,就是在拼接字符串,先给data赋值为一个{,然后判断存在哪些属性数据,就将这些数据拼接到data中,最后再加一个},最终得到节点全部属性data。
节点属性data获取好了,再来获取一下子节点列表children。
获取子节点列表children其实就是遍历AST的children属性中的元素,然后根据元素属性的不同生成不同的VNode创建函数调用字符串,如下:
export function genChildren (el): {
if (children.length) {
return `[${children.map(c => genNode(c, state)).join(',')}]`
}
}
function genNode (node: ASTNode, state: CodegenState): string {
if (node.type === 1) {
return genElement(node, state)
} if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
2、文本节点
文本型的VNode可以调用_v(text)函数来创建,所以生成文本节点的render函数就是生成一个_v(text)函数调用的字符串。_v()函数接收文本内容作为参数,如果文本是动态文本,则使用动态文本AST节点的expression属性,如果是纯静态文本,则使用text属性。其生成代码如下:
export function genText (text: ASTText | ASTExpression): string {
return `_v(${text.type === 2
? text.expression // no need for () because already wrapped in _s()
: transformSpecialNewlines(JSON.stringify(text.text))
})`
}
3、注释节点
注释型的VNode可以调用_e(text)函数来创建,所以生成注释节点的render函数就是生成一个_e(text)函数调用的字符串。_e()函数接收注释内容作为参数,其生成代码如下:
export function genComment (comment: ASTText): string {
return `_e(${JSON.stringify(comment.text)})`
}