前言:目前来说,nextTick
我们遇到的比较少,至少对我来说是这样的,但是有一些聪明的小朋友早早就注意到这个知识点了。nextTick
是前端开发(尤其是 Vue 生态)中的核心知识点,原理上跟Vue的异步更新有密切关系,对于面试者考察很有区分度,如果能回答的很好,对面试也是很有帮助的,所以我们有必要花费时间来学习一下。
我们来看看官方的定义:
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
看完是不是有一堆问号?没关系,我们来拆分一下关键词
下次 DOM 更新循环结束之后
?
延迟回调
?
更新后的 DOM
?
下次 DOM 更新循环结束之后
Vue更新DOM是有策略的,不是同步更新,是异步的,批量更新DOM,避免重绘导致的性能损耗(比如连续修改数据时仅触发一次渲染)
如果你学过防抖和节流的话,有没有感觉这里逻辑有点相像,他们都是前端性能优化手段,那么我们顺便就来复习一下吧。
他们有着共同目标:减少高频操作带来的性能损耗。
实现逻辑上的对比:
机制 | Vue异步更新 | 防抖(Debounce) | 节流(Throttle) |
---|---|---|---|
触发条件 | 数据变化 | 事件触发(如resize) | 事件触发(如scroll) |
执行策略 | 合并同一事件循环内的所有修改 | 延迟执行,仅保留最后一次操作 | 固定间隔执行一次 |
应用场景 | 虚拟DOM批量渲染 | 搜索框输入联想 | 滚动加载更多 |
核心区别:
Vue异步更新:
依赖浏览器事件循环
自动完成,开发者无需手动控制
防抖/节流:
需开发者显式编码实现
作用于业务逻辑层而非框架底层
呼~复习完了,我们进入正题
这个关键词跟浏览器的事件循环有关,我们来简单回顾一下:
JS是一门单线程的语言,因为它运行在浏览器的渲染主线程中,而主线程只有一个。而渲染主线程承担着许多的工作,渲染页面、执行JS都在其中执行。如果使用同步的方式,极有可能导致主线程产生阻塞,从而导致消息队列中的许多其他任务无法得到执行,所以浏览器采用异步的方式来避免。
console.log('脚本开始')
setTimeout(() => console.log('定时器'), 0)
Promise.resolve().then(() => {
console.log('Promise')
setTimeout(() => console.log('嵌套定时器'), 0)
})
console.log('脚本结束')
输出: /* 输出顺序: 脚本开始 脚本结束 Promise 定时器 嵌套定时器 */
延迟回调
这里的"延迟"不是指setTimeout那样的宏任务延迟,而是指将回调函数推入微任务队列(microtask queue)等待执行。Vue会根据运行环境自动选择最优的实现方式:
优先使用 Promise.then()
:
如果当前环境支持 Promise
,Vue 会优先使用 Promise.then()
来实现微任务。
这是现代浏览器中最高效的方式,因为它直接利用了 JavaScript 的微任务机制。
降级方案:MutationObserver
:
如果当前环境不支持 Promise
,但支持 MutationObserver
,Vue 会使用 MutationObserver
作为替代方案。
MutationObserver
的回调会被加入微任务队列,虽然它主要用于监听 DOM 变化,但也可以用来实现微任务的效果。
降级方案:setImmediate
:
如果当前环境不支持 Promise
和 MutationObserver
,但支持 setImmediate
,Vue 会使用 setImmediate
。
setImmediate
是 Node.js 和 IE10+ 提供的一种机制,虽然它是宏任务,但它的执行时机比 setTimeout(fn, 0)
更早。
兜底方案:setTimeout(fn, 0)
:
如果当前环境不支持 Promise
、MutationObserver
和 setImmediate
,Vue 会使用 setTimeout(fn, 0)
作为兜底方案。
虽然 setTimeout(fn, 0)
是宏任务,但它是最通用的实现方式,确保在当前同步代码执行完毕后执行回调。
更新后的 DOM
由于Vue的响应式更新是异步的,直接修改数据后立即访问DOM获取的是旧值。通过nextTick
可以确保获取到的是最新渲染结果:
{{ num }}
打印结果:
第一次打印:1
第一次打印DOM:0
第二次打印:1
第二次打印DOM:1
总的来说,nextTick就是一个工具,用来确保在 Vue 更新完 DOM 之后,再执行某些操作。它特别有用,因为有时候你修改了数据,Vue 需要一点时间来更新 DOM,而 nextTick
可以让你在 DOM 更新完成之后,立即执行你需要的操作。
获取更新后的元素尺寸/位置
基于新DOM初始化第三方库(如图表库)
Vue 3 nextTick 示例
父子组件生命周期执行顺序导致的问题:
如自动聚焦输入框:
同步代码执行完毕后,事件循环开始处理微任务队列。
执行微任务队列中的 Promise
回调,输出 Promise
。
在 Promise
回调中,setTimeout(() => console.log('嵌套定时器'), 0)
被加入宏任务队列。
微任务队列清空后,事件循环开始处理宏任务队列。
执行第一个宏任务 setTimeout(() => console.log('定时器'), 0)
,输出 定时器
。
执行第二个宏任务 setTimeout(() => console.log('嵌套定时器'), 0)
,输出 嵌套定时器
。
执行顺序:同步代码 → 微任务队列 → 宏任务队列
同步代码执行:首先执行当前的同步代码。
清空微任务队列:在同步代码执行完毕后,事件循环会立即清空微任务队列,依次执行微任务队列中的所有任务。
执行宏任务:清空微任务队列后,事件循环会从宏任务队列中取出一个任务执行。
重复步骤 2 和 3:每次执行完一个宏任务后,事件循环会再次清空微任务队列,然后继续执行下一个宏任务。
宏任务:通常用于延迟执行、异步操作或与浏览器的 UI 渲染相关。它们的执行时机在事件循环的每次迭代中,每次只执行一个。(setTimeout
和 setInterval
setImmediate
I/O 操作 UI 渲染)
微任务:通常用于需要在当前任务执行完毕后立即执行的回调。它们的优先级高于宏任务,会在每次宏任务执行完毕后立即清空。(Promise
MutationObserver
queueMicrotask
)
注意,这里为了方便理解,我们把任务笼统归类为宏任务和微任务,现在对于宏任务和微任务已经有了更细的划分,大家可以去了解一下,但是这里只是为了让大家方便理解,采用了宏任务和微任务的说法,其实也是对的,但是这样说比较旧
猜猜最后打印出什么结果
Vue 3 nextTick 示例
首先执行同步操作,打印1
执行微队列的任务promise和nextTick,打印2和3
执行宏队列任务,打印4
猜猜最后打印出什么结果
Vue 3 nextTick 示例
打印:22
先执行同步操作,先后吧count的值改为1和2,然后执行微队列的任务,打印两次count的值
整理了一些面视可能会问的问题
Q:nextTick的实现原理是什么? A:基于JavaScript事件循环机制,优先使用微任务队列实现异步回调。Vue会维护一个回调队列,在同一个tick中多次调用nextTick只会向队列添加任务,最终通过异步API批量执行。
Q:nextTick与setTimeout(fn, 0)的区别? A:nextTick会优先使用微任务(执行早于setTimeout),且能自动选择最优的异步方案。setTimeout属于宏任务,执行时机更晚。
Q:为什么Vue选择异步更新DOM? A:主要考虑两点:
性能优化:合并同一事件循环中的所有数据变更
逻辑合理性:确保无论修改多少次数据,组件只更新一次
Q:nextTick回调中再修改数据会怎样? A:会进入新的更新周期,可能触发额外的渲染。应避免这种嵌套使用。
Q:为什么 nextTick
回调中修改数据可能导致额外渲染? A:因为 nextTick
回调执行时,当前渲染周期已结束。若在回调中修改数据,会触发新的渲染周期(类似“连锁反应”)。
A:
1、确保 DOM 更新完成后执行某些操作(如获取元素尺寸、滚动位置等)。
2、在子组件挂载后执行某些操作(如调用子组件的方法)。
3、在批量更新数据后,确保所有更新完成后再执行某些操作。
A:
通过确保在 DOM 更新完成后执行回调,nextTick
可以避免在 DOM 更新过程中进行不必要的操作,从而减少性能开销。
上面只是一部分,多的大家可以自己下去搜索一些
bilibili 浏览器事件循环原理哔哩哔哩bilibili
官方网站 nextTick | Vue3
腾讯元宝 豆包 kimi
我自己
爱学习的你们