近两年来,vue里面的nextTick很容易被问到,赶在春招之前,我们赶紧来复习下
nextTick的作用是在dom更新后去执行的,起到了等待dom渲染完成后的作用
我们来看一个情景,我们使用vue语法获取dom结构,如下
xml
<template>
<div>
<p ref="refP">消息: {{msg}}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
const msg = ref('你好啊')
const refP = ref(null)
consoloe.log(refP.value)
</script>
<style lang="css" scoped>
</style>
这个时候如果你去运行,你会发现打印null,如果这个打印你放在定时器内,1s后打印,才可以打印出值
这是为何?这里是通过ref获取dom结构,允许给一个dom结构打一个ref标记是vue的语法,vue中的js想要打印出refP的值,就需要等到p标签被解析完成并挂载,vue的js全局执行打印是不需要等挂载的,因此打印出初始值null
这就涉及到vue的生命周期了。所谓生命周期指的是vue文件在读取到它的那一刻到它能成功渲染到浏览器页面的过程
生命周期全过程
这张图就是整个生命周期过程。红色代表vue2和vue3的公共部分。
最先是组合式api,然后是创建之前,再是初始化选项式api,其实就是初始化数据源,methods方法等等,created就是创建完成,这代表整个vue文件被读取完毕,然后看有无template模板,这就是挂载之前,然后就是初始化渲染,植入dom节点。即时编译模板
就是前面说的编译,vue的代码被编译成html代码。数据源变更后就会有个beforeUpdate和updated,让页面重新渲染,最后就是渲染前和渲染后。
全局打印就是最初的入口函数setup第一步,而读取到refP需要等到vue被编译完成,也就是挂载之前按道理就可以读取到,没错,但是如果这时你拿着onBeforeMount
去打印,还是null,这是因为编译完成不错,但是refP = ref(null)
这个赋值还没完成,编译完需要给到别人用,也就说还没用上它。
如果我们把定时器的时间改为0ms,还是可以拿到p标签。这是为何?
setTimeout
永远是异步宏任务,无论时间夺少,从上面我们可以推出即时编译模板
一定赶在定时器之前完成,编译模板其实是个同步代码
好了,现在进入今天的主题nextTick
nextTick
还是上面的打印p标签这个栗子,我们把它放入nextTick中打印是可以打印到的
xml
<template>
<div>
<p ref="refP">消息: {{msg}}</p>
</div>
</template>
<script setup>
import { ref, nextTick } from 'vue';
const msg = ref('你好啊')
const refP = ref(null)
// consoloe.log(refP.value)
nextTick(() =>{
console.log(refP.value, 'nextTick')
})
</script>
<style lang="css" scoped>
</style>
值得一提的是,nextTick不是生命周期,它仅仅是个函数,它非常之特殊,它的执行时间是在dom更新完成之后执行的
所以说onBeforeMount在nextTick之前执行。
我们看看nextTick和全局打印相比是什么样的
javascript
nextTick(() =>{
console.log(refP.value, 'nextTick')
})
console.log(refP.value, 'log')
最后打印发现,nextTick后执行,所以它一定是个异步函数
,同步的代码从上往下,怎么可能会先打印log
异步分为宏和微,我们再来试试看nextTick是哪一种
打印下面这个栗子
javascript
setTimeout(() => {
console.log(refP.value, 'setTimeout')
}, 0)
nextTick(() =>{
console.log(refP.value, 'nextTick')
})
这个打印结果是可以看出nextTick是宏还是微,为何?setTimeout是宏任务,如果setTimeout先打印,那么nextTick一定是宏任务,两个宏任务打印顺序是遵循队列,先入先执行。如果是nextTick先打印,那么nextTick一定就是微任务,因为事件循环机制中,微任务先执行
不清楚event-loop可以翻看这篇文章透析js事件循环机制event-loop【拿捏面试】 - 掘金 (juejin.cn)
最终打印发现是nextTick先执行,所以nextTick是个异步微任务
其实vue2.0的时候,nextTick就是微任务,2.2的时候被改成了宏任务,2.5的时候又被改成了微任务,直到现在一直都是微任务。
刚才说了,nextTick是dom更新后执行的,dom更新后先是挂载,再是拿到浏览器去渲染,那nextTick是挂载执行还是渲染执行呢?
我们再比较下
javascript
nextTick(() =>{
console.log(refP.value, 'nextTick')
})
onMounted(() => { // 挂载完执行onMounted
console.log(refP.value, 'onMounted')
})
这个打印结果是onMounted先,如果nextTick是挂载完执行,那么一定是从上到下打印,所以结果证明nextTick是渲染完后执行
应用
我们看一个情景,假设有很多列表,当我们点击更新列表的时候会新增很多列表,然后希望可以自动滚到最后一个列表
xml
<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>
这里是默认有20个列表,然后点击后新增10个,按道理会滑倒最后一个,但是最终效果是滑到了新增的第一个
为什么会这样?点击按钮时触发函数,里面的代码都是同步代码,此时需要新增10个li并去渲染完成,而同步代码执行是瞬时的,不会等你渲染完成再去移到最后一个li,因此这里的效果就是只能移到第一个
如果用nextTick就可以解决,nextTick保证了浏览器等dom更新后再去执行
xml
<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>
效果如下
nextTick可以给你一个时间差,让你确保dom更新完成后再去执行某段逻辑。
手搓
已经理解了nextTick的原理,我们现在开始手搓
nextTick就是接收一个回调,然后让他在某个时间点触发这个回调。这个时间点就是dom更新完成后。
首先一定需要拿到dom,这里我就直接拿已知的
javascript
export function myNextTick (fn) {
let app = document.getElementById('app')
}
如何看dom是否更新完成就需要用上一个高级方法MutationObserver
这个高级方法我曾在event-loop提到过,是个微任务
创建一个dom监听器,还需要配置一下,最终写法如下
javascript
export function myNextTick (fn) {
let app = document.getElementById('app')
// 配置项
var observerOptions = {
childList: true, // 观察目标子节点的变化,是否有添加或者删除
attributes: true, // 观察属性变动
subtree: true, // 观察后代节点,默认为 false
};
// 创建一个dom监听器,dom更新时触发回调
let observer = new MutationObserver(() => {
fn()
})
observer.observe(app, observerOptions); // 监听某个dom节点以及子节点
}
监听的dom一旦有变更,就会走回调,回调中放入传入的函数
MutationObserver这个方法直接让你实现了nextTick的核心原理。最终你可以试着用刚才的列表栗子换上自己手搓的nextTick,可以试试效果,最终是一样的
最后
nextTick是个特殊的函数,但不是生命周期,他是在dom渲染完成后执行,并且是个异步微任务,并且从源码看就是套了层MutationObserver的外衣
另外有不懂之处欢迎在评论区留言,如果觉得文章对你学习有所帮助,还请"点赞+评论+收藏"一键三连,感谢支持!
本次学习代码已上传至本人GitHub学习仓库:github.com/DolphinFeng...
假如您也和我一样,在准备春招。欢迎加我微信Dolphin_Fung,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!