浅析-$nextTick

574 阅读3分钟

源码

Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
};

function noop(a, b, c) {};

function flushCallbacks() {
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for (var i = 0; i < copies.length; i++) {
        copies[i]();
    }
}

var isUsingMicroTask = false;
var callbacks = [];
var pending = false;
var timerFunc;

if (typeof Promise !== 'undefined' && isNative(Promise)) {
    var p = Promise.resolve();
    timerFunc = function () {
        p.then(flushCallbacks);
        if (isIOS) {
            setTimeout(noop);
        }
    };
    isUsingMicroTask = true;
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
        isNative(MutationObserver) ||
        // PhantomJS and iOS 7.x
        MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) {
    // Use MutationObserver where native Promise is not available,
    // e.g. PhantomJS, iOS7, Android 4.4
    // (#6466 MutationObserver is unreliable in IE11)
    var counter = 1;
    var observer = new MutationObserver(flushCallbacks);
    var textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
        characterData: true
    });
    timerFunc = function () {
        counter = (counter + 1) % 2;
        textNode.data = String(counter);
    };
    isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    // Fallback to setImmediate.
    // Technically it leverages the (macro) task queue,
    // but it is still a better choice than setTimeout.
    timerFunc = function () {
        setImmediate(flushCallbacks);
    };
} else {
    // Fallback to setTimeout.
    timerFunc = function () {
        setTimeout(flushCallbacks, 0);
    };
}

function nextTick(cb, ctx) {
    var _resolve;
    callbacks.push(function () {
        if (cb) {
            try {
                cb.call(ctx);
            } catch (e) {
                handleError(e, ctx, 'nextTick');
            }
        } else if (_resolve) {
            _resolve(ctx);
        }
    });

    if (!pending) {
        pending = true;
        timerFunc();
    }
    
    // $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(function (resolve) {
            _resolve = resolve;
        })
    }
}

解析:

在使用$nextTick之前,Vue首先会去检测运行环境是否支持Promise、MutationObserver、setImmediate,这么做的目的是为后面添加异步事件做准备。

我们看到isUsingMicroTask这个变量初始化为false,当检测到运行环境支持Promise || MutationObserver时,把isUsingMicroTask置为了true,这么做是因为Promise || MutationObserver产生的都是微任务(MicroTask),而setImmediate || setTimeout产生的都是宏任务(MacroTask)。

当我们调用通过Vue实例调用$nextTick时,其实就是调用Vue.prototype.$nextTick,得到nextTick函数的返回值!

初始化pending,执行nextTick函数,会在callbacks回调队列中添加一个方法,该方法执行时,会调用在$nextTick被调用时传入的回调函数,并且为这个回调函数绑定了上下文(当前实例)。

接下来会去判断pending是否为false,如果是false,那么把pending置为true,执行timerFunc函数。我们以浏览器支持Promise为例,

timerFunc = function () {
    p.then(flushCallbacks);
    if (isIOS) {
        setTimeout(noop);
    }
};

那么执行timerFunc函数,会去调用p.then(),此时就产生了一个微任务flushCallbacks。

假如事件循环执行到了这个微任务,

function flushCallbacks() {
    pending = false;
    var copies = callbacks.slice(0);
    callbacks.length = 0;
    for (var i = 0; i < copies.length; i++) {
        copies[i]();
    }
}

那么它首先把pending又置为了false,并且依次执行回调队列callbacks中所有的函数,也就是依次之前我们调用$nextTick设置的回调函数,这也就是$nextTick异步执行的原因。

当我们在调用$nextTick时没有设置回调函数,并且浏览器支持Promise,那么此时的$nextTick就是一个Promise实例,因此可以:

this.$nextTick()
.then(function(){
    
});

为什么$nextTick是异步执行的原因我们分析过了,现在回过头来思考一个问题,那就是为什么在nextTick函数在执行的时候,回去判断pending是否为false,如果为false,又为什么置为true呢?

假如此时,我们多次连续的调用$nextTick,那么nextTick函数就会执行多次。如果我们不设置pending这个状态直接调用timerFunc函数,就会产生多个微任务,当上一个微任务执行完,然后去执行这个微任务时,发现callbacks已经是空的,没有回调函数需要执行,那这其实是多余了,而且浪费性能!

最好的办法是只产生一个微任务,一个微任务里面执行当前所有回调。

事实也正是如此,第一次调用nextTick时,pending为false,然后将其置为true调,并用timerFunc产生一个微任务。假如此时这个微任务还没执行,那么在第二次调用nextTick时,pending为true,那我们仅仅是在回调队列callbacks添加一个回调函数,但不会再产生新的微任务。只有在上一个微任务已经执行callbacks为空时,再次调用nextTick,才会产生新的微任务。

异步更新队列

Vue在异步更新队列中也提到过,

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

Vue的这种类似异步队列的思想也不是第一次出现,就像我们之前介绍过的click-outside-x这个插件,也是产生了一个事件处理程序的队列。我们都知道用户的操作其实是产生了一个宏任务,事件处理程序的队列思想,让每种事件类型只会在document绑定一个监听事件,去循环执行事件处理程序,而不会产生多个宏任务,也是很好的提升了性能,类似的思想在jQuery上面也很常见!