template 模板编译
Compile
分为三个阶段parse
optimize
generate
。最终会得到render function
。
![]()
- 实例
<div :class="c" class="demo" v-if="isShow">
<span v-for="item in sz">{{item}}</span>
</div>
var html = '<div :class="c" class="demo" v-if="isShow"><span v-for="item in sz">{{item}}</span></div>';
一、parse
首先通过 正则表达式对template字符串进行处理解析,得到指令、class、style、等数据,形成AST
(抽象语法树)。
// AST 如下
{
// 标签属性的map,记录了标签上的属性
"attrsMap": {
':class':'c',
'class':'demo',
'v-if': 'isShow'
},
// 解析得到的:class
'classBinding': 'c',
'if': 'isShow',
// if条件
'ifConditions': [
'exp': 'isShow'
],
// 静态class
'staticClass': 'demo',
// 标签的tag
'tag': 'div',
children: [
{
'attrsMap': {
'v-for': 'item in sz'
}.
// 循环的对象
'for': 'sz',
// 循环的参数
'alias': 'item',
// 循环是否已经被处理的标记位
'forProcessed': true,
'tag' : 'span',
'children' :[
{
/* 表达式,_s是一个转字符串的函数 */
'expression': '_s(item)',
'text': '{{item}}'
}
]
}
]
}
这样一颗AST树,比较清晰的描述了标签的属性以及依赖关系。
- 如何通过正则来把template模板编译为我们需要的AST呢?
// parse 里定义的一些正则
export const onRE = /^@|^v-on:/ //匹配 v-on
export const dirRE = /^v-|^@|^:/ //匹配 v-on 和 v-bind
export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/ //匹配 v-for 属性
export const forIteratorRE = /\((\{[^}]*\}|[^,]*),([^,]*)(?:,([^,]*))?\)/ //匹配 v-for 的多种形式
const ncname = '[a-zA-Z_][\\w\\-\\.]*';
const singleAttrIdentifier = /([^\s"'<>/=]+)/
const singleAttrAssign = /(?:=)/
const singleAttrValues = [
/"([^"]*)"+/.source,
/'([^']*)'+/.source,
/([^\s"'=<>`]+)/.source
]
const attribute = new RegExp(
'^\\s*' + singleAttrIdentifier.source +
'(?:\\s*(' + singleAttrAssign.source + ')' +
'\\s*(?:' + singleAttrValues.join('|') + '))?'
)
const qnameCapture = '((?:' + ncname + '\:)?' + ncname + ')'
const startTagOpen = new RegExp('^<' + qnameCapture)
const startTagClose = /^\s*(/?)>/
const endTag = new RegExp('^<\/' + qnameCapture + '[^>]*>')
const defaultTagRE = /{{((?:.|\n)+?)}}/g
const forAliasRE = /(.?)\s+(?:in|of)\s+(.)/
advance
因为我们解析 template 采用循环进行字符串匹配的方式,所以每匹配解析完一段我们需要将已经匹配掉的去掉,头部的指针指向接下来需要匹配的部分。
function advance (n) {
index += n
html = html.substring(n)
}
举个例子,当我们把第一个 div 的头标签全部匹配完毕以后,我们需要将这部分除去,也就是向右移动 43 个字符。
advance(43);
我们可以把这个过程理解为一个截取的过程,它把 template
字符串里的元素、属性和文本一个个地截取出来,其中的细节十分琐碎,涉及到各种不同情况(比如不同类型的 v-for,各种 vue 指令、空白节点以及父子关系等等),我们不再赘述。
假设我们有一个元素<div id="test">texttext</div>
,在 parse 完之后会变成如下的结构并返回:
ele1 = {
type: 1,
tag: "div",
attrsList: [{name: "id", value: "test"}],
attrsMap: {id: "test"},
parent: undefined,
children: [{
type: 3,
text: 'texttext'
}
],
plain: true,
attrs: [{name: "id", value: "'test'"}]
}
二、Optimize
optimize
主要作用就跟它的名字一样,用作「优化」
。
这个涉及到后面要讲 patch
的过程,因为 patch
的过程实际上是将 VNode
节点进行一层一层的比对,然后将「差异」
更新到视图上。那么一些静态节点
是不会根据数据变化而产生变化的,这些节点我们没有比对的需求,是不是可以跳过这些静态节点的比对,从而节省一些性能呢?
那么我们就需要为静态的节点做上一些「标记」
,在 patch 的时候我们就可以直接跳过这些被标记的节点的比对,从而达到「优化」的目的。
经过 optimize
这层的处理,每个节点会加上 static
属性,用来标记是否是静态的。
{
'attrsMap': {
':class': 'c',
'class': 'demo',
'v-if': 'isShow'
},
'classBinding': 'c',
'if': 'isShow',
'ifConditions': [
'exp': 'isShow'
],
'staticClass': 'demo',
'tag': 'div',
/* 静态标志 */ 【1】
'static': false,
'children': [
{
'attrsMap': {
'v-for': "item in sz"
},
'static': false,
'alias': "item",
'for': 'sz',
'forProcessed': true,
'tag': 'span',
'children': [
{
'expression': '_s(item)',
'text': '{{item}}',
'static': false
}
]
}
]
}
三、generate 生成 render
生成 render
的 generate 函数的输入也是 AST,它递归了 AST 树,为不同的 AST 节点创建了不同的内部调用方法,等待后面的调用。生成 render 函数的过程如下:
假设我们有这么一段 template
<template>
<div id="test">
{{val}}
<img src="http://xx.jpg">
</div>
</template>
// 最终转换为函数字符串
{render: "with(this){return _c('div',{attrs:{"id":"test"}},[[_v(_s(val))]),_v(" "),_m(0)])}"}
几种内部方法
_c:对应的是 createElement 方法,顾名思义,它的含义是创建一个元素(Vnode)
_v:创建一个文本结点。
_s:把一个值转换为字符串。(eg: {{data}})
_m:渲染静态内容
总结
整个 Vue 渲染过程,前面我们说了 complie 的过程,在做完 parse、optimize 和 generate 之后,我们得到了一个 render 函数字符串。
接下来就会对数据做watcher,对绑定的数据进行监听,当数据发生变化,通过diff
对新的VNode 和 之前的VNode 进行比较,重新渲染改变了的部分。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。