如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,为了性能考虑,Vue 会在本轮数据更新后,再异步去更新视图。Vue 内部会汇总 data 的修改,然后一次性更新视图,这样做是为了减少 DOM 操作次数,提高性能。通过 Vue 中的 $nextTick
函数也可以看出是异步更新。在数据更新后,如果我们要拿到更新后的 DOM 状态,应该使用 $nextTick
函数,比如使用数据把 li 列表增加了,想要获取到增加后的 li 元素数目,可以这么做:
methods: {
add (item) {
this.list.push(item);
this.$nextTick(() => {
console.log(this.$refs.ul.children.length);
});
}
}
在 Vue 中,使用 vue-template-complier 这个模块可以将模板(template)编译成 render 函数,执行 render 函数会生成虚拟 DOM。模板并不是 HTML,它有指令、插值、JS 表达式,还可以使用判断和循环,而 HTML 是标签语言,只有 JS 才能实现判断、循环。因此,模板一定是转换成了某种 JS 代码,通过对模板编译,生成原生的 JavaScript 代码。
比如下面的例子,使用 vue-template-complier 这个模块将模板代码编译成 JS 代码:
const complier = require('vue-template-compiler');
const template = "<div id='main' v-if='isShow'><button class='btn' @click='handle'>Click</button></div>";
const res = complier.compile(template); // 编译模板
console.log(res.render); // 获取 render 属性
res.render
会打印出:
with(this){return (isShow)?_c('div',{attrs:{"id":"main"}},[_c('button',{staticClass:"btn",on:{"click":handle}},[_v("Click")])]):_e()}
with
是 JavaScript 中一个不建议使用的语句。它可以传入一个对象,在 with 语句内部,获取变量其实是在获取传入对象的属性。
var obj = {
a: 1,
b: 2
};
with(obj){
console.log(a); // 获取的是 obj.a,1
console.log(b); // 2
console.log(c); // with 中获取不存在的属性或报错!
}
可见 res.render
内部的变量其实都是 Vue 实例上的属性。_c
函数即 createElement,创建 DOM 元素,_v
则相当于 createTextNode。模板被编译成了由 JavaScript 描述的 DOM 结构。在 Vue 中,除了使用 template JSX 写法之外,也可以这样定义 template:
{
render: function(createElement){
return createElement(
'div',{
staticClass: 'container'
},
[
createElement('h2'),
this.msg
]
);
}
}
上面代码就相当于:
<div class="container">
<h2>{{ msg }}</h2>
</div>
编译 v-model
const complier = require('vue-template-compiler');
const template = `<div id='main'><input v-model='name' /></div>`;
const res = complier.compile(template);
console.log(res.render);
编译后的代码如下:
with(this){return _c('div',{attrs:{"id":"main"}},[_c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})])}
可以发现,v-model 默认通过绑定 input
事件更新 name 数据。
Vue 内部使用了大量的正则表达式和字符串拼接,将 template 编译成上面的代码形式。
模板编译成 render 函数,当执行 render 函数后会返回虚拟 DOM,基于虚拟 DOM 再执行 patch 和 diff。使用 webpack vue-loader,会在开发环境下编译模板(vue-cli 也是使用 webpack 构建),而不是在运行时编译,这样可以提高编译速度。这在启动本地服务时也能看出来,首次编译会比较慢,而之后修改代码更新页面会发现很快。
渲染过程
-
初次渲染
- 解析模板为 render 函数(或在开发环境下已完成);
- 触发响应式,监听 data 属性的 getter、setter(初次渲染时一般是触发 getter 执行);
- 执行 render 函数,生成虚拟 DOM(vnode),调用 patch(elem, vnode); 将 vnode 挂载到 elem 上。
-
更新过程
- 当修改数据时,会触发 setter;
- 重新执行 render 函数,生成 newVnode(新的虚拟 DOM);
- 执行 patch(vnode, newNode),新旧 vnoode 对比(diff 算法),更新视图。
渲染过程如下图所示:
图中,拦截属性的获取会进行依赖收集(Collect as Dependency);拦截属性的更新操作,对相关依赖进行通知(Notify)。