一个Vue.nextTick DEMO 引发的学(血)案
上Demo代码
<div id="example">
<div ref="test">{{test}}</div>
<button @click="handleClick">click</button>
</div>
var vm = new Vue({
el: '#example',
data: {
test: 'begin',
},
methods: {
handleClick() {
this.test = 'end';
console.log('1')
setTimeout(() => { // macroTask
console.log('3')
}, 0);
Promise.resolve().then(function() { //microTask
console.log('promise!')
})
this.$nextTick(function () {
console.log('2')
})
}
}
})
点击按钮后控制台打出什么?
这段代码执行的顺序有人说是1、2、promise、3。也有人说是1、promise、2、3。
那到底是什么呢???
以上的demo涉及两块知识1.js事件循环机制 2.vue中nextTick的实现机制 js事件循环机制是我们今天讨论的这篇文章的基础,但不是重点,市面上介绍event loop的文章很多所以提供以下几篇文章大家自行了解下:
developer.mozilla.org/zh-CN/docs/…
www.ruanyifeng.com/blog/2014/1…
什么时候需要用到Vue.nextTick?
移步官方文档api
Vue.nextTick( [callback, context] )
参数:
{Function} [callback]
{Object} [context]
用法:
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
oh my god! 疑问又来了 用法中写到的“下次DOM更新循环结束”是什么时候?
江湖传说vue官方文档有一个神奇的特点,当你解决一个问题后,反过去去原文档里找,总能找的着。但是假如你没找着解决办法的时候,你也别指望在文档里找到办法。这就是把“什么都有,什么都找不到。”发挥到了一定境界。
言归正传: 请看异步更新队列
ok原来如此! Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行DOM的更新(参看深入响应式原理)。那么为了在数据变化之后等待 Vue 完成更新 DOM后 搞些与DOM有关的事情,就可以用Vue.nextTick(callback) 。这样回调函数将在 DOM 更新完成后被调用。换句话说:在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候要用到Vue.nextTick()
Vue.nextTick到底做了啥
我们来看下vue2.4.4中nextTick的实现(关键点源码中写了注释)
/**
* Defer a task to execute it asynchronously.
*/
export const nextTick = (function () {
/*首先声明3个变量,callbacks用来存储所有需要执行的回调函数,
pending用来标志是否正在执行回调函数,
timerFunc用来触发执行回调函数。*/
const callbacks = []
let pending = false
let timerFunc
//声明nextTickHandler函数,这个函数用来执行callbacks里存储的所有回调函数。
function nextTickHandler () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// the nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore if */
//断是否原生支持promise,如果支持,则利用promise来触发执行回调函数;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//立即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
// in problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
//如果支持MutationObserver,则实例化一个观察者对象,
//观察文本节点发生变化时,触发执行所有回调函数。
//每次调用timerFunc时,会对文本节点进行重新赋值
} 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
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
//用MutationObserver绑定该DOM并指定回调函数,
//在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行)
textNode.data = String(counter)
}
} else {
//如果都不支持,则利用setTimeout设置延时为0
// fallback to setTimeout
/* istanbul ignore next */
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
//返回一个queueNextTick函数,用来往callbacks里存入回调函数,这里可以支持回调函数和promise两种形式。
return function queueNextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
//判断如果没有在执行回调函数,则调用timerFunc来触发执行回调函数,从而执行用户传入的代码。
if (!pending) {
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
_resolve = resolve
})
}
}
})()
由此vue2.4.4版本执行后就是 1、2、promise、3 为什么 2 在 promise前面 和事件循环有关了看这段代码
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//立即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
var p = Promise.resolve()
var logError = err => { console.error(err) }
timerFunc = () => {
p.then(nextTickHandler).catch(logError)
if (isIOS) setTimeout(noop)
}
早已在初始化的时候有个resolve 状态的 promise对象了 在执行nextTick当前相当于在本轮事件循环中直接执行了回调函数
大家已经发现我在上文强调了vue的版本号,那么和版本有什么关系呢?这也是为何会有两种执行顺序的原因。
vue2.5+ 中的nextTick (本文以2.5.21为例)
和之前版本有哪些改动?
1.从Vue 2.5+开始,抽出来了一个单独的文件next-tick.js来执行它。
2.microTask与macroTask
源码中有这么一段注释,以及这几个变量
// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
var microTimerFunc;
var macroTimerFunc;
var useMacroTask = false;
在Vue 2.4之前的版本中,nextTick几乎都是基于microTask实现的,但是由于microTask的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,就会导致一些诡异的问题;但是如果全部都改成macroTask,对一些有重绘和动画的场景也会有性能的影响。所以最终nextTick采取的策略是默认走microTask,对于一些DOM的交互事件,如v-on绑定的事件回调处理函数的处理,会强制走macroTask。
具体做法就是,在Vue执行绑定的DOM事件时,默认会给回调的handler函数调用withMacroTask方法做一层包装,它保证整个回调函数的执行过程中,遇到数据状态的改变,这些改变而导致的视图更新(DOM更新)的任务都会被推到macroTask。 看两个函数
function add$1 (
event,
handler,
capture,
passive
) {
handler = withMacroTask(handler);
target$1.addEventListener(
event,
handler,
supportsPassive
? { capture: capture, passive: passive }
: capture
);
}
/**
* Wrap a function so that if any code inside triggers state change,
* the changes are queued using a (macro) task instead of a microtask.
* 包装参数fn,让其使用marcotask
* 这里的fn为我们在事件上绑定的回调函数
*/
function withMacroTask (fn) {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true;
try {
return fn.apply(null, arguments)
} finally {
useMacroTask = false;
}
})
}
其实绑定在onclick上的回调函数是在这个函数内以apply的形式触发的,useMacroTask = true ,这才是很关键的东西 什么时候用到了呢,看源码
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);
}
});
// 标记位,保证之后如果有this.$nextTick之类的操作不会再次执行以下代码
if (!pending) {
pending = true;
//用微任务还是用宏任务,此例中运行到现在为止Vue的选择是用宏任务
// 其实我们可以理解成所有用v-on绑定事件所直接产生的数据变化都是采用宏任务的方式
// 因为我们绑定的回调都经过了withMacroTask的包装,withMacroTask中会使useMacroTask为true
if (useMacroTask) {
macroTimerFunc();
} else {
microTimerFunc();
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
ok,如果 useMacroTask == true 会直接执行macroTimerFunc() 强制走(macro)task
那没强制的时候2.5+版本是怎么做的呢?
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = function () {
setImmediate(flushCallbacks);
};
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = flushCallbacks;
macroTimerFunc = function () {
port.postMessage(1);
};
} else {
/* istanbul ignore next */
macroTimerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
microTimerFunc = function () {
p.then(flushCallbacks);
// in problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) {
setTimeout(noop);
}
};
} else {
// fallback to macro
microTimerFunc = macroTimerFunc;
}
对于macroTask的执行,Vue优先检测是否支持原生setImmediate(高版本IE和Edge支持),不支持的话再去检测是否支持原生MessageChannel,如果还不支持的话为setTimeout(fn, 0)。对于microTask的执行 用的还是Promise。
废弃MutationObserver使用MessageChannel也是2.5以后带来的变化之一
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
新建一个MessageChannel对象,该对象通过port1来检测信息,port2发送信息。通过port2的主动postMessage来触发port1的onmessage事件,进而把回调函数flushCallbacks作为macroTask参与事件循环。 可以看到源码中优先使用了MessageChannel,而不是setTimeout。什么要优先MessageChannel创建macroTask而不是setTimeout? HTML5中规定setTimeout的最小时间延迟是4ms,也就是说理想环境下异步回调最快也是4ms才能触发。
Vue使用这么多函数来模拟异步任务,其目的只有一个,就是让回调异步且尽早调用。而MessageChannel的延迟是小于setTimeout的。
总结
所以回到最初的问题vue2.5+版本执行后就是 1、promise、2、3 ; 而vue2.4.4版本执行后就是 1、2、promise、3
到这里这篇文章也就结束了
所以碰到类似这种问题的时候,最好的办法是查看官方文档,并查阅源码,不能死记概念和顺序,因为标准也是会变的。
欢迎大家指正不足之处。