前言
最近,当面试官问到有关于Vue的问题时,nextTick
是十分容易被问到的,今天我们就来聊聊nextTick。
nextTick
类似于生命周期,它是一个函数,接受一个回调函数作为参数,在页面渲染完成后执行(起到了一个等待DOM渲染完成的作用)
我先来一段情景,详细的带大家慢慢了解nextTick
。
<template> <div> <p ref="refP">消息: {{msg}}</p> </div> </template> <script setup> import { ref } from 'vue'; const msg = ref('Hello') const refP = ref(null) console.log(refP.value) </script> <style lang="css" scoped> </style>
在vue中,我们在标签中写ref
,这是vue中的独有语法,可以去获取该标签的一个DOM
结构。然后我们使用console.log
打印一下该DOM结构:
我们会发现打印为null
,
如果我们到代码中再加入一个定时器,在定时器中去打印:
setTimeout(() => { console.log(refP.value); }, 1);
我们发现可以成功的拿到DOM结构:
为何我们直接打印不能拿到标签的DOM结构呢?
若是我们想要成功获取DOM结构
的值,需要等待挂载完成之后才可以拿到,而在vue中,全局代码的执行是在挂载之前的。
谈到这里,我们就不得不聊一聊生命周期了,生命周期指的是vue文件在读取到它的那一刻到它能成功渲染到浏览器页面的过程
生命周期
我们先来看一张图片,该图片来自于Vue官方:
这张图就代表着Vue的整个生命周期,蓝色方框代表Vue3独有的生命周期,而红色方框则表示Vue2和Vue3公共的。
首先是组合式API的setup
,然后是创建之前,再去初始化选项式API,比如初始化数据源data
,方法method
等等。created
创建完整,代表整个vue
文件被完整读取。然后去查看是否存在预编译模板template
,并且及时编译模板,这是在组件挂载之前进行的操作beforeMount
。当编译完成后,就是初始渲染,创建和插入DOM节点,进行一个挂载mounted
。
当数据源变更后,就会进行beforeUpdate
和updated
,重新渲染页面。
这也就更能解释为什么我们打印会得到null
了。因为我们在全局打印就是在setup
这个生命周期中进行,而获取到一个标签的DOM结构需要等到模板被编译完成,而setup
在编译完成之前执行,所以打印null
。
那么如果我们在beforeMount
中去获取标签的DOM结构可以拿到吗?答案也是不行的。
为什么呢?当beforeMount
触发的时候,模板也是编译完成的。这是因为我们通过ref
去获取DOM结构,当我们模板template
编译完成时,成功拿到了DOM结构,但当输出refP
的值时,vue并没有将DOM结构赋值过去,所以打印出null
。
而在挂载完成时,赋值则已经完成,所以可以拿到。
我们上面提到了,如果我们使用定时器,可以成功获取到p标签
的DOM结构。如果我们把定时器的事件改为0ms
,也是可以拿到p标签的,这是为什么?
因为setTImeout
是一个异步宏任务,不管时间多少,总是会先执行同步代码,编译模板其实就是个同步代码!并且定时器的执行是在挂载之后的。
以上都是铺垫,我所讲这些就是为了帮助我们去更好的理解nextTick
。
nextTick
我们可以在nextTick
中,去获取标签的DOM结构:
<template> <div> <p ref="refP">消息: {{ message }}</p> </div> </template> <script setup> import { ref, onBeforeMount, nextTick, onMounted } from 'vue' const message = ref('初始消息') const refP = ref(null) nextTick(() => { console.log(refP.value); }) </script> <style lang="scss" scoped> </style>
这里需要说明一下,nextTick
比较特殊,它并不是Vue中的一个生命周期,它只是一个函数
,它在dom更新完成之后触发。
当挂载完成后,页面进行渲染,当渲染完成时,nextTick执行,所以nextTick起到了一个等待DOM渲染完成的作用
nextTick异步微任务
const refP = ref(null) nextTick(() => { console.log(refP.value,'nextTick'); }) console.log(refP.value, 'setup');
我们来看看哪份代码先执行:
我们可以发现,先打印setup
,那么同步代码执行的顺序是从上往下的,如果nextTick
是同步代码的话应该是先打印nextTick,所以可以推出nextTick是异步函数
。
而异步又分为宏任务和微任务,我们再来推断一下nextTick
是什么:
我们这里使用定时器来判断,定时器是异步宏任务:
setTimeout(() => { console.log(refP.value, 'setTimeout') }, 0) nextTick(() =>{ console.log(refP.value, 'nextTick') })
从上得知,我们发现nextTick
先执行。如果nextTick
是异步宏任务的话,那么一定先打印上面的宏任务setTimeout
。因为宏任务的打印遵循队列,谁先进入队列就先打印谁。所以nextTick
是异步微任务,微任务的执行在宏任务之前。
宏任务和微任务涉及到了JS中十分重要的事件循环机制
event loop
,不清楚这方面的小伙伴们可以去网上查阅一下,有机会的话我会在之后也写一篇详细的文章。
应用
我们来看看一个情景,假设我们有很多个列表项,当我们点击更新列表时,还会新增许多个列表,我们希望点击更新时,自动的帮我们滚到更新后的最后一个列表项。
<template> <div id="app"> <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') // 获取到最后一个li liItem.scrollIntoView({ behavior: 'smooth' }) // 原生js的方法 } </script> <style lang="css" scoped> li { height: 100px; background-color: aquamarine; margin: 10px; } </style>
这里我们简单的给li
添加一些样式,方便识别。使用v-for
循环生成列表,当点击按钮时,往数组中推入十个列表。并且获取到最后一个li
元素。使用原生js自带的方法自动滚动。
我们来看看效果:
这是我们的页面效果,当我们点击更新按钮时:
我们可以看到,最终只滚动到了新增的第一个元素身上,我们想要实现的效果是滚动到新增的最后一个元素身上。
这是为什么呢?当我们点击按钮触发函数时,函数内部的代码都是同步代码,而此时我们需要十个li
渲染完成并且滚到到最后一个li
上,同步代码的执行是立刻的,它不会去等待li
全部渲染完成。所以这里只滚到到第一个新增li
。
如果遇到这种情景,就不得不用到nextTick
了。因为nextTick
能保障dom全部渲染完成时再执行。
<template> <div id="app"> <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: 100px; background-color: aquamarine; margin: 10px; } </style>
当我们再点击更新按钮时:
直接滚到最后一个新增li
了。
现在我们可以清晰的看出nextTick
的作用了叭,这里等待页面全部渲染完再去执行操作。
手写一个nextTick
我们已经明白了nextTick
的作用,在dom结构全部渲染完成后执行。而nextTick
就是接受一个回调,在这个时间点去触发该函数。
那我们就需要去监听DOM结构的变更:
JS提供了一个高级的方法MutationObserve
,它可以去帮我们监听DOM结构是否更新完成:
export function myNextTick(fn) { let app = document.getElementById('app') var observerOptions = { childList: true, // 观察目标子节点的变化,是否有添加或者删除 attributes: true, // 观察属性变动 subtree: true, // 观察后代节点,默认为 false }; // 让fn()在dom更新完成后执行 // 创建一个DOM监听器 let observer = new MutationObserver((el) => { // 当被监听的DOM更新完成时,该回调会触发 console.log(el); fn() }) observer.observe(app, observerOptions) // 监听上某个dom节点及子节点 }
首先我们通过document.getElementById('app')
去拿到一个DOM结构,然后写一个监听的配置项。 创建一个监听DOM的对象observer
。
而当我们监听的这个app
有变更时,就会触发里面的回调函数。
这样我们就简单的实现了一个nextTick
,还是挺简单的,使用了JS当中自带的方法MutationObserve
。
家人们可以拿我们自己手搓的nextTick
去上述列表更新项试一下,效果出现了,功能一模一样,我们的nextTick
成功了!
最后
这篇文章我们总结了一下生命周期,以及nextTick
函数的作用及用法,最后手搓了一遍nextTick
加深我们的理解。
值得一提的是,nextTick
并不是Vue中的生命周期,它是一个异步微任务的函数。
写文章不易,如果帮助到了小伙伴们,可以给本文点赞收藏评论三连呀。有不懂的地方欢迎到评论区留言,我会及时回复。