无论是手写虚拟 DOM(渲染函数)还是使用模板,都属于声明式地描述 UI,并且 Vue.js 同时支持这两种描述 UI 的方式。
我们已经知道虚拟 DOM 是如何渲染成真实 DOM 的,那么模板是如何工作的呢?这就要提到 Vue.js 框架中的另外一个重要组成部分:编译器。
编译器和渲染器一样,只是一段程序而已,不过它们的工作内容不同。
编译器的作用其实就是将模板编译为渲染函数,例如给出如下模板:
<div @click="handler">click me</div>
以我们熟悉的 .vue 文件为例,一个 .vue 文件就是一个组件,如下所示:
<template>
<div @click="handler">click me</div>
</template>
<script>
export default {
data() {
/* ... */
},
methods: {
handler: () => {
/* ... */
},
},
};
</script>
其中 <template> 标签
里的内容就是模板内容,编译器会把模板内容编译成渲染函数并添加到 <script> 标签
块的组件对象上,所以最终在浏览器里运行的代码就是:
export default {
data() {
/* ...
*/
},
methods: {
handler: () => {
/* ... */
},
},
render() {
return h("div", { onClick: handler }, "click me");
},
};
所以,无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟 DOM 渲染为真实 DOM,这就是模板的工作原理,也是 Vue.js 渲染页面的流程。
Vue.js 是各个模块组成的有机整体
组件的实现依赖于渲染器,模板的编译依赖于编译器,并且编译后生成的代码是根据渲染器和虚拟 DOM 的设计决定的,因此 Vue.js 的各个模块之间是互相关联、互相制约的,共同构成一个有机整体。
我们以编译器和渲染器这两个非常关键的模块为例,看看它们是如何配合工作,并实现性能提升的。
假设我们有如下模板:
<div id="foo" :class="cls"></div>
根据上文的介绍,我们知道编译器会把这段代码编译成渲染函数:
render() {
// 为了效果更 加直观,这里没有使用 h 函数,而是直接采用了虚拟 DOM 对象
// 下面的代码 等价于:
// return h('div', { id: 'foo', class: cls })
return {
tag: 'div',
props: {
id: 'foo',
class: cls
}
}
}
在这段代码中,cls 是一个变量,它可能会发生变化。
我们知道渲染器的作用之一就是寻找并且只更新变化的内容,所以当变量 cls 的值发生变化时,渲染器会自行寻找变更点。
对于渲染器来说,这个“寻找”的过程需要花费一些力气。
那么从编译器的视角来看,它能否知道哪些内容会发生变化呢?如果编译器有能力分析动态内容,并在编译阶段把这些信息提取出来,然后直接交给渲染器,这样渲染器不就不需要花费大力气去寻找变更点了吗?这是个好想法并且能够实现。Vue.js 的模板是有特点的,拿上面的模板来说,我们一眼就能看出其中 id=“foo” 是永远不会变化的,而 :class=“cls” 是一个 v-bind 绑定,它是可能发生变化的。
所以编译器能识别出哪些是静态属性,哪些是动态属性,在生成代码的时候完全可以附带这些信息:
render() {
return {
tag: "div",
props: {
id: "foo",
class: cls,
},
patchFlags: 1, // 假设数字 1 代表 class 是动态的
};
}
如上面的代码所示,在生成的虚拟 DOM 对象中多出了一个 patchFlags 属性,我们假设数字 1 代表“ class 是动态的”,这样渲染器看到这个标志时就知道:“哦,原来只有 class 属性会发生改变。”对于渲染器来说,就相当于省去了寻找变更点的工作量,性能自然就提升了。
通过这个例子,我们了解到编译器和渲染器之间是存在信息交流的,它们互相配合使得性能进一步提升,而它们之间交流的媒介就是虚拟 DOM 对象。当然,虚拟 DOM 对象中会包含多种数据字段,每个字段都代表一定的含义。