目录
前言
在了解nextTick()之前,需要先了解JavaScript的“事件循环机制”和Vue的“异步更新策略”。
一、JavaScript的事件循环机制
1.1执行栈
JavaScript是“单线程”的,所有代码在“执行栈”中依次执行。
即:在代码中,代码执行顺序为从上至下,依次执行
console.log('1')
function foo() { console.log('2') }
foo()
console.log('3')
// 执行顺序:1 → 2 → 3
1.2任务队列
异步操作(setTimeout、Promise)等会将回调函数放入任务队列,等待执行栈为空时执行。
即:所有同步代码执行完后,再统一执行需要异步操作的代码
任务队列分为两种:
- 宏任务:setTimeout、setInterval、I/O操作、UI渲染等
- 微任务:Promise.then、MutationObeserver
1.2.1宏任务
宏任务是事件循环中的“宏观任务”。
常见场景:
- 定时器:setTimeout、setInterval
- I/O操作:文件读写、网络请求
- UI渲染:浏览器的DOM更新、布局计算
- 事件回调:鼠标点击、键盘输入等事件的回调
执行规则:
- 事件循环的一个周期内,先执行一个宏任务
- 宏任务执行过程中,可以同步产生新的宏任务或微任务
- 宏任务执行完毕后,会立即执行所有微任务(直到微任务队列为空)
1.2.2微任务
微任务是在当前宏任务执行结束后、下一个宏任务开始前执行的“微观任务”,优先级高于宏任务
常见场景:
- Promise回调:Promise.then()、Promise.catch()
- MutationObeserver:监听DOM变化的API
- Vue的nextTick:Vue内部基于Promise实现的微任务
执行规则:
- 微任务会在“当前宏任务执行完毕后,下一个宏任务开始前执行”
- 统一宏任务中产生的多个微任务会被“批量执行”(清空微任务队列)
- 微任务执行过程中可以继续产生新的微任务,这些微任务会立即加入队列并立即执行
1.3事件循环
事件循环的工作流程:
- 执行栈所有同步代码执行完毕
- 处理“微任务队列”中的所有任务(直到队列为空)
- 执行一个宏任务(UI渲染)
- 重复步骤2和3
1.4宏任务与微任务执行例子
下面是一个宏任务与微任务的执行示例:
console.log('同步代码开始') // 同步代码
// 宏任务 1:setTimeout
setTimeout(() => {
console.log('setTimeout 回调(宏任务1)')
// 微任务:在宏任务中添加新微任务
Promise.resolve().then(() => {
console.log('setTimeout 中的微任务')
})
}, 0)
// 宏任务2:setTimeout
setTimeout(() => {
console.log('setTimeout 回调(宏任务2)')
})
// 微任务 1:Promise.then
Promise.resolve().then(() => {
console.log('Promise.then 回调(微任务)')
// 微任务:在微任务中添加新微任务
Promise.resolve().then(() => {
console.log('微任务中的微任务')
})
})
console.log('同步代码结束') // 同步代码
执行结果:
执行顺序分析:
- 首先顺序执行所有“同步代码”
- 之后,执行“微任务队列”中的所有微任务
- 之后,从“宏任务队列”中取出一个宏任务执行
- 之后,再次执行“微任务队列”中的所有微任务
- 之后,再次从“宏任务队列”中取出一个宏任务执行
- 最后,微任务队列和宏任务队列全部为空,但是事件循环仍然在循环并不断检测宏任务队列和微任务队列
二、Vue的异步更新策略
2.1为什么Vue选择异步更新
我们都知道Vue最大的特点在于它的“响应式更新”,但是如果每次数据更新都立即更新DOM,会导致大量的DOM操作,影响性能
// 同步更新会触发三次DOM重渲染
data.count = 1;
data.count = 2;
data.count = 3;
// 异步更新只会触发一次DOM重渲染
setTimeout(() => {
Promise.resolve().then(() => {
data.count = 1;
data.count = 2;
data.count = 3;
})
}, 0);
2.2Vue的异步更新实现
当我们修改Vue的响应式数据时:
- Vue会将DOM更新延迟到下一个“tick”
- 同一轮事件循环中的所有数据变更会被“合并”
- 所有变更处理完成后,Vue会“批量更新DOM”
注意:
- 正因为Vue的“批量更新DOM”,所以在我们修改数据后,无法立即获取更新后的DOM
- Vue中的DOM更新是在“微任务阶段”执行的
2.3nextTick
nextTick是Vue中一个处理“异步操作”的回调函数
nextTick本质上:“将回调函数放入微任务队列”,因此只要执行DOM操作后,接着使用nextTick()即可获取更新后的DOM
注意:
nextTick()的执行时机也可能会影响调用结果,即nextTick()插入微任务队列的先后位置,也将影响结果。
nextTick()可以正确调用微任务队列前面任务已经执行的结果,不能调用nextTick()之后未执行的结果。
详细例子如下:
import { ref, nextTick } from 'vue'
const A = ref(0)
const B = ref(0)
// 模拟微任务队列中的三个任务
const enqueueMicrotasks = () => {
// 任务1:修改响应式变量A
Promise.resolve().then(() => {
A.value = 100
console.log('任务1:修改A为', A.value)
})
// 任务2:nextTick回调,读取B的最新值
nextTick(() => {
console.log('任务2:nextTick读取B的值为', B.value) // 输出 0(未被任务3修改)
})
// 任务3:修改响应式变量B
Promise.resolve().then(() => {
B.value = 200
console.log('任务3:修改B为', B.value)
})
}
// 执行微任务队列
enqueueMicrotasks()
执行结果:
可以看到,nextTick并没有正确读取B的值,因为B的修改被单独作为一个微任务排放在nextTick()回调的后面,所以自然调取不到。
另一个nextTick()例子:
// 模拟微任务队列中的三个任务
const enqueueMicrotasks = () => {
// 任务1:修改响应式变量A
A.value = 100
// 任务2:nextTick回调,读取B的最新值
nextTick(() => {
console.log('任务2:nextTick读取B的值为', B.value)
console.log('任务2:nextTick读取A的值为', A.value)
})
// 任务3:修改响应式变量B
B.value = 200
}
// 另一个微任务队列
const enqueueMicrotasks2 = () => {
A.value = 300
}
// 执行微任务队列
enqueueMicrotasks()
enqueueMicrotasks2()
执行结果:
- enqueueMicrotasks和enqueueMicrotasks2都是同步代码,包括里面的A.value和B.value这三个赋值语句都是同步代码,因此先依次执行这三段同步代码语句
- nextTick()是微任务,因此在三段同步代码语句执行完毕后,会获取到最新的DOM元素
结论:
- 对响应式数据的修改不被单独作为一个微任务时,nextTick()此时放置在任何一个位置都可以获取到新数据
- 在Promise.then、Promise.catch中对响应式数据的修改,会单独作为一个微任务放置在微任务队列中,这个时候要正确使用nextTick()回调的位置
2.4使用nextTick的最佳时机
- 当需要确保DOM已经更新时
- 当需要访问组件引用或DOM元素时
- 当需要在数据变更后执行依赖DOM操作时