Vue.nextTick 浅析
Vue 的特点之一就是响应式,但数据更新时,DOM 并不会立即更新。当我们有一个业务场景,需要在 DOM 更新之后再执行一段代码时,可以借助nextTick
实现。以下是来自官方文档的介绍:
将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。
具体的使用场景和底层代码实现在后面的段落说明和解释。
用途
Vue.nextTick([callback, context])
与 vm.$nextTick([callback])
前者是全局方法,可以显式指定执行上下文,而后者是实例方法,执行时自动绑定this
到当前实例上。
此外,在 2.1.0 版本还新增了不传入回调的使用方式,这种调用会返回一个 Promise,在 then 的回调执行目标操作即可,如vm.$nextTick().then(cb)
。
以下是一个nextTick
使用例子:
<div id="app">
<button @click="add">add</button>
{{count}}
<ul ref="ul">
<li v-for="item in list">
{{item}}
</li>
</ul>
</div>
new Vue({
el: "#app",
data: {
count: 0,
list: []
},
methods: {
add() {
this.count += 1;
this.list.push(1);
let li = this.$refs.ul.querySelectorAll("li");
li.forEach(item => {
item.style.color = "red";
});
}
}
});
以上的代码,期望在每次新增一个列表项时都使得列表项的字体是红色的,但实际上新增的列表项字体仍是黑色的。尽管data
已经更新,但新增的 li 元素并不立即插入到 DOM 中。如果希望在 DOM 更新后再更新样式,可以在nextTick
的回调中执行更新样式的操作。
new Vue({
el: "#app",
data: {
count: 0,
list: []
},
methods: {
add() {
this.count += 1;
this.list.push(1);
this.$nextTick(() => {
let li = this.$refs.ul.querySelectorAll("li");
li.forEach(item => {
item.style.color = "red";
});
});
}
}
});
解释
数据更新时,并不会立即更新 DOM。如果在更新数据之后的代码执行另一段代码,有可能达不到预想效果。将视图更新后的操作放在nextTick
的回调中执行,其底层通过微任务的方式执行回调,可以保证 DOM 更新后才执行代码。
源码
在/src/core/instance/index.js
,执行方法renderMixin(Vue)
为Vue.prototype
添加了$nextTick
方法。实际在Vue.prototype.$nextTick
中,执行了nextTick(fn, this)
,这也是vm.$nextTick( [callback] )
自动绑定this
到执行上下文的原因。
nextTick
函数在/scr/core/util/next-tick.js
声明。在next-tick.js
内,使用数组callbacks
保存回调函数,pending
表示当前状态,使用函数flushCallbacks
来执行回调队列。在该方法内,先通过slice(0)
保存了回调队列的一个副本,通过设置callbacks.length = 0
清空回调队列,最后使用循环执行在副本里的所有函数。
const callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
接着定义函数marcoTimerFunc
、microTimerFunc
。
先判断是否支持setImmediate
,如果支持,使用setImmediate
执行回调队列;如果不支持,判断是否支持MessageChannel
,支持时,在port1
监听message
,将flushCallbacks
作为回调;如果仍不支持MessageChannel
,使用setTimeout(flushCallbacks, 0)
执行回调队列。不管使用哪种方式,macroTimerFunc
最终目的都是在一个宏任务里执行回调队列。
if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks);
};
} else if (
typeof MessageChannel !== "undefined" &&
(isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === "[object MessageChannelConstructor]")
) {
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = flushCallbacks;
macroTimerFunc = () => {
port.postMessage(1);
};
} else {
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
然后判断是否支持Promise
,支持时,新建一个状态为resolved
的Promise
对象,并在then
回调里执行回调队列,如此,便在一个微任务中执行回调,在 IOS 的 UIWebViews 组件中,尽管能创建一个微任务,但这个队列并不会执行,除非浏览器需要执行其他任务;所以使用setTimeout
添加一个不执行任何操作的回调,使得微任务队列被执行。如果不支持Promise
,使用降级方案,将microTimerFunc
指向macroTimerFunc
。
if (typeof Promise !== "undefined" && isNative(Promise)) {
const p = Promise.resolve();
microTimerFunc = () => {
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;
}
在函数nextTick
内,先将函数cb
使用箭头函数包装起来并添加到回调队列callbacks
,转入的回调cb
会在callbacks
被遍历执行的时候执行。如果没有传入cb
,则是形如this.$nextTick().then(cb)
的使用方式,所以要返回一个fulfilled
的 Promise,在箭头函数内则需要执行resolved
,令返回的 Promise 状态变为fulfilled
。接着判断当前是否正在执行回调,如果不是,将pengding
设置为真。判断回调执行是宏任务还是微任务,分别通过marcoTimerFunc
、microTimerFunc
来触发回调队列。最后,如果,没有传入cb
,则需要创建一个Promise
实例并返回以支持链式调用,并且将_resolve
指向返回 Promise 的resolve
函数。
export function nextTick(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);
}
});
if (!pending) {
pending = true;
if (useMacroTask) {
macroTimerFunc();
} else {
microTimerFunc();
}
}
// $flow-disable-line
if (!cb && typeof Promise !== "undefined") {
return new Promise(resolve => {
_resolve = resolve;
});
}
}
而全局方法Vue.nextTick
在/src/core/global-api/index.js
中声明,是对函数nextTick
的引用,所以使用时可以显式指定执行上下文。
Vue.nextTick = nextTick;
小结
本文关于nextTick
的使用场景和源码做了简单的介绍,如果想深入了解这部分的知识,可以去了解一下微任务mircotask
和宏任务marcotask
。