批量更新和nextTick 原理
为什么要异步策略
我们之前了解到如何在我们修改了data、中的数据之后修改视图了。其实就是setter
Dep
watcher
patch
视图
。
- 实例
<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++;
}
}
}
}
当按下之后会执行一个循环,data
中的number
会变化1000次
,难道会触发1000次setter ?
当然不是了。
Vue
默认当某一个数据触发setter
更改时,就将其对应的watcher
对象push
到一个queue
队列中,在下一次tick
的时候一次性处理,(wacher对象上的一个方法来触发patch操作);
nextTick
Vue.js 实现了一个 nextTick 函数,传入一个 cb ,这个 cb 会被存储到一个队列中,在下一个 tick 时触发队列中的所有 cb 事件。
用 setTimeout 来模拟这个方法
首先定义一个callbacks
数组用来存储nextTick
,在下一个 tick 处理这些回调函数之前,所有的cb
都会被存在这个callbacks
数组中。pending
是一个标记位,代表一个等待的状态。`setTimeout
会在task
中创建一个事件flushCallbacks
,flushCallbacks
则会在执行时将 callbacks 中的所有 cb 依次执行。
let callbacks = [];
let pending = false;
function nextTick (cb) {
// 每次nexttick 就是将回调push进入一个数组
callbacks.push(cb);
if (!pending) {
pending = true;
// 异步
setTimeout(flushCallbacks, 0);
}
}
function flushCallbacks () {
pending = false;
// 缓冲一个新的数组
const copies = callbacks.slice(0);
callbacks.length = 0;
// 清空之前的
for (let i = 0; i < copies.length; i++) {
copies[i]();
// 遍历执行
}
}
watcher
第一个例子中,当我们将 number 增加 1000 次时,先将对应的 Watcher 对象给 push 进一个队列 queue 中去,等下一个 tick 的时候再去执行,这样做是对的。但是有没有发现,另一个问题出现了?
因为 number 执行 ++ 操作以后对应的 Watcher 对象都是同一个,我们并不需要在下一个 tick 的时候执行 1000 个同样的 Watcher 对象去修改界面,而是只需要执行一个 Watcher 对象,使其将界面上的 0 变成 1000 即可。
那么,我们就需要执行一个过滤的操作,同一个的 Watcher 在同一个 tick 的时候应该只被执行一次,也就是说队列 queue 中不应该出现重复的 Watcher 对象。
- 那么我们给 Watcher 对象起个名字吧~用 id 来标记每一个 Watcher 对象,让他们看起来“不太一样”。
let uid = 0;
class Watcher {
constructor () {
this.id = ++uid;
}
update () {
console.log('watch' + this.id + ' update');
queueWatcher(this);
}
run () {
console.log('watch' + this.id + '视图更新啦~');
}
}
- 将 Watcher 对象自身传递给 queueWatcher 方法。
let has = {};
let queue = [];
let waiting = false;
function queueWatcher(watcher) {
const id = watcher.id;
// 这里会排除id相同的watcher对象
if (has[id] == null) {
has[id] = true;
queue.push(watcher);
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue);
}
}
}
- flushSchedulerQueue
function flushSchedulerQueue () {
let watcher, id;
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
id = watcher.id;
has[id] = null;
// 最终执行watcher的run
watcher.run();
}
waiting = false;
}
- 举例子
let watch1 = new Watcher();
let watch2 = new Watcher();
watch1.update();
watch1.update();
watch2.update();
我们现在 new 了两个 Watcher 对象,因为修改了 data 的数据,所以我们模拟触发了两次 watch1 的 update 以及 一次 watch2 的 update。
假设没有批量【异步更新策略】的话,理论上应该执行 Watcher 对象的 run,那么会打印。
watch1 update
watch1视图更新啦~
watch1 update
watch1视图更新啦~
watch2 update
watch2视图更新啦~
实际上是:
watch1 update
watch1 update
watch2 update
watch1视图更新啦~
watch2视图更新啦~
这就是异步更新策略的效果,相同的 Watcher 对象会在这个过程中被剔除,在下一个 tick 的时候去更新视图,从而达到对我们第一个例子的优化。
number 会被不停地进行 ++ 操作,不断地触发它对应的 Dep 中的 Watcher 对象的 update 方法。然后最终 queue 中因为对相同 id 的 Watcher 对象进行了筛选,从而 queue 中实际上只会存在一个 number 对应的 Watcher 对象。在下一个 tick 的时候(此时 number 已经变成了 1000),触发 Watcher 对象的 run 方法来更新视图,将视图上的 number 从 0 直接变成 1000。
简单化原理代码
let uid = 0;
class Watcher {
constructor () {
// 【1】为每一个响应式数据的watcher对象添加一个唯一的标识
this.id = ++uid;
}
update () {
// 【2】每次data中的属性发生变化的时候就会触发
console.log('watch' + this.id + ' update');
queueWatcher(this);
}
run () {
// 【3】 nextTick最后触发的更新渲染操作
console.log('watch' + this.id + '视图更新啦~');
}
}
let callbacks = [];
let pending = false;
// nexttick
function nextTick (cb) {
// 每次触发nextTick,就会push一个回调进入
callbacks.push(cb);
if (!pending) {
pending = true;
setTimeout(flushCallbacks, 0); // 异步执行
}
}
function flushCallbacks () {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
let has = {};
let queue = [];
let waiting = false;
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;
}
// 每当数据发生变化的时候,就会去判断是否是相同的id
function queueWatcher(watcher) {
const id = watcher.id;
if (has[id] == null) {
has[id] = true;
queue.push(watcher);
if (!waiting) {
waiting = true;
// 统一执行queue队列中的watcher的run
nextTick(flushSchedulerQueue);
}
}
}
// 模拟执行
(function () {
let watch1 = new Watcher();
let watch2 = new Watcher();
watch1.update();
watch1.update();
watch2.update();
})();
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。