在 Vue 官网上,对于 nextTick 的说法是这样的:
问题入手,灵魂五问
相信大家大部分都会用,那我提几个问题供大家思考一下:
- 如果以下代码,打印出的是什么?
<template>
<div class="page">
<input ref="iref" type="text" v-if="visible">
<button @click="updateTitle">Click</button>
</div>
</template>
<script>
import { nextTick } from 'vue'
export default {
data() {
return {
visible: false,
}
},
methods: {
updateTitle() {
this.visible = true;
nextTick(() => {
console.log(this.$refs.iref);
});
}
}
}
</script>
显而易见,正确答案是:
- 如果以下代码,打印出的是什么?
<template>
<div class="page">
<input ref="iref" type="text" v-if="visible">
<button @click="updateTitle">Click</button>
</div>
</template>
<script>
import { nextTick } from 'vue'
export default {
data() {
return {
visible: false,
}
},
methods: {
updateTitle() {
nextTick(() => {
console.log(this.$refs.iref);
});
this.visible = true;
}
}
}
</script>
答案也显而易见,正确答案是:
- 上点难度了!如果以下代码,打印出的是什么?
<template>
<div class="page">
<p>{{ title }}</p>
<button @click="updateTitle">Click</button>
</div>
</template>
<script>
import { nextTick } from 'vue'
export default {
data() {
return {
title: 'This is a title.',
}
},
methods: {
updateTitle() {
nextTick(() => {
console.log(this.title);
});
this.title = 'change the title.';
}
}
}
</script>
答案也许和你想得不一样:
- 地狱难度来了!如果代码如下,打印出的会是什么?
<template>
<div class="page">
<input ref="iref" type="text" v-if="visible">
<p>{{ title }}</p>
<button @click="updateTitle">Click</button>
</div>
</template>
<script>
import { nextTick } from 'vue'
export default {
data() {
return {
title: 'This is a title.',
visible: false,
}
},
methods: {
updateTitle() {
this.title = 'change the title.';
nextTick(() => {
console.log(this.title, this.$refs.iref);
});
this.visible = true;
}
}
}
</script>
答案如下,有没有出乎意料?
- Vue 是如何保证,等待 DOM 更新完成以后,再去执行 nextTick 里面的方法呢?
其实,归根结底,很少会有人思考上面的第五个问题。前面的四道题,都与第五个问题有千丝万缕的联系。
从 nextTick 原理出发
先贴一下 nextTick 源码,为了方便看,我把源码注释去掉,加上了自己的说明注释:
// 任务 “队列”
const callbacks: Array<Function> = []
// 是否可执行队列
let pending = false
// 执行任务 “队列”
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// 下面一堆是降级策略,优先使用 Promise
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) ||
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 {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick(): Promise<void>
export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void
export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
// 加入任务 “队列”
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
总之,就 nextTick 来说,是这样执行的:
- 调用 nextTick 时,将具体逻辑,加入任务 “队列”;
- 利用 Promise 或其它降级方式的异步特性,并不会立刻执行任务 “队列” 的代码;
- 等同步代码执行完成以后,再去依次执行任务 “队列” 的逻辑;
nextTick 与状态变更(data 数据变更)的关系
我们直接跳到上面的第四题,这题可以说是很容易答错了,再看下题目:
<template>
<div class="page">
<input ref="iref" type="text" v-if="visible">
<p>{{ title }}</p>
<button @click="updateTitle">Click</button>
</div>
</template>
<script>
import { nextTick } from 'vue'
export default {
data() {
return {
title: 'This is a title.',
visible: false,
}
},
methods: {
updateTitle() {
this.title = 'change the title.';
nextTick(() => {
console.log(this.title, this.$refs.iref);
});
this.visible = true;
}
}
}
</script>
我来概括一下执行的路径:
- 当执行到
this.title = 'change the title.'
时,会先进入到title
的set
方法中(数据劫持)。set
方法先比较了title
新老值是否发生了变化,如果没有发生变化,不会进行任何后续操作,如果发生变化,会将**title**
的值变更为新值,然后通知 组件的监听器(watcher); - 注意!!!题目中的
**template**
依赖了两个状态值:**title**
和**visible**
,所以这里的监听器的执行的任务中,后续可能会有两个状态的变更。但监听器后续的执行逻辑,不会马上执行,而是将后续的逻辑,也通过 nextTick 加到任务 “队列” 中。是不是没想到,监听器后续视图变更逻辑,也是通过 nextTick 异步执行的? - 接着,回到题目中来,继续执行到我们自己写的 nextTick ,会将我们自己的逻辑也加入到任务 “队列” 中。这时候,我们的逻辑就排在监听器逻辑的后面了;
- 再次执行到
this.visible = true
,重复步骤 1; - 注意!!!这里并不会再将一个监听器加到任务 “队列” 中去了,只要判断组件的监听器已经存在,即会 return 出来,不做后续操作了;
- 同步逻辑走完,开始跑任务 “队列” 了,先进任务 “队列” 的是监听器逻辑,也就先开始执行监听器逻辑了。监听器发现自己监听的组件有两个视图依赖状态发生变更,后续将会更新视图;
- 任务 “队列” 继续执行我们自己通过 nextTick 加入的逻辑,这时候,由于状态和视图已经变更完毕,打印出了以下结果:
总结
- 由于 data 值的变更(仅谈变更),是同步的(见上面步骤 1),所以,无论 data 的属性的变更在 nextTick 之前或之后,nextTick 都可以获取到 data 属性变更后的值;
- 由于监听器的后续逻辑,也是通过 nextTick 进行异步执行的,与我们调用 nextTick 执行的逻辑是属于同一级别的任务,这样就可以保证,nextTick 可以拿到最终渲染的 dom(前提是监听器后续的逻辑,进任务 “队列” 的时机要比我们自己写的 nextTick 早);
建议再回过头来看看上面的五道问题,以及 Vue 官方文档对于 nextTick 的描述,是否清晰了许多呢