2

批量更新和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 中创建一个事件 flushCallbacksflushCallbacks 则会在执行时将 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();
})();

Meils
1.6k 声望157 粉丝

前端开发实践者