这一篇主要讲讲
nextTick
源码,看看该方法的实现,以及为何能在这个方法里保证拿到DOM
节点。
nextTick
方法在./src/core/util/next-tick.js
,下面为部分源码展示:
nextTick
方法接受两个入参,分别是回调方法cb
和上下文ctx
;- 函数部分逻辑,首先不管是否存在
cb
参数都会往队列推入一个函数,后续任务队列根据cb
参数判断是否调用cb
或者是否执行_resolve(ctx)
修改promise
状态; - 判断
pending
状态是否执行任务 - 最后则是该函数的返回值为一个
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()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
先来说说调用nextTick
的返回值,因为返回值是一个promise
,所以我们可以使用then
的写法或者async/await
的写法,加上使用cb
的写法,存在三种写法。
this.$nextTick(function() {
// do something
})
or
this.$nextTick().then((ctx)=> {
// do something
})
or
await this.$nextTick()
// do something
接下来则是nextTick
里比较重要的方法timerFunc
的实现:
- 优先使用原生
Promise
; - 后使用
MutationObserver
; - 再后使用
setImmediate
; - 最后使用
setTimeout
;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
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)
}
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)
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.
// Techinically 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)
}
}
从代码中isUsingMicroTask
中可以看到只有Promise
、MutationObserver
属于微任务,另外两个则属于宏任务;看到该方法的实现我们就可以知道为什么在nextTick
方法中能保证拿到DOM
。
两种场景的解释:
- 在
vue
第一次初始化的时候,我们在beforeCreated
和created
生命周期里想要使用DOM
则必须使用nextTick
,这是因为初始化的过程属于宏任务,整个函数调用栈未清空,nextTick
的回调属于微任务,所以nextTick
的回调必须在整个初始化结束后才会执行。 - 在修改
data
数据后,又如何保证获取修改后的数据DOM
?修改data
数据实际上是触发组件实例的watcher
执行update
更新,而在update
里面又执行了queueWatcher
,下面👇则是queueWatcher
方法的代码,在代码里面我们可以看到最后实际上也是调用nextTick(flushSchedulerQueue)
。因此,想获取data
修改后的DOM
,调用nextTick
能保证这种任务执行的顺序。
了解watcher
可以看这篇juejin.cn/post/684490…。
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
其实queueWatcher
方法里面的逻辑还告诉了我们另外一个框架知识点:
为什么我们同时修改多个data属性,不会多次更新视图?
在update
方法里,因为最后实际上调用nextTick
执行微任务去更新视图,了解过event loop
机制的应该知道,必须等待当前宏任务的调用栈清空才去执行微任务,这也就是为什么当我们同时修改多个data
属性时候,该判断if (has[id] == null)
防止重复添加更新任务,并且利用了event loop
机制在合适的时机去更新视图。