1. $nextTick
的作用是什么?
$nextTick
的作用是将回调延迟到下次DOM更新周期
之后执行。
new Vue({
// ...
methods:{
example: function(){
= 'change';
this.$nextTick(function(){
// DOM更新了,可以拿到最新的DOM
})
}
}
})
2.为什么要在$nextTick
中才能拿到数据改变后的最新DOM?
这时由于采用了异步更新队列
来进行更新DOM。我们知道 2.0开始使用虚拟DOM进行渲染,变化侦测的通知只发送到组件,组件内用到的所有状态都会通知到同一个watcher,然后虚拟DOM会对整个组件进行对比并更新DOM。也就是说,如果在同一轮事件循环中有两个数据发生了变化,那么组件的watcher会受到两份通知,从而进行两次渲染。事实上,并不需要渲染两次,虚拟DOM会对整个组件进行渲染,所以只需要等所有状态都修改完毕后,一次性将整个组件的DOM渲染到最新即可。
要解决整个问题,的实现方式是将受到通知的watcher实例添加到队列中缓存起来,并且在添加到队列之前检查其中是否已经存在相同的watcher,只有不存在时,才将watcher实例添加到队列中。然后在下一次事件循环
中,会让队列中的watcher触发渲染流程并清空队列。这样就可以即便在同一事件中有两个状态发生改变,watcher最后也执行一次渲染流程。
下次DOM更新周期
的意思是下次微任务执行时更新DOM
,而vm.$nextTick
其实是将回调添加到微任务中(只有在特殊情况下才降级到宏任务)
,所以我们要在$nextTick中才能拿到数据改变后的最新DOM。
3. $nextTick的原理
3.1. 一次事件循环中多次调动$nextTick只会将回调添加到任务队列中一次
$nextTick
将回调延迟到下次DOM更新周期之后执行,但是在一次事件循环中多次调动$nextTick,Vue只会将回到添加到任务队列中一次。vue中使用pending来判断是否已经将回到添加到任务队列中,
const callbacks = [] // 用来存储vm.$nextTick参数中提供的回调
let pending = false // 标记是否已经向任务队列中添加了一个任务(每当向任务队列中插入任务时,将pending设置为true,每当任务被执行时将pending设置为false)
// 被注册的任务
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0 // 清除callbacks
for (let i = 0; i < copies.length; i++) {
copies[i]() // 依次触发
}
}
const p = Promise.resolve() // 微任务队列
timerFunc = () => {
p.then(flushCallbacks)
}
export function nextTick (cb?: Function, ctx?: Object) {
// 将回调函数添加到callbacks中
callbacks.push(() => {
if (cb) {
cb.call(ctx)
})
// 只推一次(在一次事件循环中调用了两次nextTick,只有第一次才会将任务推进任务队列,第二次只会改变callbacks,因为中执行的是callbacks)
if (!pending) {
pending = true // 标记已经推进
timerFunc()
}
}
在上面的代码中我们可以看到,当第一次调用$nextTick
时,我们会将回调函数注册到callbacks队列中,同时会调用timerFunc将flushCallbacks使用Promise微任务进行包装,这时候相当于将flushCallbacks添加到微任务任务队列中,同时将pending设置为true。在同一次事件循环中再次调用$nextTick时,pending为true,将不会再次执行timerFunc,也就是不会再将flushCallbacks添加到微任务任务队列中。但是回调函数会注册到callbacks队列中。由于callbacks是引用类型,所以这两次的回调函数在flushCallbacks中都会被一一触发。
3.2 $nextTick添加到任务队列的类型
在$nextTick优先使用微任务(Promise)进行包装,但是如果Promise不支持的情况下,将会降级使用宏任务(setImmediate、setTimeout)
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve() // 微任务队列
timerFunc = () => {
p.then(flushCallbacks)
}
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 宏任务
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 宏任务
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
在上面的代码中我们可以看到Vue在$nextTick
使用任务队列优先级。
3.3 使用$nextTick没有指定回调函数的情况
当使用$nextTick
没有指定回调函数并且支持Promise
时,$nextTick将会返回一个Promise
,同时将当前实例传出去。
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
timerFunc()
}
/**
* 如果没有提供回调且在支持Promise的环境中,则返回一个Promise
* this.$nextTick().then((ctx)=>{
* // dom更新了
* })
*/
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
在以上代码中,我们先定义_resolve,当没有传入回调函数并且Promise部位空的情况下,将返回一个Promise,同时将传入的回调函数赋值给_resolve,并将ctx(当前实例)传出去。
3.4 完整代码
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false // 是否使用微任务
const callbacks = [] // 用来存储vm.$nextTick参数中提供的回调
let pending = false // 标记是否已经向任务队列中添加了一个任务(每当向任务队列中插入任务时,将pending设置为true,每当任务被执行时将pending设置为false)
// 被注册的任务:将callbacks中的所有函数依次执行(一轮事件循环中flushCallbacks只会执行一次)
function flushCallbacks () {
pending = false // 清除
const copies = callbacks.slice(0)
callbacks.length = 0 // 清除callbacks
for (let i = 0; i < copies.length; i++) {
copies[i]() // 依次触发
}
}
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc // 作用是将flushCallbacks添加到异步任务队列中(微任务或宏任务)
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native 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 next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve() // 微任务队列
timerFunc = () => {
p.then(flushCallbacks)
// In problematic UIWebViews, 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, . handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// . PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
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 = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
// 宏任务
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 将回调函数添加到callbacks中
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 只推一次(在一次事件循环中调用了两次nextTick,只有第一次才会将任务推进任务队列,第二次只会改变callbacks,因为中执行的是callbacks)
if (!pending) {
pending = true // 标记已经推进
timerFunc()
}
/**
* 如果没有提供回调且在支持Promise的环境中,则返回一个Promise
* this.$nextTick().then((ctx)=>{
* // dom更新了
* })
*/
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}