优秀的编程知识分享平台

网站首页 > 技术文章 正文

细聊vue的nextTick(vuex nexttick)

nanyue 2024-10-29 14:50:20 技术文章 4 ℃

近两年来,vue里面的nextTick很容易被问到,赶在春招之前,我们赶紧来复习下

nextTick的作用是在dom更新后去执行的,起到了等待dom渲染完成后的作用

我们来看一个情景,我们使用vue语法获取dom结构,如下

<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)
</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中打印是可以打印到的

<template>
  <div>
    <p ref="refP">消息: {{msg}}</p>
  </div>
</template>

<script setup>
import { ref, nextTick } from 'vue';

const msg = ref('你好啊')

const refP = ref(null)

// console.log(refP.value)
nextTick(() =>{  
  console.log(refP.value, 'nextTick')
})
</script>

<style lang="css" scoped>

</style>


值得一提的是,nextTick不是生命周期,它仅仅是个函数,它非常之特殊,它的执行时间是在dom更新完成之后执行的

所以说onBeforeMount在nextTick之前执行。

我们看看nextTick和全局打印相比是什么样的

nextTick(() =>{  
  console.log(refP.value, 'nextTick')
})

console.log(refP.value, 'log')


最后打印发现,nextTick后执行,所以它一定是个异步函数,同步的代码从上往下,怎么可能会先打印log

异步分为宏和微,我们再来试试看nextTick是哪一种

打印下面这个栗子

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是挂载执行还是渲染执行呢?

我们再比较下

nextTick(() =>{  
  console.log(refP.value, 'nextTick')
})

onMounted(() => {  // 挂载完执行onMounted
  console.log(refP.value, 'onMounted')
})


这个打印结果是onMounted先,如果nextTick是挂载完执行,那么一定是从上到下打印,所以结果证明nextTick是渲染完后执行

应用

我们看一个情景,假设有很多列表,当我们点击更新列表的时候会新增很多列表,然后希望可以自动滚到最后一个列表

<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更新后再去执行

<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,这里我就直接拿已知的

export function myNextTick (fn) {

    let app = document.getElementById('app')

}


如何看dom是否更新完成就需要用上一个高级方法MutationObserver

这个高级方法我曾在event-loop提到过,是个微任务

创建一个dom监听器,还需要配置一下,最终写法如下

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的外衣


作者:Dolphin_海豚
链接:https://juejin.cn/post/7337958423780638747

Tags:

最近发表
标签列表