vm.$nextTick 的作用:
nextTick 接受一个回调函数作为参数,它的作用是将回调函数延迟到下次 DOM 更新周期之后执行。如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise 。
我们在开发过程中会碰到一种场景:当状态更新之后,需要对 DOM 做一些操作,但这时我们获取不到更新后的 DOM,因为还没有重新渲染。这个时候我们就需要用到 nextTick 了。
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
那么,下次 DOM 更新周期是什么时候呢?
在 Vue 中,当状态发生变化的时候,watcher 会得到通知,然后触发虚拟 DOM 的渲染流程。而 watcher 触发渲染这个操作是异步的。Vue 中有一个队列,每当需要渲染的时候,会将 watcher 推送到这个队列中,在下一次事件循环机制中再让 watcher 触发渲染的流程。
Vue 为什么使用异步更新队列?
Vue 使用虚拟 DOM 进行渲染,变化侦测的通知只发送到组件,组件内用到的所有状态的变化都会通知到同一个 watcher,然后虚拟DOM会对整个组件进行比对(diff),并更改 DOM。如果同一轮事件循环中,同一个 watcher 中有两个以上的数据发生变化,watcher 也只会收到一份通知,并等到所有的状态都修改完毕之后,才一次性将整个组件的 DOM 渲染到最新状态。那么,如何实现呢?
Vue 的实现方式是将收到通知的 watcher 实例添加到队列中缓存起来,添加之前先检查该实例是否已经存在,存在,则跳过,不存在,则将该 watcher 实例添加到队列中。然后再下一次事件循环中,Vue 会让队列中的 watcher 触发渲染流程并清空队列。
使用异步更新队列主要是为了提升性能,因为如果在主线程中更新 DOM,循环100次就要更新100次 DOM;但是如果等事件循环完成之后更新DOM,就只需要更新一次。
探探 nextTick 的源码
const callbacks = [] // 存储回调函数
let pending = false // 判断是否需要向任务队列中新增任务
/**
* 当 flushCallbacks 函数被触发的时候,会依次执行
* callbacks 中的函数,并将 callbacks 清空
* 并将 pending 置为 false
* 一次事件循环中 flushCallbacks 只会执行一次
*/
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0 // 清空队列
for (let i = 0; i < copies.length; i++) {
// 遍历队列中的回调函数,并一个个执行
copies[i]()
}
}
let microTimerFunc // 该函数的作用是将 flushCallbacks 添加到微任务队列中
const p = Promise.resolve()
microTimerFunc = () => {
// 将 flushCallbacks 添加到微任务队列中
p.then(flushCallbacks)
}
export function nextTick(cb, ctx) {
// 如果是本轮事件循环第一次调用(pending为false)
// 则需要向任务队列中添加任务 microTimerFunc
callbacks.push(() => {
if (cb) {
cb.call(ctx)
}
})
if (!pending) {
pending = true // 避免重复调用
microTimerFunc()
}
}
microTimerFunc 的实现原理是使用 Promise.then,但并不是所有的浏览器都支持 Promise。当不支持的时候,会降级成 macroTimerFunc。
let macroTimerFunc // 宏任务队列函数
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
}
} else {
microTimerFunc = macroTimerFunc
}
如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。比如,可以这样使用 vm.$nextTick
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
实现方式如下:
export function nextTick(cb, ctx) {
// 如果是本轮事件循环第一次调用(pending为false)
// 则需要向任务队列中添加任务 microTimerFunc
let _resolve
callbacks.push(() => {
if (cb) {
cb.call(ctx)
} else {
// 没有提供回调函数
_resolve(ctx)
}
})
if (!pending) {
pending = true // 避免重复调用
microTimerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve) => {
_resolve = resolve
})
}
}
前面提到的 macroTimerFunc 是什么呢?微任务的优先级太高,在某些场景下可能会出问题,所以 Vue 提供了可以在特殊场合强制使用宏任务的方法。
let macroTimerFunc // 宏任务队列函数
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 优先使用 setImmediate,但它只能在IE中使用
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) || MessageChannel.toString === '[object MessageChannelConstructor]')) {
// 备选方案
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () {
port.postMessage(1)
}
} else {
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
HTML5 中规定 setTimeout 的最小时间延迟是4ms,也就是说理想环境下异步回调最快也是4ms才能触发。而 MessageChannel 和 setImmediate 的延迟明显是小于setTimeout 的,优先使用 MessageChannel 和 setImmediate 可以让异步回调且尽早调用。。
下面来看看最终完整的代码
const callbacks = [] // 存储回调函数
let pending = false // 判断是否需要向任务队列中新增任务
/**
* 当 flushCallbacks 函数被触发的时候,会依次执行
* callbacks 中的函数,并将 callbacks 清空
* 并将 pending 置为 false
* 一次事件循环中 flushCallbacks 只会执行一次
*/
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0 // 清空队列
for (let i = 0; i < copies.length; i++) {
// 遍历队列中的回调函数,并一个个执行
copies[i]()
}
}
let microTimerFunc // 该函数的作用是将 flushCallbacks 添加到微任务队列中
let macroTimerFunc // 该函数的作用是将 flushCallbacks 添加到宏任务队列中
let useMacroTask = false // 优先使用微任务
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 优先使用 setImmediate,但它只能在IE中使用
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) || MessageChannel.toString === '[object MessageChannelConstructor]')) {
// 备选方案
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () {
port.postMessage(1)
}
} else {
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
// 将 flushCallbacks 添加到微任务队列中
p.then(flushCallbacks)
}
} else {
microTimerFunc = macroTimerFunc
}
export function withMacroTask (fn) {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
export function nextTick(cb, ctx) {
// 如果是本轮事件循环第一次调用(pending为false)
// 则需要向任务队列中添加任务 microTimerFunc
let _resolve
callbacks.push(() => {
if (cb) {
cb.call(ctx)
} else {
// 没有提供回调函数
_resolve(ctx)
}
})
if (!pending) {
pending = true // 避免重复调用
if (useMacroTask) {
// 使用宏队列
macroTimerFunc()
} else {
// 使用微队列
microTimerFunc()
}
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve) => {
_resolve = resolve
})
}
}
以上内容是刘博文的《深入浅出Vue.js 》的读书笔记~