在执行 initGlobalAPI(Vue) 初始化 Vue 全局 API 中,这么定义 Vue.nextTick。
function initGlobalAPI(Vue) {//...Vue.nextTick = nextTick;
}
可以看出是直接把 nextTick 函数赋值给 Vue.nextTick,就可以了,非常简单。
Vue.prototype.$nextTick = function (fn) {return nextTick(fn, this)
};
可以看出是 vm.$nextTick 内部也是调用 nextTick 函数。
nextTick 函数的作用可以理解为异步执行传入的函数,这里先介绍一下什么是异步执行,从 JS 运行机制说起。
JS 的执行是单线程的,所谓的单线程就是事件任务要排队执行,前一个任务结束,才会执行后一个任务,这就是同步任务,为了避免前一个任务执行了很长时间还没结束,那下一个任务就不能执行的情况,引入了异步任务的概念。JS 运行机制简单来说可以按以下几个步骤。
nextTick 函数异步执行传入的函数,是一个异步任务。异步任务分为两种类型。
主线程的执行过程就是一个 tick,而所有的异步任务都是通过任务队列来一一执行。任务队列中存放的是一个个的任务(task)。规范中规定 task 分为两大类,分别是宏任务(macro task)和微任务 (micro task),并且每个 macro task 结束后,都要清空所有的 micro task。
用一段代码形象介绍 task的执行顺序。
for (macroTask of macroTaskQueue) {handleMacroTask();for (microTask of microTaskQueue) {handleMicroTask(microTask);}
}
在浏览器环境中,
常见的创建 macro task 的方法有
在 nextTick 函数要利用这些方法把通过参数 cb 传入的函数处理成异步任务。
var callbacks = [];
var pending = false;
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();}if (!cb && typeof Promise !== 'undefined') {return new Promise(function(resolve) {_resolve = resolve;})}
}
可以看到在 nextTick 函数中把通过参数 cb 传入的函数,做一下包装然后 push 到 callbacks 数组中。
然后用变量 pending 来保证执行一个事件循环中只执行一次 timerFunc()。
最后执行 if (!cb && typeof Promise !== 'undefined'),判断参数 cb 不存在且浏览器支持 Promise,则返回一个 Promise 类实例化对象。例如 nextTick().then(() => {}),当 _resolve 函数执行,就会执行 then 的逻辑中。
来看一下 timerFunc 函数的定义,先只看用 Promise 创建一个异步执行的 ztimerFunc 函数 。参考 Vue面试题详细解答
var timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {var p = Promise.resolve();timerFunc = function() {p.then(flushCallbacks);if (isIOS) {setTimeout(noop);}};
}
在其中发现 timerFunc 函数就是用各种异步执行的方法调用 flushCallbacks 函数。
来看一下flushCallbacks 函数
var callbacks = [];
var pending = false;
function flushCallbacks() {pending = false;var copies = callbacks.slice(0);callbacks.length = 0;for (var i = 0; i < copies.length; i++) {copies[i]();}
}
执行 pending = false 使下个事件循环中能nextTick 函数中调用 timerFunc 函数。
执行 var copies = callbacks.slice(0);callbacks.length = 0; 把要异步执行的函数集合 callbacks 克隆到常量 copies,然后把 callbacks 清空。
然后遍历 copies 执行每一项函数。回到 nextTick 中是把通过参数 cb 传入的函数包装后 push 到 callbacks 集合中。来看一下怎么包装的。
function() {if (cb) {try {cb.call(ctx);} catch (e) {handleError(e, ctx, 'nextTick');}} else if (_resolve) {_resolve(ctx);}
}
逻辑很简单。若参数 cb 有值。在 try 语句中执行 cb.call(ctx) ,参数 ctx 是传入函数的参数。
如果执行失败执行 handleError(e, ctx, 'nextTick')。
若参数 cb 没有值。执行 _resolve(ctx),因为在nextTick 函数中如何参数 cb 没有值,会返回一个 Promise 类实例化对象,那么执行 _resolve(ctx),就会执行 then 的逻辑中。
到这里 nextTice 函数的主线逻辑就很清楚了。定义一个变量 callbacks,把通过参数 cb 传入的函数用一个函数包装一下,在这个中会执行传入的函数,及处理执行失败和参数 cb 不存在的场景,然后 添加到 callbacks。调用 timerFunc 函数,在其中遍历 callbacks 执行每个函数,因为 timerFunc 是一个异步执行的函数,且定义一个变量 pending来保证一个事件循环中只调用一次 timerFunc 函数。这样就实现了 nextTice 函数异步执行传入的函数的作用了。
那么其中的关键还是怎么定义 timerFunc 函数。因为在各浏览器下对创建异步执行函数的方法各不相同,要做兼容处理,下面来介绍一下各种方法。
if (typeof Promise !== 'undefined' && isNative(Promise)) {var p = Promise.resolve();timerFunc = function() {p.then(flushCallbacks);if (isIOS) {setTimeout(noop);}};isUsingMicroTask = true;
}
执行 if (typeof Promise !== 'undefined' && isNative(Promise)) 判断浏览器是否支持 Promise,
其中 typeof Promise 支持的话为 function ,不是 undefined,故该条件满足,这个条件好理解。
来看另一个条件,其中 isNative 方法是如何定义,代码如下。
function isNative(Ctor) {return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
当 Ctor 是函数类型时,执行 /native code/.test(Ctor.toString()),检测函数 toString 之后的字符串中是否带有 native code 片段,那为什么要这么监测。这是因为这里的 toString 是 Function 的一个实例方法,如果是浏览器内置函数调用实例方法 toString 返回的结果是function Promise() { [native code] }。
若浏览器支持,执行 var p = Promise.resolve(),Promise.resolve() 方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。
那么在 timerFunc 函数中执行 p.then(flushCallbacks) 会直接执行 flushCallbacks 函数,在其中会遍历去执行每个 nextTick 传入的函数,因 Promise 是个微任务 (micro task)类型,故这些函数就变成异步执行了。
执行 if (isIOS) { setTimeout(noop)} 来在 IOS 浏览器下添加空的计时器强制刷新微任务队列。
if (!isIE && typeof MutationObserver !== 'undefined' &&(isNative(MutationObserver) ||MutationObserver.toString() === '[object MutationObserverConstructor]')
) {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;
}
MutationObserver() 创建并返回一个新的 MutationObserver 它会在指定的 DOM 发生变化时被调用,IE11浏览器才兼容,故干脆执行 !isIE 排除 IE浏览器。执行 typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) 判断,其原理在上面已介绍过了。执行 MutationObserver.toString() === '[object MutationObserverConstructor]') 这是对 PhantomJS 浏览器 和 iOS 7.x版本浏览器的支持情况进行判断。
执行 var observer = new MutationObserver(flushCallbacks),创建一个新的 MutationObserver 赋值给常量 observer, 并且把 flushCallbacks 作为回到函数传入,当 observer 指定的 DOM 要监听的属性发生变化时会调用 flushCallbacks 函数。
执行 var textNode = document.createTextNode(String(counter)) 创建一个文本节点。
执行 var counter = 1,counter 做文本节点的内容。
执行 observer.observe(textNode, { characterData: true }),调用 MutationObserver 的实例方法 observe 去监听 textNode 文本节点的内容。
这里很巧妙利用 counter = (counter + 1) % 2 ,让 counter 在 1 和 0 之间变化。再执行 textNode.data = String(counter) 把变化的 counter 设置为文本节点的内容。这样 observer 会监测到它所观察的文本节点的内容发生变化,就会调用 flushCallbacks 函数,在其中会遍历去执行每个 nextTick 传入的函数,因 MutationObserver 是个微任务 (micro task)类型,故这些函数就变成异步执行了。
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {timerFunc = function() {setImmediate(flushCallbacks);};
}
setImmediate 只兼容 IE10 以上浏览器,其他浏览器均不兼容。其是个宏任务 (macro task),消耗的资源比较小
timerFunc = function() {setTimeout(flushCallbacks, 0);
}
兼容 IE10 以下的浏览器,创建异步任务,其是个宏任务 (macro task),消耗资源较大。
Vue 历来版本中在 nextTick 函数中实现 timerFunc 的顺序时做了几次调整,直到 2.6+ 版本才稳定下来
第一版的 nextTick 函数中实现 timerFunc 的顺序为 Promise,MutationObserver,setTimeout。
在2.5.0版本中实现 timerFunc 的顺序改为 setImmediate,MessageChannel,setTimeout。
在这个版本把创建微任务的方法都移除,原因是微任务优先级太高了,其中一个 issues 编号为 #6566, 情况如下:
// block 1Expand is True // element 1
// block 2Expand is False // element 2
按正常逻辑 点击 element 1 时,会把 expand 置为 false,block 1 不会显示,而 block 2 会显示,在点击 block 2 ,会把 expand 置为 false,那么 block 1 会显示。
当时实际情况是 点击 element 1 ,只会显示 block 1。这是为什么,什么原因引起这个BUG。Vue 官方是这么解释的
点击事件是宏任务,
上的点击事件触发 nextTick(微任务)上的第一次更新。在事件冒泡到外部div之前处理微任务。在更新过程中,将向外部div添加一个click侦听器。因为DOM结构相同,所以外部div和内部元素都被重用。事件最终到达外部div,触发由第一次更新添加的侦听器,进而触发第二次更新。为了解决这个问题,您可以简单地给两个外部div不同的键,以强制在更新期间替换它们。这将阻止接收冒泡事件。
当然当时官方还是给出了解决方案,把 timerFunc 都改为用创建宏任务的方法实现,其顺序是 setImmediate,MessageChannel,setTimeout,这样 nextTick 是个宏任务。
点击事件是个宏任务,当点击事件执行完后触发的 nextTick(宏任务)上的更新,只会在下一个事件循环中进行,这样其事件冒泡早已执行完毕。就不会出现 BUG 中的情况。
但是过不久,实现 timerFunc 的顺序又改为 Promise,MutationObserver,setImmediate,setTimeout,在任何地方都使用宏任务会产生一些很奇妙的问题,其中代表 issue 编号为 #6813,代码就打出来,可以看这里。
这里有两个关键的控制
v-show="showList" 控制隐藏。初始状态:

