1 为什么要异步更新?
- Vue 是如何在修改 data 中的数据后修改视图的:
setter -> Dep -> Watcher -> patch -> 视图
1.1 栗子
<template>
<div>
<div>{{number}}</div>
<div @click="handleClick">click</div>
</div>
</template>
export default {
data() {
return {
number: 0
};
},
methods: {
handleClick() {
for(let i = 0; i < 1000; i++) {
this.number++;
}
}
}
}
number+1
,DOM 就更新一次?
1.2 异步更新策略
Vue 在默认情况下
-
每次触发某个数据的
setter
方法后,对应的watcher
对象会被push
进一个队列queue
中 -
在下一个
tick
时再触发queue
的watcher
对象的run
(用来触发patch
操作)
什么是下一个 tick?
2 nextTick
- Vue 实现了一个
nextTick
函数,传入一个cb
,这个cb
会被存储到一个队列中,在下一个tick
时触发队列中的所有cb
事件 - Vue 源码分别用
Promise
、setTimeout
、setImmediate
等方式在microtask
中创建一个事件,目的是在当前调用栈执行完毕之后才会去执行这个事件(不一定立即执行)
// 001 callbacks数组用来存储nextTick
// 在下一个tick处理这些回调函数之前,所有的cb会被存在这个callbacks数组中
let callbacks = [];
// 002 pending是一个标记位,代表一个等待的状态
let pending = false;
function nextTick(cb) {
callbacks.push(cb);
if(!pending) {
pending = true;
setTimeout(flushCallbacks, 0);
}
}
// 003 flushCallbacks会在执行时将callbacks中的所有cb依次执行
function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for(let i = 0; i < copies.length; i++) {
copies[i]();
}
}
number+1
,Watcher 就 push
一次、执行一次?
3 Watcher
需要执行一个过滤操作,同一个
Watcher
在同一个tick
时应该只被执行一次,即队列queue
中不应该出现重复的Watcher
对象
-
用
id
标记每一个Watcher
对象 -
实现
update
方法,在修改数据后由Dep
来调用,而run
方法才是真正的触发patch
更新视图的方法
let uid = 0;
class Watcher {
constructor() {
this.id = ++uid;
}
update() {
console.log('watch' + this.id + 'uodate');
queueWatcher(this);
}
run() {
console.log('watch' + this.id + '视图更新啦~');
}
}
4 queueWatcher
// 001 has用来判断是否已经存在相同的Watcher对象
// has->map,存放id->ture|false的形式
let has = {};
let queue = [];
// 003 wating是一个标记位
// 标记是否向nextTick传递了flushSchedulerQueue方法
let waiting = false;
function queueWatcher(watcher) {
const id = watcher.id;
// 002 如果queue没有这个Watcher对象
if(has[id] == null) {
has[id] = true;
// 0021 该对象会被push进queue中
queue.push(watcher);
if(!waiting) {
waiting = true;
// 004 在下一个tick时执行flushSchedulerQueue来flush queue,执行他里面的所有Watcher对象的run方法
nextTick(flushSchedulerQueue);
}
}
}
5 flushSchedulerQueue
function flushSchedulerQueue() {
let watcher, id;
for(index = 0; index < queue.length; index++) {
watcher = queue[index];
id = watcher.id;
has[id] = null;
watcher.run();
}
waiting = false;
}
6 栗子
let watch1 = new Watcher();
let watch2 = new Watcher();
watch1.update();
watch1.update();
watch2.update();
如果没有异步更新策略,理论上应该执行 Watcher 对象的 run,则会打印
watch1 updata
watch1更新啦~
watch1 updata
watch1更新啦~
watch2 updata
watch2更新啦~
实际上执行
watch1 updata
watch1 updata
watch2 updata
// 这就是异步更新策略的效果,相同的Watcher对象会在这个过程中被剔除,在下一个tick时去更新视图
watch1更新啦~
watch2更新啦~