一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第13天,点击查看活动详情。
nextTick是做什么的?
nextTick
可以让我们在下次 DOM
更新循环结束之后执行延迟回调,用于获得更新后的 DOM
为什么需要nextTick?其使用场景是什么?
了解nextTick之前,我们需要先了解一下Vue的异步更新策略。
Vue的异步更新策略:
当数据变化时Vue不会立刻更新DOM,而是开启⼀个队列,把组件更新函 数保存在队列中,在同⼀事件循环中发生的所有数据变更会异步的批量更新。这⼀策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick。
使用场景:
例如在事件处理逻辑时希望获取更新后的dom状态,示例:
<template>
<div>
<h1 ref="msgRef">{{msg}}</h1>
<button @click="setMsg">修改</button>
</div>
</template>
<script>
export default {
data() {
return {
msg: '123'
};
},
methods: {
setMsg() {
this.msg = '设置的内容'
console.log(this.$refs['msgRef'].innerHTML) // 第一个输出
this.$nextTick(function() {
console.log(this.$refs['msgRef'].innerHTML) // 第二个输出
})
}
},
};
</script>
点击按钮后,第一个输出123
,第二个输出设置的内容
。
nextTick
签名如下: function nextTick(callback?: () => void): Promise
所以我们只需要在传入的回调函数中访问最新DOM状态即可,或者我们可以await nextTick⽅法返回的 Promise之后做这件事。
在Vue内部,nextTick
之所以能够让我们看到DOM更新后的结果,是因为我们传⼊的callback会被添加到队列 刷新函数(flushSchedulerQueue)的后⾯,这样等队列内部的更新函数都执⾏之后,所有DOM操作也就结束 了,callback⾃然能够获取到最新的DOM值。
原理
nextTick
是 Vue 的一个核心实现,在介绍 Vue 的 nextTick 之前,为了方便大家理解,我先简单介绍一下 JS 的单线程及事件循环机制。
js主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。然后去循环这个过程(宏任务->清空微任务->宏任务->清空微任务......)
nextTick
主要使用了宏任务和微任务的原理。根据执行环境分别尝试采用
Promise
MutationObserver
setImmediate
- 如果以上都不行则采用
setTimeout
在源码 src/core/util/next-tick.js
中:
next-tick.js
申明了microTimerFunc
和macroTimerFunc
2 个变量,它们分别对应的是 micro task 的函数和 macro task 的函数。对于 macro task 的实现,优先检测是否支持原生setImmediate
,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的MessageChannel
,如果也不支持的话就会降级为setTimeout 0
;而对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现。next-tick.js
对外暴露了 2 个函数,先来看nextTick
,这就是我们在上一节执行nextTick(flushSchedulerQueue)
所用到的函数。它的逻辑也很简单,把传入的回调函数cb
压入callbacks
数组,最后一次性地根据useMacroTask
条件执行macroTimerFunc
或者是microTimerFunc
,而它们都会在下一个 tick 执行flushCallbacks
,flushCallbacks
的逻辑非常简单,对callbacks
遍历,然后执行相应的回调函数。- 这里使用
callbacks
而不是直接在nextTick
中执行回调函数的原因是保证在同一个 tick 内多次执行nextTick
,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕。
源码直达:
在源码中定义了一个异步方法,多次调用
nextTick
会将方法存入队列中,通过这个异步方法清空当前队列
- 在
Vue 2.4
之前都是使用的microtasks
微任务,但是microtasks
的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果都使用macrotasks
又可能会出现渲染的性能问题。所以在新版本中,会默认使用microtasks
,但在特殊情况下会使用macrotasks
,比如v-on
。 - 对于实现
macrotasks
,会先判断是否能使用setImmediate
,不能的话降级为MessageChannel
,以上都不行的话就使用setTimeout
如何判断能不能使用相应的API
:
if (typeof setImmediate !== 'undefined' && isNative(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 {
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
总结:
-
新版本中默认是
mincrotasks
,v-on
中会使用macrotasks
-
macrotasks
宏任务的实现:setImmediate / MessageChannel / setTimeout