likes
comments
collection
share

nextTick详解➕手搓

作者站长头像
站长
· 阅读数 5

小序

各位掘友们是否曾在使用 Vue 开发过程中感到困扰,明明数据已经变了,但是 DOM 还没有更新? 这时,Vue 中的 nextTick 就派上用场了!本文我们将深入探讨 Vue 中这个神奇的异步更新机制,同时还会为大家展示如何手写一个简化版的 nextTick

深度剖析nextTick

什么是 nextTick?

在 Vue 的生命周期中,有一段时间是异步的,所以有的时候会遇到数据还未挂载到DOM节点就获取数据时就会出错,而 nextTick 就是让我们在这段异步时间结束后执行自己的代码的工具。它确保在DOM更新后执行回调 (起到了等待DOM渲染的作用)。

nextTick 基本用法

首先,让我们看一个简单的例子:

<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); 打印的结果是啥?

我们来到控制台,看到输出的结果为: nextTick详解➕手搓

这是为什么呢?我们来看看Vue3官方文档 找找原因

nextTick详解➕手搓

哦!原来如此,这段代码中setup是先执行的,当我们console.log()的时候这个组件甚至还未挂载,所以最终的输出结果就是null啦!

那么,接下来我们加上nextTick试试~

nextTick(() => {
  console.log(refP.value); // <p>消息:初始消息</p>
})

来到控制台看下打印结果为: nextTick详解➕手搓

从结果我们就可以看出nextTick的作用就是等待DOM渲染完毕后执行对应的fn,完美地解决了我们的需求:等待DOM渲染完成。

但是需要注意的是,nextTick 是vue中封装的异步微任务 具体核心实现方法有很多,如果遇到面试官妙语连珠,我们也可以提前对此了解一下:

  1. 在 Vue 2 中,nextTick 的实现涉及到了浏览器的异步执行机制,因为不同浏览器对于异步任务的处理方式可能不同。Vue 2 的 nextTick 实现了对于 PromiseMutationObserversetImmediatesetTimeout 四种异步执行机制的兼容处理。

  2. 在 Vue 3 中,nextTick 的实现方式发生了变化,主要是由于 Vue 3 引入了 Composition API 和更现代的 JavaScript 特性。Vue 3 中的 nextTick 使用了 Promise,不再需要 MutationObserver 或 setImmediate 这样的兼容性处理。

nextTick实际运用效果

到这里,我相信大家对于nextTick的概念以及使用有了大致的了解了,那么接下来让我们借助一个简单的小Demo来看看什么时候可以运用nextTick这个工具。

<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。

nextTick详解➕手搓

又到了各位友友思考的时间了,请问这是什么原因呢?

问题就出在 scrollIntoView 的调用时机上。在 Vue 中,当我们调用 list.value.push(...(new Array(10).fill(1))) 后,Vue 并不会立即更新 DOM,因为Vue 的更新是异步的,这样可以批量处理多个数据变更,提高性能。因此,在调用 scrollIntoView 的时候,<ul> 元素的更新还没有完成,li:last-child 仍然是原本的最后一个元素。

那么我们用nextTick是否可以解决呢?让我们来试试!

<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

在搞明白以上内容之后,nextTick对于各位友友来说已经a piece of cake了,接下来让我们一起来试着手搓一个nextTick吧!

首先,我们来想一想要通过完成哪些功能就能实现nextTick呢?比如,我们需要有一个DOM监听器来监听DOM节点是否更新完成,并且能够在特定时机触发我们的回调函数,好像也没有很复杂,话不多说直接开搓!

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是否有效呢?

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'
        })
    })
}

nextTick详解➕手搓

从结果中我们看到和vue中的nextTick效果是一样的, 到这里我们就手写实现了一个简单的vue中的nextTick了。

结语

终于是写完了,要是在春招中遇到这个问题,那就偷着乐吧...

假如您也和我一样,在准备春招。欢迎加我微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!