Vue.js 内部运行机制(六)---- 批量异步更新策略及 nextTick 原理

之前我们学到了 Vue 更新数据是如何更新视图的。

简单回顾

数据更新(setter)-> 通知依赖收集集合(Dep) -> 调用所有观察者(Watcher) -> 比对节点树(patch) -> 视图

在更新视图这一步,使用异步更新策略

为什么呢?引用小册中的例子,下面有一个这样的 Vue 组件


在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 原理》

参考文章

《批量异步更新策略及 nextTick 原理》

你可能感兴趣的:(JS,Vue,随记)