Vue的模板

Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析。在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。

模板渲染

Vue模板的渲染过程入下图所示,首先通过模板compiler编译器将模板编译成AST(抽象语法树),再由AST生成Vue的render函数,渲染函数结合数据再生成vnode(Virtual DOM树),对vnode进行diff和patch后生成新的UI。
'vue-template'

Virtual DOM

由于操作真实的DOM会对性能带来损失,可以通过模拟真实的DOM对象树,建立一个虚拟DOM对真实DOM 发生的变化保持追踪。
vnode包含以下属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
this.tag = tag //当前节点的标签名
this.data = data //当前节点的数据对象
this.children = children //数组类型,包含了当前节点的子节点
this.text = text //当前节点的文本,一般文本节点或注释节点会有该属性
this.elm = elm //当前虚拟节点对应的真实的DOM节点
this.ns = undefined //节点的namespace
this.context = context //编译作用域
this.fnContext = undefined //函数节点的上下文
this.fnOptions = undefined //函数options
this.fnScopeId = undefined //函数作用域id
this.key = data && data.key //节点的key属性,用于作为节点的标识,有利于patch的优化
this.componentOptions = componentOptions //创建组件实例时会用到的选项信息
this.componentInstance = undefined //组件实例
this.parent = undefined //组件占位节点
this.raw = false //Raw HTML
this.isStatic = false //静态节点的标识
this.isRootInsert = true //是否作为根节点插入,被<transition>包裹的节点,该属性的值为false
this.isComment = false //当前节点是否是注释节点
this.isCloned = false //当前节点是否为克隆节点
this.isOnce = false //当前节点是否有v-once指令
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false

createElement函数

createElement函数用来创建vnode,createElement接受的参数,

1
2
3
4
5
6
7
8
export function createElement (
context: Component,
tag: any, //一个 HTML 标签字符串,组件选项对象,或者一个返回值
data: any, //一个包含模板相关属性的数据对象
children: any, //子节点 (VNodes),由 `createElement()` 构建而成
normalizationType: any,
alwaysNormalize: boolean
)

render函数

Vue的模板实际是编译成了render函数,可以使用Vue.compile(template)方法编译下面的模板

1
2
3
4
5
6
7
8
9
10
11
<div>
<header>
<h1>I'm a template!</h1>
</header>
<p v-if="message">
{{ message }}
</p>
<p v-else>
No message.
</p>
</div>

方法会返回一个对象,对象中有render和staticRenderFns两个值,下面是生成的render函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function() {
with(this) {
return _c(
//创建一个div元素
'div',
[
// 静态节点 header,此处对应 staticRenderFns 数组索引为 0 的 render 函数
_m(0),
// 三元表达式,判断 message 是否存在
// 如果存在,创建 p 元素,元素里面有文本,值为 toString(message)
// 如果不存在,创建 p 元素,元素里面有文本,值为 No message.
(message)? _c('p', [ _v( _s(message))]) : _c('p', [ _v("No message.")])
]
)
}
}
其中:
_c : createElement(创建元素)
_m : renderStatic(渲染静态节点)
_v : createTextVNode(创建文本DOM)
_s : toString (转换为字符串)

staticRenderFns数组与diff算法优化相关,我们会在编译阶段给后面不会发生变化的vnode节点打上static为true的标签,那些被标记为静态节点的vnode就会单独生成staticRenderFns函数:

1
2
3
4
5
6
7
8
9
10
_m(0): function anonymous() {
with(this) {
return _c(
'header',
[
_c('h1', [ _v("I'm a template!")])
]
)
}
}

观察者(Watcher)

每个Vue组件都有一个对应的Watcher,这个Watcher会在组件render的时候收集组件所依赖的数据,并在依赖有更新的时候重新渲染组件。
上图中我们可以以render函数作为分割线,render函数左边称为编译期,将vue的模板转换为渲染函数。render函数的右边是在vue的运行时,主要是基于渲染函数生成Virtual Dom树,然后对Virtual Dom进行diff和patch。
下面图描述了vue模板渲染的流程:
vue-template-10

  • new Vue():实例化Vue
  • $mount():获取模板,并且在这过程中调用相关方法_count,new Watcher()实现数据响应式,当Watcher监听到数据变化,就会执行render函数输出一个新的vnode树形结构的数据
  • compileToFunction():将tenplate编译成render函数。首先读缓存,在compileToFunction()中,会创建一个对象,把complie编译完后的对象的render 和 staticRenderFns 两个属性分别转换成函数缓存在对象中,然后把对象存进缓存,没有缓存就调用compile 方法拿到 render 函数的字符串形式,在通过new Function的方式生成真正的渲染函数。
  • compile:将 template 编译成 render 函数的字符串形式,这个函数主要有三个步骤组成:parse,optimize和generate,最终输出一个包含 ast,render 和 staticRenderFns的对象。compile 函数主要是将 template 转换为 AST,优化 AST,再将 AST 转换为render函数字符串,render 函数与数据通过 Watcher 产生关联。
  • update():update判断是否首次渲染,是则直接创建真实DOM,否则调用patch(),并且进行触发钩子和更新引用等其他操作。
  • patch():新旧vnode对比的diff函数,对两个树结构进行完整的diff和patch的过程,最终只有发生了变化的节点才会被更新到真实 DOM 树上。
  • destroy():完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。触发beforeDestroy和destroyed的钩子。在大多数场景中你不应该调用这个方法。最好使用 v-if 和 v-for 指令以数据驱动的方式控制子组件的生命周期。