一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
一、用法
Vue.nextTick( [callback, context] )
复制代码
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
二、为什么需要$nextTick
我们先看一个例子
<template>
<h1 ref="msgRef">{{message}}</h1>
<button @click="handleChangeMsg">点击</button
</template>
<script>
export default {
name: 'App',
data() {
return {
message: '小Y'
}
},
methods: {
handleChangeMsg() {
this.message = 'DanDan'
console.log(222, this.$refs['msgRef'].textContent)
this.$nextTick(() => {
console.log(1111, this.$refs['msgRef'].textContent)
})
}
},
}
</script>
复制代码
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。然后,在下一个的事件循环tick
中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
对于上面例子的问题,我们就清楚了,Vue 在更新 DOM 时是异步执行的,在我们修改完成message的数据后,我们无法马上就拿到最新的DOM,这时我们需要应用nextTick来处理。
三、源码分析
在分析源码前,你需要明白什么是事件循环,如果你还不清楚,可以先了解一下什么是事件循环。
nextTick
的源码在/src/core/util/next-tick.js
中,代码不多就上百行代码,大家可以读一下。
首先我们看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
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
复制代码
我们可以看到在执行nextTick的时候,将回调函数cb追加到callbacks这个数组里面,然后有一个pending,我们看到在外层定义pending为false,false时会去执行timerFunc,我们再看timerFunc。
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
复制代码
对于timerFunc函数,我们看到对他的赋值,有四层判断。我们知道,异步处理会涉及到微任务和宏任务。源码里面我们看到先使用原生的Promise.then
、MutationObserver
和setImmediate
,都不支持的时候最后选择setTimeout
,采用了一个降级的处理将flushCallbacks延迟执行。
对于flushCallbacks,它写的很简单,先是将pending置为false,然后将callbacks复制一份然后去循环执行,将callbacks的长度置为0。回过头我们看pending,它其实就是一个标识,在一个事件循环里面只执行一次timerFunc。
四、$nextTick原理总结
- 首先将调用nextTick的回调添加到callbacks数组中,我们将它看作一个队列。
- 然后有一个标识pending,这个标识的目的就是,在一次事件循环里面,只执行一次timerFunc函数。
- 由于Vue对于DOM的更新是异步的,当我们修改完数据,不能马上拿到最新的DOM,所以我们需要nextTick去处理这些异步,我们自然想到微任务和宏任务去处理。源码里面
Promise.then
、MutationObserver
、setImmediate
和setTimeout
去降级处理去处理,将flushCallbacks函数包裹在timerFunc去执行。