在日常开发中,偶尔会有这样的一种场景,翻看代码逻辑以及书写顺序都没有什么问题(至少作为新手的我是这么认为的),然后随手在没有实现预期效果的代码前面加上await setTimeout(100ms)或者await NextTick问题就解决了。
上述场景大多数出现于监听某个dom是否渲染完成,那么为什么会出现这种情况呢?为什么加个定时器或者NextTick方法就解决了呢?接下来就一起揭开它们的神秘面纱吧~
mounted
相信大家对这个生命周期不是很陌生,当Vue实例被挂载完成后触发该钩子函数。在官方文档中对这个钩子函数的描述还附加了一条注意点。
nextTick又被点名了,那就继续往下看吧。
NextTick简介
Defer the callback to be executed after the next DOM update cycle. Use it immediately after you’ve changed some data to wait for the DOM update.
--将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。
源码
vue2.x
/* @flow */
/* globals MutationObserver */
// 中文注释为本人个人见解,仅供参考,如有误导请见谅。英文注释为源码注释。
/**
* Perform no operation.
* Stubbing args to make Flow happy without leaving useless transpiled code
* with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
* export function noop (a?: any, b?: any, c?: any) {}
*/
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks = []
let pending = false
// 备份callbacks数组并依次执行每个cb
function flushCallbacks () {
pending = false
// slice return 一个新的数组
// 为什么要新建一个备份数组?
const copies = callbacks.slice(0)
callbacks.length = 0
// 循环遍历,按照 队列 数据结构 “先进先出” 的原则,逐一执行所有 callback 。
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
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #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 (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc
// 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 next, $flow-disable-line */
// 检测Promise是否为原生func(检测兼容性)
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 和 new 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.
// 个人理解:部分场景Promise没有办法完整执行完毕,加一个空的定时器即可解决该问题
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
}
// 由于MutationObserver为微任务,且兼容性比Promise强,但是处理比较繁琐(猜的),所以它放在Promise的后面。
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
//MutationObserver - Web API 接口参考 | MDN
//创建并返回一个新的 MutationObserver 它会在指定的DOM发生变化时被调用。
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
//https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserverInit
//设为 true 以监视指定目标节点或子节点树中节点所包含的字符数据的变化。无默认值。
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
}
//宏任务,兼容性极差,但是由于SetTimeOut不设置time也会有默认的4ms缺省值,故放在其前
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.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
})
}
}
vue3.x
export function nextTick(
this: ComponentPublicInstance | void,
fn?: () => void
): Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
Q&A
为什么要新建一个备份数组?
由于callbacks为全局变量,只要调用一次NextTick函数就会对callbacks进行push操作,当nextTick嵌套使用时,flushCallbacks通过拷贝并清空原数组的方式可以保证每次循环执行的都是当前nextTick的cb入参。
Const p = promise.resolve() 和 const p = new Promise(r=>r())的区别?
为什么选择先微任务后宏任务的降级处理?
优先选择微任务原因是当使用nextTick时可以确保队列中的微任务能在一次事件循环前执行完毕,如果选择宏任务的话,则需要等待当前任务队列执行完毕后在下一次事件循环中才能执行nextTick中的callback。
参考文献
developer.mozilla.org/zh-CN/docs/…
developer.mozilla.org/zh-CN/docs/…