序
在进行日常的vue项目开发时,我们采用的时官方推荐的SFC(single file component)规范,将我们<template>标签中写入我们需要渲染的视图模板,vue会根据组件内的业务逻辑生成最终的实际视图。模板中的内容跟我们日常的html文档节点类似,但是vue的模板中还拥有静态html不具备的能力,模板内容中除了静态的元素节点,还包含属性、指令、插槽、组件、表达式等更丰富的内容。从简单字符串模板到最终的实际dom,这中间经历了哪些过程?
1、模板解析
为了开发者开发的便利,vue提供了template的模板语法。对于开发者来说,这种结构近于真实的dom结构,利于视图的构建。但是对于vue程序来说,他们此时还只是一堆有特殊意义的字符串而已(只是文本描述),如果需要使用的话还需要进行进一步的处理(得到js对象的描述)。因为在vue生成实际的视图时,需要了解到视图内部的状态、视图中的属性、组件、静态节点、指令等内容。所以我们需要让vue知道vue关心的内容。
vue的模板解析是通过正则捕获关键数据+字符串的截取,边解析边截取,并最终生成一个可以描述整个模板的AST抽象语法树实现的。
假定模板如下:
<div class="test" :prop1="vProp1" @click="clickHandler" v-dir="d">
<span>text</span>
<component-a></component-a>
</div>
这里为了能完整描述模板的树型层级结构,需要使用一个栈型结构来记录匹配记录,保证每个节点的都能正确获取到元素的父子层级关系。栈中最后一个元素节点将被标记为当前父元素,接下来开始模板解析......
说明:
以下'删除'代指删除模板字符串内对应的字符串
栈中最后一个元素会被标记为当前父元素
1、首先通过正则捕获模板的开始标签('<div'),记录开始标签的元素名称(首个开始元素会被标记为根节点,'div'进栈)。'删除<div'。然后循环匹配标签上的属性(指令、事件),匹配到一个属性就记录一个属性的信息并'删除对应属性',直到匹配到开始标签的结束标识符('>')。匹配到开始标签的结束标识符后直接删除开始标签的结束标签('>')。
2、匹配span的开始标签('<span',span当前的父元素是节点是div,span进栈),'删除<span',匹配span的结束标签('>'),'删除>'。匹配文本('text',文本节点的父亲元素是span),'删除text',匹配span的元素结束标签('</span>'),'删除</span>'。span元素弹出(栈),这时当前父元素又变成div。
3、匹配component-a的开始标签('component-a',component-a的当前父元素是div,component-a进栈),'删除<component-a'。匹配component-a开始标签的结束符('>'),'删除>',匹配component-a的结束标签('</component-a>'),'删除</component-a>',component-a'元素'弹出(栈)。
4、匹配div开始标签的结束标签('</div>'),'删除</div>'。div弹出(栈),模板内容截取剩余为空。模板解析完毕。
通过上面的一系列流程后,我们得到一个类似于下面这种AST对象:
{
ast: {
type: 1,
tag: 'div',
attrsList: [
{ name: ':prop1', value: 'vProp1' },
{ name: '@click', value: 'clickHandler' },
{ name: 'v-dir', value: 'd' }
],
children: [
{
type: 1,
tag: 'span',
attrsList: [],
parent: {
type: 1,
tag: 'div',
attrsList: [Array],
parent: undefined,
children: [Circular *1],
attrs: [Array],
events: [Object],
directives: [Array],
},
children: [
{ type: 3, text: 'text' }
]
},
{
type: 1,
tag: 'component-a',
attrsList: [],
parent: {
type: 1,
tag: 'div',
attrsList: [Array],
parent: undefined,
children: [Circular *1],
events: [Object],
directives: [Array]
},
children: [],
}
],
attrs: [ [Object] ],
events: { click: [Object] },
directives: [ [Object] ],
}
}
2、代码生成
得到ast语法树后,vue默认会进行一些优化。这里先忽略......
可以看到ast语法树上有标识当前元素的标签名、类型、属性、子元素、父元素。接下来该使用这些信息了。
如果只是静态的元素节点,当元素的标签、类型、属性明确后,这就足够去创建一个真实的元素了。如果标签名是原生的html标签,就创建原生的元素节点;如果是文本节点,就可以创建文本节点插入到指定位置。
这种定式的逻辑,我们肯定会使用函数去处理。vue中会使用codegen将整个ast语法树转化成一个可以创建视图的函数。codegen函数内部会遍历整个ast语法树,通过字符串拼接+ with + new Function的方式生成一个render函数,所以一般的视图渲染函数中会看到许多_c、_v、_t这种方法,它们实际的作用就是创建对应的节点。with方法中会传入当前视图的实例,所以我们可以省略this。执行render函数后可以生成对应模板的视图。
上面的ast最终会得到下面这种结果。
with(this){
return _c('div',{
directives:[
{name:"dir",rawName:"v-dir",value:(d),expression:"d"}
],
staticClass:"test",
attrs:{"prop1":vProp1},
on:{"click":clickHandler}
},
[_c('span',[_v("text")]),_v(" "),_c('component-a')],1)},1)
3、总结
最后总结一下:vue的模板解析是有:
1、字符模板-> 2、ast抽象语法树 -> 3、render函数 这3个步骤。使用webpack时,webpack会使用vue-loader和vue-tempalte-copmpiler事先将模板先编译成render函数。
当然具体的内部实现还会更加复杂,但是大致实现都是差不多的。
在生成抽象语法树的过程中需要注意的是:
1、模板解析的工具是正则
2、模板解析的顺序从开始标签->属性->开始标签结束符->子节点->标签结束标签
在生成render函数需要注意的是:
1、函数是通过字符串拼接 + new Function + with 实现的
2、特殊属性的处理:如指令、事件、动态属性、class、style
3、插槽的处理
4、以_开头的方法实际上是创建各种节点