nextTick是Vue的一个核心功能,在Vue内部实现中也常常用到nextTick。可是,不少新手不理解nextTick的原理,甚至不清楚nextTick的做用。浏览器
那么,咱们就先来看看nextTick是什么。dom
看看官方文档的描述:异步
在下次 DOM 更新循环结束以后执行延迟回调。在修改数据以后当即使用这个方法,获取更新后的 DOM。ide
再看看官方示例:函数
// 修改数据
vm.msg = 'Hello'
// DOM 尚未更新
Vue.nextTick(function () {
// DOM 更新了
})
// 做为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
复制代码
2.1.0 起新增:若是没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。请注意 Vue 不自带 Promise 的 polyfill,因此若是你的目标浏览器不原生支持 Promise (IE:大家都看我干吗),你得本身提供 polyfill。oop
能够看到,nextTick主要功能就是改变数据后让回调函数做用于dom更新后。不少人一看到这里就懵逼了,为何须要在dom更新后再执行回调函数,我修改了数据后,不是dom自动就更新了吗?源码分析
这个和JS中的Event Loop有关,网上教程不可胜数,在此就再也不赘述了。建议明白Event Loop后再继续向下阅读本文。post
举个实际的例子:测试
咱们有个带有分页器的表格,每次翻页须要选中第一项。正常状况下,咱们想的是点击翻页器,向后台获取数据,更新表格数据,操纵表格API选中第一项。idea
可是,你会发现,表格数据是更新了,可是并无选中第一项。由于,你选中第一项时,虽然数据更新了,可是DOM并无更新。此时,你可使用nextTick,在DOM更新后再操纵表格第一项的选中。
那么,nextTick到底作了什么了才能实如今DOM更新后执行回调函数?
nextTick的源码位于src/core/util/next-tick.js,总计118行,十分的短小精悍,十分适合初次阅读源码的同窗。
nextTick源码主要分为两块:
1.能力检测
2.根据能力检测以不一样方式执行回调队列
这一块其实很简单,众所周知,Event Loop分为宏任务(macro task)以及微任务( micro task),无论执行宏任务仍是微任务,完成后都会进入下一个tick,并在两个tick之间执行UI渲染。
可是,宏任务耗费的时间是大于微任务的,因此在浏览器支持的状况下,优先使用微任务。若是浏览器不支持微任务,使用宏任务;可是,各类宏任务之间也有效率的不一样,须要根据浏览器的支持状况,使用不一样的宏任务。
nextTick在能力检测这一块,就是遵循的这种思想。
// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
// 若是浏览器不支持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)
}
}
复制代码
首先,检测浏览器是否支持setImmediate,不支持就使用MessageChannel,再不支持只能使用效率最差可是兼容性最好的setTimeout了。
以后,检测浏览器是否支持Promise,若是支持,则使用Promise来执行回调函数队列,毕竟微任务速度大于宏任务。若是不支持的话,就只能使用宏任务来执行回调函数队列。
执行回调函数队列的代码恰好在一头一尾
// 回调函数队列
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源码中使用了一个异步锁的概念,即接收第一个回调函数时,先关上锁,执行异步方法。此时,浏览器处于等待执行完同步代码就执行异步代码的状况。
打个比喻:至关于一群旅客准备上车,当第一个旅客上车的时候,车开始发动,准备出发,等到全部旅客都上车后,就能够正式开车了。
固然执行flushCallbacks函数时有个难以理解的点,即:为何须要备份回调函数队列?执行的也是备份的回调函数队列?
由于,会出现这么一种状况:nextTick套用nextTick。若是flushCallbacks不作特殊处理,直接循环执行回调函数,会致使里面nextTick中的回调函数会进入回调队列。这就至关于,下一个班车的旅客上了上一个班车。
说了这么多,咱们来实现一个简单的nextTick:
let callbacks = []
let pending = false
function nextTick (cb) {
callbacks.push(cb)
if (!pending) {
pending = true
setTimeout(flushCallback, 0)
}
}
function flushCallback () {
pending = false
let copies = callbacks.slice()
callbacks.length = 0
copies.forEach(copy => {
copy()
})
}
复制代码
能够看到,在简易版的nextTick中,经过nextTick接收回调函数,经过setTimeout来异步执行回调函数。经过这种方式,能够实如今下一个tick中执行回调函数,即在UI从新渲染后执行回调函数。