nextTick()的解释:
前情提要:在vue中修改状态时,DOM更新不是同步的。vue会把所有数据变更缓存到一个队列中,到下一次触发时间的时候才执行。
好处是:可以将多次数据更新合并成一次,减少DOM的更新次数,提高性能。
nextTick():
接受一个回调函数,这个回调函数在数据修改之后立即使用。可以确保传入的回调函数会在数据更新完成之后执行。
nextTick()的源码分析:
实现原理:
nextTick中维护了一个回调函数队列,一个pending,一个timeFunc()。
pending的作用:用来标识同一个时间只执行一次。
timeFunc的作用: 选择当前环境优先支持的异步方法,多次调用nextTick函数中,timeFunc只会执行一次。
步骤:
(1)将回调函数放入回调函数队列中等待
(2)如果是第一次使用nextTick函数,会使用timeFunc判断一下当前环境中支持的异步方式。
(3)将执行任务放入微任务或者任务中。事件循环到任务时,依次执行回调函数队列中的函数。
// 标记 nextTick 最终是否以微任务执行
export let isUsingMicroTask = false
// 回调函数队列,保存使用 nextTick 时传入的回调函数
const callbacks = []
// 用来标记任务队列中是否含有任务,如果有,就执行timeFunc函数;
let pending = false
// 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
// 回调的 this 自动绑定到调用它的实例上
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)
}
})
// timeFunc 函数选择当前环境优先支持的异步方法
if (!pending) {
pending = true
timerFunc()
}
// 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
// 在返回的这个 promise.then 中 DOM 已经更新好了
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
// 判断当前环境优先支持的异步方法,优先选择微任务
// 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
let timerFunc
// 判断当前环境是否原生支持 promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
// 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
// 这里的 setTimeout 是用来强制刷新微任务队列的
// 因为在 ios 下 promise.then 后面没有宏任务的话,微任务队列不会刷新
}
// 标记当前 nextTick 使用的微任务
isUsingMicroTask = true
// 如果不支持 promise,就判断是否支持 MutationObserver
// 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
// new 一个 MutationObserver 类
const observer = new MutationObserver(flushCallbacks)
// 创建一个文本节点
const textNode = document.createTextNode(String(counter))
// 监听这个文本节点,当数据发生变化就执行 flushCallbacks
observer.observe(textNode, { characterData: true })
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter) // 数据更新
}
isUsingMicroTask = true // 标记当前 nextTick 使用的微任务
// 判断当前环境是否原生支持 setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => { setImmediate(flushCallbacks) }
} else {
// 以上三种都不支持就选择 setTimeout
timerFunc = () => { setTimeout(flushCallbacks, 0) }
}
// 如果多次调用 nextTick,会依次执行上面的方法,将 nextTick 的回调放在 callbacks 数组中
// 最后通过 flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0) // 拷贝一份 callbacks
callbacks.length = 0 // 清空 callbacks
for (let i = 0; i < copies.length; i++) { // 遍历执行传入的回调
copies[i]()
}
}