nextTick() Api介绍
等待下一次 DOM 更新刷新的工具方法。
- 类型
function nextTick(callback?: () => void): Promise<void>
- 详细信息
当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个“tick”才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。
nextTick() 可以在状态改变后立即使用,以等待 DOM 更新完成。你可以传递一个回调函数作为参数,或者 await 返回的 Promise。 - 示例
<script setup> import { ref, nextTick } from 'vue' const count = ref(0) async function increment() { count.value++ // DOM 还未更新 console.log(document.getElementById('counter').textContent) // 0 await nextTick() // DOM 此时已经更新 console.log(document.getElementById('counter').textContent) // 1 } </script> <template> <button id="counter" @click="increment">{{ count }}</button> </template>
$nextTick()
绑定在实例上的 nextTick() 函数。
- 类型
interface ComponentPublicInstance { $nextTick(callback?: (this: ComponentPublicInstance) => void): Promise<void> }
- 详细信息
和全局版本的 nextTick() 的唯一区别就是组件传递给 this.$nextTick() 的回调函数会带上 this 上下文,其绑定了当前组件实例。nextTick() this.$nextTick()
异步更新队列
Vue在观察到数据变化时并不是直接更新DOM,而是开启一个队列,并缓冲在同一个事件循环中发生的数据改变。在缓冲时会去除重复数据,从而避免不必要的计算和DOM操作。然后,在下一个事件循环tick中,Vue刷新队列并执行实际(已去重的)工作。所以如果你用一个for循环来动态改变数据100次,其实它只会应用最后一次改变,如果没有这种机制,DOM就要重绘100次,这固然是一个很大的开销。
Vue会根据当前浏览器环境优先使用原生的Promise.then和MutationObserver,如果都不支持,就会采用setTimeout代替。
$nextTick()适用场景
- 改变数据后更新DOM元素
<template> <div class="message" ref="messageRef"> {{ message }} </div> <button @click="updateMessage">更新Message</button> </template> <script lang="ts" setup> import { nextTick, onMounted, ref } from 'vue'; const messageRef = ref<HTMLElement>() // 这里定义一个和div中ref名字一样的变量名即可 let message = ref('Message') onMounted(() => { if (messageRef.value) { console.log(messageRef.value) } }) const updateMessage = () => { message.value = 'Updated Message' // 如果没有用nextTick,页面上展示 Updated Message, 如果用了nextTick 页面展示DOM Message nextTick(() => { messageRef.value.textContent = 'DOM Message'; }) } </script>
- 获取更新后的DOM尺寸和位置
<template> <div class="message" ref="messageRef"> {{ message }} </div> <button @click="getSize">获取Div尺寸</button> </template> <script lang="ts" setup> import { nextTick, onMounted, ref } from 'vue'; const messageRef = ref<HTMLElement>() // 这里定义一个和div中ref名字一样的变量名即可 let message = ref('Message') onMounted(() => { if (messageRef.value) { console.log(messageRef.value) } }) const getSize = () => { message.value = 'Updated Message' nextTick(() => { console.log(messageRef.value?.offsetWidth, messageRef.value?.offsetHeight) }) } </script>
- 执行复杂的计算
<template> <div class="message" ref="messageRef"> {{computedValue}} </div> </template> <script lang="ts" setup> import { nextTick, onMounted, ref, computed } from 'vue'; const messageRef = ref<HTMLElement>() // 这里定义一个和div中ref名字一样的变量名即可 let message = ref('Message') let items = ref([1, 2, 3, 4, 5]); onMounted(() => { if (messageRef.value) { console.log(messageRef.value) // <div class="message" ref="messageRef">15</div> } }) const computedValue = computed(() => { // 执行复杂的计算 const total = items.value.reduce((sum, val) => sum + val, 0) console.log(`Total1: ${total}`) // Total1: 15 console.log(document.getElementsByClassName('message').length) // 0 // 确保下一个 DOM 周期中更新视图 nextTick(() => { console.log(`Total2: ${total}`) // Total1: 15 console.log(document.getElementsByClassName('message')[0].textContent) // 15 }) return total })
- 在父组件中,等待子组件数据更新后再执行操作
//child.vue <template> <div>{{message}}</div> </template> <script lang="ts" setup> import { defineExpose, ref } from 'vue'; let message = ref('Message'); function updateMessage() { message.value = 'Updated Message' } // 将父组件要调用的方法抛出去 defineExpose({updateMessage}) </script> // Parent.vue <template> <Child ref="childRef" /> </template> <script lang="ts" setup> import { nextTick, onMounted, ref, computed } from 'vue'; import Child from "@/views/child.vue"; let childRef = ref<HTMLElement>(); // 这里定义一个和div中ref名字一样的变量名即可 onMounted(() => { console.log(childRef.value); nextTick(() => { console.log(childRef.value); childRef.value.updateMessage() }) }) </script>
- 等待 Vue.js 插件初始化后再执行操作
<template> <input v-model="message" ref="childRef" /> </template> <script lang="ts" setup> import { nextTick, onMounted, ref } from 'vue'; let childRef = ref<HTMLElement>(); // 这里定义一个和div中ref名字一样的变量名即可 let message = ref(''); onMounted(() => { nextTick(() => { childRef.value?.focus(); }) }) </script>
- 监听视图变化并执行相应操作
<template> <div> <input ref="inputMessage" v-model="message"> <div>Message Length: {{messageLength}}</div> </div> </template> <script lang="ts" setup> import { computed, nextTick, onMounted, ref } from 'vue'; let inputMessage = ref<HTMLElement>(); // 这里定义一个和div中ref名字一样的变量名即可 let message = ref(''); let messageLength = computed(() => { return message.value.length }) onMounted(() => { focusInput(); }) function focusInput() { nextTick(() => { inputMessage.value?.focus(); }) } </script>
$nextTick和setTimeout区别
- nextTick 在vue 源码中是利用 Promise.resolve()实现的。该问题实际就是Promise与setTimeout的区别,本质是Event Loop中微任务与宏任务的区别。
nextTick:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。 - setTimeout:只是延迟执行,在延迟执行的方法里,DOM有可能会更新也有可能没有更新。
- setTimeout:就是个延时回调,和DOM操作无关。
- 建议使用nextTick在有涉及DOM更新的场景
JS中的Event Loop
javascript是单线程的,所有的任务都会在主线程中执行的,当主线程中的任务都执行完成之后,系统会 “依次” 读取任务队列里面的事件,因此对应的异步任务进入主线程,开始执行。
但是异步任务队列又分为: macrotasks(宏任务) 和 microtasks(微任务)。 他们两者分别有如下API:
macrotasks(宏任务): setTimeout、setInterval、setImmediate、I/O、UI rendering 等。
microtasks(微任务): Promise、process.nextTick、MutationObserver 等。
promise的then方法的函数会被推入到 microtasks(微任务) 队列中(Promise本身代码是同步执行的),而setTimeout函数会被推入到 macrotasks(宏任务) 任务队列中,在每一次事件循环中 macrotasks(宏任务) 只会提取一个执行,而 microtasks(微任务) 会一直提取,直到 microtasks(微任务)队列为空为止。
也就是说,如果某个 microtasks(微任务) 被推入到执行中,那么当主线程任务执行完成后,会循环调用该队列任务中的下一个任务来执行,直到该任务队列到最后一个任务为止。而事件循环每次只会入栈一个 macrotasks(宏任务), 主线程执行完成该任务后又会循环检查 microtasks(微任务) 队列是否还有未执行的,直到所有的执行完成后,再执行 macrotasks(宏任务)。 依次循环,直到所有的异步任务完成为止。
console.log(1);
setTimeout(function(){
console.log(2);
}, 0);
new Promise(function(resolve) {
console.log(3);
for (var i = 0; i < 100; i++) {
i === 99 && resolve(i);
}
console.log(4);
}).then(function() {
console.log(5);
});
console.log(6);
console.log(1);
setTimeout(function(){
console.log(2);
}, 10);
new Promise(function(resolve) {
console.log(3);
for (var i = 0; i < 10000; i++) {
i === 9999 && resolve(i);
}
console.log(4);
}).then(function() {
console.log(5);
});
setTimeout(function(){
console.log(7);
},1);
new Promise(function(resolve) {
console.log(8);
resolve();
}).then(function(){
console.log(9);
});
console.log(6);