面试官:聊聊 nextTick

在最近的面试中,不少面试官叫我聊聊 nextTicknextTick 是个啥,这篇文章咱来好好聊聊!

我的回答

nextTick 是官方提供的一个异步方法,用于在 DOM 更新之后执行回调。正好在我的项目中用到了,就拿它来形容一下,大概的场景是渲染一个列表,每次点击按钮就会往列表后面添加十条数据,并立即跳到第十条数据的位置。我们知道渲染列表是需要耗时的,想要直接跳到第十条数据是行不通的,因为这时候数据并没有加载出来。这个时候就得进行异步操作,你可以添加一个定时器,但显然不太优雅,这时候 nextTick 就是最佳选择,它会在 DOM 更新完成后执行。

官方描述

参考官方文档

描述

等待下一次 DOM 更新刷新的工具方法。

当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个"tick"才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。

nextTick() 可以在状态改变后立即使用,以等待 DOM 更新完成。你可以传递一个回调函数作为参数,或者 await 返回的 Promise。

示例

html 复制代码
<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是异步微任务?一个例子告诉你

什么?你不知道什么是异步微任务?这里跳转往期介绍------面试官:聊一下 Event - Loop - 掘金 (juejin.cn)

html 复制代码
<template>
  <div>
    <p>消息: {{ message }}</p>
    <button @click="updateMsg">修改消息</button>
  </div>
</template>

<script setup>
import { ref, nextTick } from 'vue';
const message = ref('初始消息');
const updateMsg = () => {
  console.log(message.value, '同步任务');
  setTimeout(() => {
    console.log(message.value, '异步宏任务------setTimeout');
  }, 0)
  nextTick(() => {
    console.log(message.value, '异步微任务------nextTick');
  })
  message.value = '修改后的消息';
}
</script>

当点击按钮时,会修改 message 的值,我们观察同步代码,以及 setTimeoutnextTick 的顺序,在修改消息前打印message.value,进行以下猜想:

  • 如果nextTick打印的是修改后的值,说明它是异步代码
  • 如果nextTicksetTimeout先打印,则说明它是异步微任务,反之则为异步宏任务

观察结果:

结果不出所料,nextTick为异步微任务。

项目中的应用

接下来介绍一下nextTick的实际应用场景

来看开头提到的demo:

html 复制代码
<template>
    <div>
        <button @click="updateList">更新列表</button>
        <ul>
            <li v-for="i in list">{{i}}</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))) //模拟往列表中添加十个1
    //实现点击跳转
    const listItem = document.querySelector('li:last-child')//获取最后一个li的DOM结构
    listItem.scrollIntoView({behavior: 'smooth'})//平滑滚动至最后一条数据
}
</script>
<style lang="css" scoped>
li{
    list-style: none;
    font-size: 64px;
    height: 100px;
    background-color: #42b883aa;
    margin: 10px;
}
</style>

这里我们实现每次点击按钮就会往列表后面添加十条数据,并立即跳到最后条数据的位置,即最后一个1,看着逻辑好像是没什么问题,来看效果:

好像不太对,为什么跳到第一个1,才露一点点头就不滚了?

原因是新加载的 DOM 还没有渲染完成,滚动是同步代码,不会等待渲染完成在滚动,所以我们要人为进行一些操作,让它等 DOM 渲染完再滚动,这正好对应了nextTick的效果,在 DOM 渲染完成执行回调,我们在回调中执行滚动即可。

2.0 版本诞生:

html 复制代码
<script setup>
import {nextTick , ref} from 'vue'
const list = ref(new Array(20).fill(0))

const updateList = ()=>{
    list.value.push(...(new Array(10).fill(1)))
    nextTick (()=>{ //在nextTick中执行回调
        const listItem = document.querySelector('li:last-child')
        listItem.scrollIntoView({behavior: 'smooth'})
    })
}
</script>

太丝滑了!

手写nextTick

手写nextTick毋庸置疑是大厂面试手写题的常客。

