之前我们学到了 Vue 更新数据是如何更新视图的。
简单回顾
数据更新(setter)-> 通知依赖收集集合(Dep) -> 调用所有观察者(Watcher) -> 比对节点树(patch) -> 视图
在更新视图这一步,使用异步更新策略
为什么呢?引用小册中的例子,下面有一个这样的 Vue 组件
<template>
<div>
<div>{{number}}</div>
<div @click="handleClick">click</div>
</div>
</template>
<script>
export default {
data () {
return {
number: 0
};
},
methods: {
handleClick () {
for(let i = 0; i < 1000; i++) {
this.number++;
}
}
}
}
</script>
在for循环中,我们连续更改了1000次绑定数据 number ,如果使用同步更新,则需要1000次的 patch ,也就是1000次的 Diff ,1000次更新,这就很可怕了。
所以,在 Vue 里使用异步更新的方法,每次触发某个数据的 setter 方法后,对应的 Watcher 对象其实会被 push 进一个队列 queue 中,在下一个 tick(代表一次异步) 的时候将这个队列 queue 全部拿出来 run( Watcher 对象的一个方法,用来触发 patch 操作) 一遍。
nextTick
在 Vue 里,实现了一个 nextTick 函数,主要用来异步操作,参数为一个 callback 函数,会被存放在 callback 队列中,在下一个 tick 时触发队列中的所有 callback 事件。
在 Vue 源码中,使用 setImmediate、MessageChannel、setTimeout 来实现 macroTimerFunc(nextTick 中使用的异步方法),使用 Promise 来实现 microTimerFunc ,感兴趣可以看看 next-tick 。下面我们同样用 setTimeout 来举例。
/* cb 函数集合、 pending 是一个标记位,代表一个等待的状态 */
let callbacks = [];
let pending = false;
/**
* 异步钩子函数
* 目的是在当前调用栈执行完毕以后(不一定立即)才会去执行这个事件
*/
function nextTick(cb) {
callbacks.push(cb);
/* 第一次执行时,设定异步执行器 timeout 当前执行栈执行完,调用 flushCallbacks */
if (!pending) {
pending = true;
setTimeout(flushCallbacks, 0);
}
}
/* cb 集合执行函数 */
function flushCallbacks() {
pending = false;
/* 生成 cb 集合副本 copies */
const copies = callbacks.slice(0);
/* cb 集合长度赋值为0 */
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
注意:Vue文档中说明,在DOM更新之后立即执行callback函数,可以使用Vue.$nextTick(),我理解是应该是将这些callback函数,放在dom更新的函数后面
重写Watcher
在前面说到,连续更新1000次 number ,不可能连续渲染视图1000次,因此 有一个 queue 队列来收集过滤 Watcher ,同一个 Watcher 在同一个 tick 的时候只执行一次。
/* 用来给 Watcher 作唯一标识 */
let uid = 0;
/* 观察者 */
class Watcher {
constructor() {
/* 标识 */
this.id = ++uid;
}
/* 调用 queueWatcher 将 Watcher 推入过滤队列 */
update() {
console.log('watch' + this.id + ' update');
queueWatcher(this);
}
/* 更新视图函数 在函数里触发 patch */
run() {
console.log('watch' + this.id + '视图更新啦~');
}
}
注意: update 方法,在修改数据后由 Dep 对象来调用,而 run 函数才是真正触发 patch 函数更新视图的方法
queueWatcher
用来存放并过滤相同 Watcher 的函数,第一次调用时,会将调用执行 Watcher 队列的函数推入 nextTick 函数,达到异步更新的效果。
/* 用来区分当前 Watcher 是否已存放的 map */
let has = {};
/* 存取 Watcher 的队列 */
let queue = [];
/* 标识位,标记是否已经向 nextTick 传递了 flushSchedulerQueue 方法 */
let waiting = false;
function queueWatcher(watcher) {
const id = watcher.id;
/* 相同的 Watcher 不会被重复传入 */
if (has[id] == null) {
has[id] = true;
queue.push(watcher);
/* 第一次被调用,将 flushSchedulerQueue 函数推入 nextTick */
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue);
}
}
}
flushSchedulerQueue
用来调用所有队列中的 Watcher.run() 函数,触发 patch 函数。
/* 用来调用所有队列中的 Watcher.run() 函数,触发 patch 函数 */
function flushSchedulerQueue() {
let watcher, id;
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
id = watcher.id;
/* 将区分 map 归为原始值 */
has[id] = null;
watcher.run();
}
waiting = false;
}
注:代码参考《批量异步更新策略及 nextTick 原理》