原理
通过异步队列控制DOM更新和nextTick回调函数先后执行的方式。
作用
Vue中的nextTick用于延迟执行一段代码,它接受2个参数(回调函数和执行回调函数的上下文环境),如果没有提供回调函数,那么将返回promise对象。
简单来说就是在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构,这个操作都应该放进Vue.nextTick()的回调函数中。
this.$nextTick(_=>{
this.$refs.saveTagInput.4resf.input.focus()
});
应用场景
-
在Vue生命周期的
created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中 -
在
created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted()钩子函数,因为该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题 。 -
在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进
Vue.nextTick()的回调函数中。
源码解析
基础检测
Event Loop分为宏任务(macro task)以及微任务( micro task),不管执行宏任务还是微任务,完成后都会进入下一个tick,并在两个tick之间执行UI渲染检测
// 如果浏览器不支持Promise,使用宏任务来执行nextTick回调函数队列
// 能力检测,测试浏览器是否支持原生的setImmediate(setImmediate只在IE中有效)
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 如果支持,宏任务( macro task)使用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 */
// 都不支持的情况下,使用setTimeout
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
执行回调函数队列
// 回调函数队列
const callbacks = []
// 异步锁
let pending = false
// 执行回调函数
function flushCallbacks () {
// 重置异步锁
pending = false
// 防止出现nextTick中包含nextTick时出现问题,在执行回调函数队列前,提前复制备份,清空回调函数队列
const copies = callbacks.slice(0)
callbacks.length = 0
// 执行回调函数队列
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
...
// 我们调用的nextTick函数
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
// 2.1.0新增,如果没有提供回调,并且支持Promise,返回一个Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
实现nextTick
通过nextTick接收回调函数,通过setTimeout来异步执行回调函数。通过这种方式,可以实现在下一个tick中执行回调函数,即在UI重新渲染后执行回调函数。
三个重要参数 :
callbacks
用来存储所有需要执行的回调函数
pending
用来标志是否正在执行回调函数
timerFunc
用来触发执行回调函数
const callbacks = []
let pending = false
let timerFunc
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
})
}
}
总结
- 先定义了一个callbacks存放所有的nextTick里的回调函数
- 然后判断一下当前的浏览器内核是否支持Promise,支持就用Promise来触发回调函数
- 如果不支持Promise,再判断是否支持MutationObserver(一个可以监听DOM结构变化的接口,观察文本节点发生变化时,触发执行所有回调函数)
- 如果不支持MutationObserver,再判断是否支持setImmediate
- 如果以上都不支持就只能用setTimeout来完成异步执行了
- 延迟调用优先级如下:Promise > MutationObserver > setImmediate > setTimeout