本文主要在于说明其中的实现思想,如果想看实际代码实现可至文末查阅。
简单来说,最基本的实现从插值表达式到变量值这一过程只要两步就行
- 模板解析: Vue 的编译器会将模板字符串解析成抽象语法树(AST),表示模板的结构。
- 虚拟 DOM 生成: 将 AST 转换为可执行的渲染函数。渲染函数是一个 JavaScript 函数,它接收数据作为参数,并返回一个虚拟 DOM(VNode)树,描述了如何渲染数据到实际的 DOM。
这两步就可以实现基本的插值表达式转实际值,但是在vue的完整的渲染流程中,还有其它的流程,如AST 优化、虚拟 DOM 比对、DOM更新等等. 这些都是优化dom更新的策略,本文重心不在这里,所以不说。
模板解析
写插值表达式都是在html代码中,以{{ }}
来表明。那么需要解析插值表达式内的值就必须要操作整个被vue接管的html或者说模板内的内容。可以说是将模板字符串逐字分析 。
设定一个基础的可以表示插值表达式的模板
html
<div id="app" style="color: rgb(45, 98, 245); font-size: 18px">
现在的毫秒时是 {{ dateTime }}
<p class="p">今天周 {{ Week }}</p>
</div>
这一段html代码由几个东西组成,标签元素、元素属性、文本、插值表达式。 记住这几个,他们将是抽象语法树(AST)的重要组成部分。
抽象语法树(AST)
在模板解析中,AST就是一个用于表示html代码的结构的树形对象,它用一组数据记录每一层元素的各种信息及与它本身的父子元素关系。
基本结构
这是一个在模板渲染中生成AST得最基本得结构。插值表达式在这个结构得text属性值中。
js
{
attr: '元素属性',
tag: '标签名',
type: '节点类型'
text: '文本节点',
children: '子元素',
parent: '父元素'
}
将上面设定得html代码解析为AST语法树大致是下面这个样子。插值表达式存在于text属性中。此时并没有去解析。
json
{
"tag": "div",
"attrs": [
{
"name": "id",
"value": "app"
},
{
"name": "style",
"value": "color: rgb(45, 98, 245); font-size: 18px"
}
],
"children": [
{
"type": 3,
"text": "现在的毫秒时是{{dateTime}}"
},
{
"tag": "p",
"attrs": [
{
"name": "class",
"value": "p"
}
],
"children": [
{
"type": 3,
"text": "今天周{{Week}}"
}
],
"type": 1,
"parent": "div"
}
],
"type": 1,
"parent": null
}
如何解析成AST的
上面说到,模板字符串转为AST的过程是一个将字符串逐字分析的过程。虽然有点儿夸张,但也贴切。
vue准备了很多的正则语法来匹配对应的词,来匹配不同的部分,确定这部分内容属于什么。
js
// 标签属性
var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
// 标签名称
var ncname = "[a-zA-Z_][\\-\\.0-9_a-zA-Z" + (unicodeRegExp.source) + "]*";
// <span:xx>
var qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
// 开始标签 获取标签名 <
var startTagOpen = new RegExp(("^<" + qnameCapture));
// 匹配开始标签的结束 >
var startTagClose = /^\s*(\/?)>/;
// 匹配结束标签 </div>
var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
利用这些正则表达式的流程我用一张图来表示,看图比文字说明更易懂。
抽象语法树的建立是虚拟 DOM 生成的必要前提,虚拟dom的建立才是解析插值表达式的地方。
虚拟 DOM 生成
在得到了AST语法树之后,首先要做的是生成渲染函数,在执行这个渲染函数的同时就会取解析插值表达式的值。 生成渲染函数 vue在生成渲染函数时定义了三个函数_c、_v、_s
,分别代表标签、文本、变量 ,最后将靠这些函数来生成虚拟dom。
渲染函数需要接受一个可执行的字符串,这个样子(是一个长字符串)。
js
_c('div',{id:"app",style:{"color":"rgb(45, 98, 245)","font-size":"18px"}},_v("现在的毫秒时是"+_s(dateTime)),_c('p',{class:"p"},_v("今天周"+_s(Week))))
转化步骤:
- 元素节点:使用
_c
第一个参数为标签名,第二个为属性,第三个为文本和_s
的组合,第四个递归子元素。 - 文本节点:直接放在
_c
的第三个参数 - 变量:使用
_s
包裹与文本放在一起
将渲染函数可接受的字符串生成了之后,需要先提出一个函数with
。虽然在MDN上表明此方法已被弃用,但是vue2是使用这个方法,作为学习了解不用关心。
with函数
用于创建一个临时的作用域,以便在其中访问特定对象的属性,从而可以在代码中更简洁地引用这些属性。也就是在with块作用域内可以直接访问传入的参数的属性,类似解构的作用。
js
with({name: 1, age: 2}){ console.log(name, age); // 1 2 }
渲染函数完整示例
js
new Function(`with(this){ return _c('div',{id:"app",style:{"color":"rgb(45, 98, 245)","font-size":"18px"}},_v("现在的毫秒时是"+_s(dateTime)),_c('p',{class:"p"},_v("今天周"+_s(Week)))) }`);
with的this就是当前vue实例,变量在渲染函数执行的过程中解析为实际的值。渲染函数执行完返回的就是虚拟dom。得到虚拟dom就可以把解析完插值表达式的模板渲染到页面上。渲染就是根据虚拟dom的结构(类似AST的结构)创建元素的过程。
相关链接