当快速拖动网页边框缩小页面宽度时,会先显示下面第一张图,然后快速的隐藏,而不是直接隐藏。

那为出现这种BUG,首先要了解一个概念,UI Render (UI渲染)的执行时机,如下所示:
这个过程也比较好理解,之前执行监听窗口缩放是个宏任务,当窗口大小小于 1000px 时,showList 会变为 flase ,会触发一个 nextTick 执行,而其是个宏任务。在两个宏任务之间,会进行 UI Render ,这时,li 的行内框设置失效,展示为块级框,在之后的 nextTick 这个宏任务执行了,再一次 UI Render 时,ul 的 display 的值切换为 none,列表隐藏。
所以 Vue 觉得用微任务创建的 nextTick 可控性还可以,不像用宏任务创建的 nextTick 会出现不可控场景。
在 2.6 + 版本中采用一个时间戳来解决 #6566 这个BUG,设置一个变量 attachedTimestamp,在执行传入 nextTick 函数中的 flushSchedulerQueue 函数时,执行 currentFlushTimestamp = getNow() 获取一个时间戳赋值给变量 currentFlushTimestamp,然后再监听 DOM 上事件前做个劫持。其在 add 函数中实现。
function add(name, handler, capture, passive) {if (useMicrotaskFix) {var attachedTimestamp = currentFlushTimestamp;var original = handler;handler = original._wrapper = function(e) {if (e.target === e.currentTarget ||e.timeStamp >= attachedTimestamp ||e.timeStamp <= 0 ||e.target.ownerDocument !== document) {return original.apply(this, arguments)}};}target.addEventListener(name,handler,supportsPassive ? {capture: capture,passive: passive} : capture);
}
执行 if (useMicrotaskFix),useMicrotaskFix 在用微任务创建异步执行函数时置为 true。
执行 var attachedTimestamp = currentFlushTimestamp 把 nextTick 回调函数执行时的时间戳赋值给变量 attachedTimestamp,然后执行 if(e.timeStamp >= attachedTimestamp),其中 e.timeStamp DOM 上的事件被触发时的时间戳大于 attachedTimestamp,这个事件才会被执行。
为什么呢,回到 #6566 BUG 中。由于micro task的执行优先级非常高,在 #6566 BUG 中比事件冒泡还要快,就会导致此 BUG 出现。当点击 i标签时触发冒泡事件比 nextTick 的执行还早,那么 e.timeStamp 比 attachedTimestamp 小,如果让冒泡事件执行,就会导致 #6566 BUG,所以只有冒泡事件的触发比 nextTick 的执行晚才会避免此 BUG,故 e.timeStamp 比 attachedTimestamp 大才能执行冒泡事件。