小序
各位掘友们是否曾在使用 Vue 开发过程中感到困扰,明明数据已经变了,但是 DOM 还没有更新? 这时,Vue 中的 nextTick
就派上用场了!本文我们将深入探讨 Vue 中这个神奇的异步更新机制,同时还会为大家展示如何手写一个简化版的 nextTick
。
深度剖析nextTick
什么是 nextTick?
在 Vue 的生命周期中,有一段时间是异步的,所以有的时候会遇到数据还未挂载到DOM节点就获取数据时就会出错,而 nextTick
就是让我们在这段异步时间结束后执行自己的代码的工具。它确保在DOM更新后执行回调 (起到了等待DOM渲染的作用)。
nextTick 基本用法
首先,让我们看一个简单的例子:
html
<template>
<div>
<p ref="refP">消息:{{ msg }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const msg = ref('初始消息')
const refP = ref(null)
console.log(refP.value);
各位友友不妨来猜猜这个console.log(refP.value);
打印的结果是啥?
我们来到控制台,看到输出的结果为:
这是为什么呢?我们来看看Vue3官方文档 找找原因
哦!原来如此,这段代码中setup是先执行的,当我们console.log()的时候这个组件甚至还未挂载 ,所以最终的输出结果就是null
啦!
那么,接下来我们加上nextTick试试~
js
nextTick(() => {
console.log(refP.value); // <p>消息:初始消息</p>
})
来到控制台看下打印结果为:
从结果我们就可以看出nextTick的作用就是等待DOM渲染完毕后执行对应的fn,完美地解决了我们的需求:等待DOM渲染完成。
但是需要注意的是,nextTick 是vue中封装的异步微任务
具体核心实现方法有很多,如果遇到面试官妙语连珠,我们也可以提前对此了解一下:
-
在 Vue 2 中,
nextTick
的实现涉及到了浏览器的异步执行机制,因为不同浏览器对于异步任务的处理方式可能不同。Vue 2 的nextTick
实现了对于Promise
、MutationObserver
、setImmediate
和setTimeout
四种异步执行机制的兼容处理。 -
在 Vue 3 中,
nextTick
的实现方式发生了变化,主要是由于 Vue 3 引入了 Composition API 和更现代的 JavaScript 特性。Vue 3 中的nextTick
使用了Promise
,不再需要 MutationObserver 或 setImmediate 这样的兼容性处理。
nextTick实际运用效果
到这里,我相信大家对于nextTick的概念以及使用有了大致的了解了,那么接下来让我们借助一个简单的小Demo来看看什么时候可以运用nextTick这个工具。
js
<template>
<div>
<button @click="updateList">更新列表</button>
<ul>
<li v-for="n in list">{{ n }}</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
const list = ref(new Array(20).fill(0))
const updateList = () => {
list.value.push(...(new Array(10).fill(1)))
const liItem = document.querySelector('li:last-child')
liItem.scrollIntoView({behavior: 'smooth'})
}
</script>
<style lang="css" scoped>
li {
height: 30px;
margin: 30px 0;
background-color: #a92e2e;
}
</style>
通过这段代码,我们可以实现一个列表加载的效果,首先页面我们初始化了20个0,当我们点击更新列表按钮后,会新增10个1在列表中,并且期望移动至最后一位数字,但是我们来看看结果,移动到了第一个1。
又到了各位友友思考的时间了,请问这是什么原因呢?
问题就出在 scrollIntoView
的调用时机上。在 Vue 中,当我们调用 list.value.push(...(new Array(10).fill(1)))
后,Vue 并不会立即更新 DOM,因为Vue 的更新是异步的,这样可以批量处理多个数据变更,提高性能。因此,在调用 scrollIntoView
的时候,<ul>
元素的更新还没有完成,li:last-child
仍然是原本的最后一个元素。
那么我们用nextTick是否可以解决呢?让我们来试试!
js
<template>
<div>
<button @click="updateList">更新列表</button>
<ul>
<li v-for="n in list">{{ n }}</li>
</ul>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const list = ref(new Array(20).fill(0))
const updateList = () => {
list.value.push(...(new Array(10).fill(1)))
nextTick(() => {
const liItem = document.querySelector('li:last-child')
liItem.scrollIntoView({
behavior: 'smooth'
})
})
}
</script>
<style lang="css" scoped>
li {
height: 30px;
margin: 30px 0;
background-color: #a92e2e;
}
</style>
当我们重新加载页面然后点击更新列表时,我们看到页面滚动至了最后一位数字1。
手写一个nextTick
在搞明白以上内容之后,nextTick对于各位友友来说已经a piece of cake
了,接下来让我们一起来试着手搓一个nextTick吧!
首先,我们来想一想要通过完成哪些功能就能实现nextTick呢?比如,我们需要有一个DOM监听器来监听DOM节点是否更新完成,并且能够在特定时机触发我们的回调函数,好像也没有很复杂,话不多说直接开搓!
js
export function myNextTick(fn) {
let app = document.getElementById('app');
var observerOptions = {
childList: true, // 观察目标子节点的变化,是否有添加或者删除
attributes: true, // 观察属性变动
subtree: true, // 观察后代节点,默认为 false
};
// 让fn()在DOM更新完成之后执行 -> MutationObserver
// 创建一个DOM监听器
let observer = new MutationObserver((el) => {
// 当被监听的DOM节点更新完成时,该回调函数会触发
console.log(el);
fn();
});
observer.observe(app,observerOptions); // 监听上某个DOM节点及子节点
}
首先我们传入一个回调函数fn给函数myNextTick,接着再获取到DOM节点,然后指定了 MutationObserver
的观察选项。设置为 childList: true
表示观察目标节点的子节点变化,attributes: true
表示观察属性变动,subtree: true
表示观察所有后代节点。接下来,创建一个MutationObserver的实例化对象,接收一个回调函数作为参数,该回调函数在被监听的 DOM 节点发生变化时触发 ,执行传入的回调函数 fn()
。最后再开始观察指定的 DOM 节点,使用之前定义的观察选项,并在 MutationObserver
的回调函数中,执行传入的回调函数 fn()
,以确保在 DOM 更新完成后执行需要执行的操作。
但是需要注意的是,我们这里只是简单地实现了nextTick的部分功能,而 Vue 的 nextTick
则是基于微任务的机制
。微任务更加轻量级,且能够在当前事件循环的末尾执行,而不需要创建额外的 DOM 监听器。因此,在实际开发中,使用 Vue 提供的 nextTick
更为推荐。
最后,让我们看看我们这个手搓出来的nextTick是否有效呢?
js
import { myNextTick } from './next-tick'
const updateList = () => {
list.value.push(...(new Array(10).fill(1)))
myNextTick(() => {
const liItem = document.querySelector('li:last-child')
liItem.scrollIntoView({
behavior: 'smooth'
})
})
}
从结果中我们看到和vue中的nextTick效果是一样的, 到这里我们就手写实现了一个简单的vue中的nextTick了。
结语
君不见黄河之水天上来,奔流到海不复回。年前本制定了一系列任务规划,却因亲朋好友的盛情难却将之抛掷脑后。当下我们迎来了充满希望的2024年,一元复始,万象更新。即使起接踵而至的是万众瞩目的春招,我现在在学习vue底层原理并且写面试题文章,愿与诸君互相学习,关注交流,一同拿下五月份大厂春招的offer。