引言
在前端开发中,我们经常遇到需要在DOM更新之后执行某些操作的需求。Vue.js 提供了一个非常有用的工具叫做 nextTick
,它能确保我们的回调函数在所有数据更改被应用到 DOM 之后执行。本文将探讨 nextTick
的实现原理,还有它是宏任务
还是微任务
,并一步一步来展示它是如何工作的。
什么是 nextTick
nextTick
是 Vue.js 中的一个方法,它的作用是在数据变化之后延迟执行回调函数。这意味着我们可以确保在回调函数执行之前,Vue 已经完成了对 DOM 的更新。
正文
下面是模仿vue里面的nextTick功能手写的一个nextTick代码:
手写的nextTick完整代码(简易版)
function nextTick(fn) {
return new Promise((resolve, reject) => {
// DOM更新完成否?
if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(() => {
const res=fn()
if(res instanceof Promise){
res.then(()=>{
resolve()
})
}else{
resolve()
}
observer.disconnect();
})
observer.observe(document.getElementById('app'), { attributes: true, childList: true, subtree: true })
}
})
}
下面我们一步一步来对比vue里面的nextTick把这个nextTick实现过程搞明白。
nextTick 的实现过程
为什么要用nextTick
我们来一个例子,这个例子是我们先拿到点击按钮前的文本宽度,点击后再次拿到更新后文本的宽度,看是否有变化:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
h2{
display: inline-block;
}
</style>
</head>
<body>
<div id="app">
<h2 ref="h2ref">{{message}}</h2>
<button @click="updateMessage">更新</button>
</div>
<script>
const { createApp,ref,onMounted } =Vue
createApp({
setup(){
const message=ref('hello')
const h2ref=ref(null)
onMounted(()=>{ //不允许在函数里面使用
console.log(h2ref.value.clientWidth)
})
const updateMessage=()=>{
message.value='更新后的message'
console.log('更新后的宽度:'+h2ref.value.clientWidth)
}
return{
message,
h2ref,
updateMessage
}
}
})
.mount('#app')
</script>
</body>
</html>
效果:
点击按钮前:
点击按钮后
解释: 可以看到点击按钮后,文本内容是发生了变化的,但是呢打印出来还59,诶这是为什么呢,其实是因为当我们用 message.value='更新后的message'
这种代码改DOM时,它要经过很多步骤的:依赖收集,再到数据更新(下面有解释),再放入异步队列,所以这时conole.log()
是不会等,会立即执行的,所以打印的还是更新前的宽度。
vue实现响应式的步骤:
- 依赖收集:当首次读取
message
的值时,Vue 会记录下当前活跃的副作用函数(effect)。在这种情况下,副作用函数可能是模板中的表达式,或者是组件的生命周期钩子等。 - 数据更新:当
message.value
被赋新值时,Vue 会检测到这个变化,并将相关的副作用函数标记为脏(dirty)。 - 异步队列:Vue 会把所有标记为脏的副作用函数放入一个异步队列中,以便稍后统一执行。这意味着所有的更新都会在一个微任务(microtask)结束时批量处理,而不是立即执行。这样做可以提高性能,因为频繁的 DOM 更新是非常昂贵的操作。
所以nextTick就是为解决这个问题而打造出来的,我们使用vue
中的nextTick再来看效果:
const { createApp,ref,onMounted,nextTick } =Vue
const updateMessage=()=>{
message.value='更新后的message'
let res=nextTick(()=>{ //保证内部代码会在页面渲染完成后执行
console.log('更新后的宽度:'+h2ref.value.clientWidth)
})
res.then(()=>{
console.log('nextTick执行完毕了')
})
console.log(res)
}
效果:
解释: 其它代码都不变,就改动了updateMessage
和引入了vue
官方nextTick
,可以看到确实有用,并且可以发现vue
中的nextTick
可以return
出来一个promise
的,这个时候已经变成了fulfilled
状态了,可以看到它可以监听DOM
的改变,DOM
改变后再执行代码。
手写nextTick过程
根据上面vue的效果,我们自己模仿动手手写一个:
1.0版本
function nextTick(fn) {
// 接收一个参数 fn,这个参数是一个函数,。
return new Promise((resolve, reject) => {
// 返回一个新的 Promise 对象,因为vue官方里面的nextTick就是return出来了一个值的并且可以接.then
if (typeof MutationObserver !== 'undefined') {
// 检查当前环境是否支持 MutationObserver。
const observer = new MutationObserver(fn);
// 创建一个新的 MutationObserver 实例,传入函数 fn,每当观察到DOM发生变化时,就会调用 fn。
observer.observe(document.getElementById('app'), { attributes: true, childList: true, subtree: true });
// 开始观察 id 为 'app' 的 DOM 元素的变化。
//这里的配置选项为(根据自己的需求):
// attributes: true 表示观察属性的变化;
// childList: true 表示观察子节点的变化;
// subtree: true 表示观察整个子树(不仅仅是直接子节点)的变化。
}
});
}
并且还是上面那个例子:
<body>
<div id="app">
<h2 ref="h2ref">{{message}}</h2>
<button @click="updateMessage">更新</button>
</div>
<script src="./MynextTick.js"></script>
<script>
const { createApp,ref,onMounted } =Vue
createApp({
setup(){
const message=ref('hello')
const h2ref=ref(null)
onMounted(()=>{ //不允许在函数里面使用
console.log(h2ref.value.clientWidth)
})
const updateMessage=()=>{
message.value='更新后的message'
let res=nextTick(()=>{
console.log('更新后的宽度:'+h2ref.value.clientWidth)
})
res.then(()=>{
console.log('nextTick执行完毕了')
})
console.log(res)
}
return{
message,
h2ref,
updateMessage
}
}
})
.mount('#app')
</script>
</body>
效果:
解释: 我们引入自己写的
nextTick
把vue
中的去掉,我们自己写的nextTick
用到了一个MutationObserver
,这是一个js
自带的方法,用来观察DOM
有没有发生改变,所以对于手写nextTick
方便多了,可以看到也实现了效果,DOM
更新后才执行console.log()
。
1.1版本
但是还是有一个问题,promise
的状态没有改变,所以后面接的.then
也就没有执行,所以我们改进一下:
function nextTick(fn) {
return new Promise((resolve, reject) => {
if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(()=>{
fn()
resolve()
observer.disconnect() // 观察结束,清理 observer
})
observer.observe(document.getElementById('app'), { attributes: true, childList: true, subtree: true })
}
})
}
效果:
解释: 既然每当观察到
DOM
发生变化时,new MutationObserver()
就会执行()
里面的函数,并且呢.then
是在fn
函数执行完在执行的,说明promise
的状态是在fn
执行完再变成fulfillled
对吧,那么我们就可以用一个回调函数,回调函数里面先执行fn
再resolve()
把promise
的状态变成fulfillled
,那么就实现了根官方一样的效果了对吧,并且我们观察结束,清理 observer
。
1.2版本(完整版本)
function nextTick(fn) {
return new Promise((resolve, reject) => {
if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(() => {
const res=fn()
if(res instanceof Promise){ //如果fn是promise我们就等它先执行
res.then(()=>{
resolve()
})
}else{
resolve()
}
observer.disconnect()
})
observer.observe(document.getElementById('app'), { attributes: true, childList: true, subtree: true })
}
})
}
上面呢是当fn
为同步代码时,这个时候呢,nextTick
里面有promise
和MutationObserver
,它们都是微任务对吧,所有nextTick
这时就是微任务没有问题吧,那如果fn
为异步代码呢,如果它是宏任务呢,那这时nextTick
是宏任务
还是微任务
呢,我们继续来深入了解一下:
首先我们先看官方
的nextTick
是怎么处理的,下面的nextTick
都是用的官方的
- 如果是
fn
宏任务:
const updateMessage=()=>{
message.value='更新后的message'
let res=nextTick(()=>{
setTimeout(()=>{
console.log('数据请求到了')
})
})
res.then(()=>{
console.log('nextTick执行完毕了')
})
console.log(res)
}
效果:
解释: 其他代码不变,我们引入
setTimeout()
,所以这时fn
是一个宏任务,可以看到如果fn
是一个宏任务,官方的nextTick
是会先执行的(打印'nextTick执行完毕了')再执行宏任务('数据请求到了'),这时候nextTick
是微任务
对吧.
- 如果是用
promise
包裹的宏任务:
const getData=()=>{
return new Promise((resolve)=>{
setTimeout(()=>{
console.log('数据请求到了')
resolve()
})
})
}
const updateMessage=()=>{
message.value='更新后的信息!!!'
let res=nextTick(()=>{
return getData()
})
res.then(()=>{
console.log('nextTick执行完毕了')
})
console.log(res)
}
效果:
解释:诶,我们可以看到
.then
后执行了,宏任务先执行了,这个时候nextTick
是宏任务对吧。
自己手写的nextTick代码解释:看到上面官方的效果,我们知道上面代码中return getData()
是会执行getData
对吧,并返回promise
,所有我们代码中用const res=fn()
接收它,如果它是promise
咱就让它执行完我们再resolve()
,这个时候的nextTick
是宏任务,如果它是同步代码或者是直接是一个宏任务,我们就直接resolve()
那么nextTick
是微任务。
总结
本文到此就结束了,希望对你理解nextTick
有所帮助,当面试官问道nextTick微任务还是宏任务,我们当说它既可以是宏任务也可以是微任务,分情况讲明白就好了,感谢你的阅读!!!