目录
1 介绍
代码生成器是模板编译的最后一步,它的作用是将AST转换成渲染函数中的内容,这个内容可以称为代码字符串。
代码字符串可以被包装在函数中执行,这个函数就是渲染函数。
渲染函数被执行之后,可以生成一份VNode,而虚拟dom可以通过这个VNode来渲染视图。
例子:
<div id="el"> Hello {{ name }} </div>
转换成AST并且经过优化之后:
{
type:1,
tag:"div",
attrsList:[{
name:"id",
value:"el"
}],
attrsMap:{
id:"el"
},
children:[{
type:2,
expression:'"Hello " + _s(name)',
text:'Hello {{name}}',
static:false
}],
plain:false,
attrs:[{
name:"id",
value:'"el"'
}],
static:false,
staticRoot:false
}
代码生成器可以通过AST来生成代码字符串,结果如下:
`with(this){
return _c(
"div",
{
attrs:{id:'el'},
},
[
_v("Hello " + _s(name))
]
)
}`
创建虚拟节点方法的三个参数分别是:
1.标签名;
2.一个包含模板相关属性的数据对象;
3.子节点列表;
渲染函数其实是执行了createElement,而createElement可以创建一个VNode。
2 通过AST生成代码字符串
生成字符串是一个递归的过程,从顶向下依次处理每一个AST节点。
节点有三种类型:
类型:元素节点 , 创建方法: createElement , 别名: _c;
类型:文本节点 , 创建方法:createTextVNode,别名: _v;
类型:注释节点 , 创建方法: createEmptyVNode ,别名:_e;
例如:
<div id="el">
<div>
<p>Hello {{name}}</p>
</div>
</div>
插入dom的结构图:
上面的整段代码会被包裹在with语句中,伪代码如下:
`with(this){ return ${code} }`
3 代码生成器的原理
节点有不同的类型,例如元素节点、文本节点和注释节点。
不同类型的生成方式是不一样,下面我们将分别介绍生成每个类型的节点。
3.1 元素节点
生成元素节点,其实就是生成一个_c的函数调用字符串,代码如下:
function genElement(el,state){
// 如果el.plain是true,则说明节点没有属性
const data = el.plain ? undefined : genData(el,state)
const children = genChildren(el,state)
code = `_c('${el.tag}'${
data ? `,${data}` : "" // data
}${
children ? `,${children}` : "" // children
})`
return code
}
代码中el的plain属性是在编译时发现的。如果节点没有属性,就会把plain设置为true。这里我们可以通过plain来判断是否需要获取节点得属性数据。
代码中得主要逻辑是用genData和genChildren分别获取data和children,然后将它们分别拼到字符串中指定得位置。
genData代码如下:
function genData(el:ASTElement,state:CodegenState){
let data = "{"
// key
if(el.key){
data += `key:${el.key},`
}
// ref
if(el.ref){
data += `ref:${el.ref},`
}
// pre
if(el.pre){
data += `pre:true,`
}
// 类似得还有很多情况
data = data.replace(/,$/,'') + '}'
return data
}
其实也是拼字符串。先给data赋值一个'{',然后发现节点存在哪些属性数据,就将这些数据拼接到data中,最后拼接一个'}',此时一个完整得data就拼好了。
genChildren代码:
function genChildren(el,state){
const children = el.children
if(children.length){
return `[${children.map(c=>genNode(c,state)).join(",")}]`
}
function genNode(node,state){
if(node.type ===1){
return genElement(node,state)
} else if(node.type === 3 && node.isComment){
return genComment(node)
}else{
return genText(node)
}
}
}
3.2 文本节点
genText函数实现代码:
function genText(text){
return `_v(${text.type === 2
? text.expression
: JSON.stringify(text.text) })`
}
如果是动态文本,则使用expression;如果是静态文本,则使用text。
3.3 注释节点
代码如下:
function genComment(comment){
return `_e${JSON.stringify(comment.text)}`
}
4 总结
代码生成器的作用及其内部原理,了解了代码生成器其实就是字符串拼接的过程。通过递归AST来生成字符串,最先生成根节点,然后在子节点字符串生成后,将其拼接在根节点的参数中,子节点的子节点拼接在子节点的参数中,这样一层一层地拼接,直到最后拼接成完整的字符串。
最后,当字符串拼接好后,会将字符串拼在with中返回给调着用。