如何判断 DOM 结构是否更新?是这个手写题的切入点。这里我们使用MutationObserver来监听 DOM 结构的变化。

参考MDN------MutationObserver.MutationObserver()

js 复制代码
//next-tick.js
export function myNextTick(fn) {
    let app = document.getElementById('app');

    // 观察目标DOM及其子节点的变化
    var observerOptions = {
        childList: true, // 观察目标子节点的变化,是否有添加或者删除
        attributes: true, // 观察属性变动
        subtree: true, // 观察后代节点,默认为 false
    };
    
    // 创建 MutationObserver 实例
    let observer = new MutationObserver(() => {
        // 当DOM结构发生变化时,执行回调函数
        fn();
    });

    // 开始监听目标DOM
    observer.observe(app, observerOptions);
}

我们的myNextTick函数使用了 MutationObserver 来观察指定 DOM 结构及其子节点的变化,并在变化发生时执行传入的回调函数 fn

我们来看看跟官方是否具有相同的效果。

html 复制代码
<script setup>
import { ref } from 'vue';
import { myNextTick } from './next-tick.js'

const message = ref('初始消息');

const refP = ref(null)
const updateMsg = () => {
  console.log(message.value, '同步任务');
  setTimeout(() => {
    console.log(message.value, '异步宏任务------setTimeout');
  }, 0)
  myNextTick(() => {//对比可知是异步
    console.log(message.value, '异步微任务------myNextTick');
  })
  message.value = '修改后的消息';
}
</script>

同样是异步微任务!


html 复制代码
<script setup>
import { ref } from 'vue'
import { myNextTick } from './next-tick.js'//引入myNextTick
const list = ref(new Array(20).fill(0))

const updateList = ()=>{
    list.value.push(...(new Array(10).fill(1)))
    myNextTick (()=>{ //myNextTick
        const listItem = document.querySelector('li:last-child')
        listItem.scrollIntoView({behavior: 'smooth'})
    })
}
</script>

同样丝滑!

最后

看到这里希望你对nextTick有了一定的认识,掌握 nextTick 的作用和实现原理,能够更好地处理 Vue 中的异步更新,提高代码的性能和质量。祝学习进步!面试顺利 ~

已将学习代码上传至 github,欢迎大家学习指正!

技术小白记录学习过程,有错误或不解的地方还请评论区留言,如果这篇文章对你有所帮助请 "点赞 收藏+关注" ,感谢支持!!

相关推荐
蟾宫曲4 小时前
在 Vue3 项目中实现计时器组件的使用(Vite+Vue3+Node+npm+Element-plus,附测试代码)
前端·npm·vue3·vite·element-plus·计时器
秋雨凉人心4 小时前
简单发布一个npm包
前端·javascript·webpack·npm·node.js
liuxin334455664 小时前
学籍管理系统:实现教育管理现代化
java·开发语言·前端·数据库·安全
qq13267029404 小时前
运行Zr.Admin项目(前端)
前端·vue2·zradmin前端·zradmin vue·运行zradmin·vue2版本zradmin
LCG元5 小时前
Vue.js组件开发-使用vue-pdf显示PDF
vue.js
魏时烟5 小时前
css文字折行以及双端对齐实现方式
前端·css
哥谭居民00016 小时前
将一个组件的propName属性与父组件中的variable变量进行双向绑定的vue3(组件传值)
javascript·vue.js·typescript·npm·node.js·css3
烟波人长安吖~6 小时前
【目标跟踪+人流计数+人流热图(Web界面)】基于YOLOV11+Vue+SpringBoot+Flask+MySQL
vue.js·pytorch·spring boot·深度学习·yolo·目标跟踪
2401_882726486 小时前
低代码配置式组态软件-BY组态
前端·物联网·低代码·前端框架·编辑器·web
web130933203986 小时前
ctfshow-web入门-文件包含(web82-web86)条件竞争实现session会话文件包含
前端·github