Vue 中的 nextTick
到底干了什么?
在使用 Vue 的过程中,很多同学可能会遇到这样的场景:明明修改了数据,却无法立刻获取最新的 DOM。这时候你可能会听说,"用 nextTick
就好了"。那 nextTick
究竟是做什么的?它背后的实现原理又是什么?这篇文章我们来聊聊这个常被忽视却非常重要的 API。
1. nextTick
是用来干什么的?
Vue 是一个响应式框架,它会在数据变化时自动更新视图。但为了提升性能,Vue 并不会在每次数据变更时都立刻操作 DOM,而是采用"异步更新"的策略:将所有的数据变更缓存起来,等下一个"事件循环"中一起更新 DOM。
这时候就会有个问题:我们在修改数据后立即访问 DOM,拿到的其实还是"旧的" DOM 结构 。这时候就需要 nextTick
出场了。
通俗点讲 :数据改了,但 DOM 还没改完。你要等 DOM 真改完了再做事,那就用
nextTick(fn)
。
2. nextTick
的原理是什么?
nextTick
的核心原理是利用 JavaScript 的事件循环机制 ------ 将回调函数插入到 微任务 或 宏任务队列 中,从而实现"延迟执行"。
我们都知道事件循环的顺序是:
- 当前同步任务执行完毕
- 执行所有微任务(如
Promise.then
) - 执行一个宏任务(如
setTimeout
)
Vue 的 nextTick
就是将回调函数放入这些任务队列中,等 DOM 渲染完毕之后再执行回调。
3. 模拟实现:深入理解调度机制
下面通过一个精简版的模拟实现,来看 Vue 如何利用 nextTick
和调度机制优化视图更新。
js
let uid = 0
class Watcher {
constructor() {
this.id = ++uid
}
update() {
console.log('watch ' + this.id + ' update')
queueWatcher(this)
}
run() {
console.log('watch ' + this.id + ' view is updating')
}
}
let callbacks = []
let pending = false
function nextTick(cb) {
callbacks.push(cb)
if (!pending) {
pending = true
setTimeout(flushCallbacks, 0)
}
}
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; ++i) {
copies[i]()
}
}
let has = {}
let queue = []
let waiting = false
function flushScheduleQueue() {
let watcher, id
for (let index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
watcher.run()
}
waiting = false
}
function queueWatcher(watcher) {
const id = watcher.id
// 防止重复
if (has[id] == null) {
has[id] = true
queue.push(watcher)
if (!waiting) {
waiting = true
nextTick(flushScheduleQueue)
}
}
}
(function () {
let watch1 = new Watcher()
let watch2 = new Watcher()
watch1.update()
watch1.update()
watch2.update()
})()
这里的输出为
js
watch 1 update
watch 1 update
watch 2 update
watch 1 view is updating
watch 2 view is updating
可以看到,这里的watch1的view更新只进行了一次。这是因为在queueWatcher函数中进行了一个去重处理,queue的队列之中只存放了两个watcher,分别是watch1和watch2。
分析代码
我们首先可以在立即执行函数中可以看到,对
watch1
还有watch2
进行了声明,然后分别执行了update
函数,然后在update
函数之中,我们对queueWatcher
函数进行了调用。
1. watch1.update()
2. queueWatcher(this)
在
queueWatcher(this)
函数内部,我们可以看到,进行了一个去重处理,使用 id 作为标记,然后将watcher
放到queue
中,最后再调用nextTick
来进行更新。
3. queue.push(watcher)
4. nextTick(flushScheduleQueue)
虽然
watch1
和watch2
都调用了update
方法,但整个过程中只会触发一次nextTick
,这是因为queueWatcher
函数中使用了waiting
标志位,确保在同一轮事件循环中只安排一次flushScheduleQueue
调用,从而避免重复调度。
在函数nextTick
中,会将当前的回调函数放入到callbacks
之中,然后等待宏任务setTimeout
的执行。在这个例子之中,我们可以看到,只有一个回调函数flushScheduleQueue
放入到callbacks
之中。最后我们在宏任务setTimeout
之中执行flushCallbacks
,这里就将callbacks
中的函数进行遍历执行。
5. callbacks.push(cb)
6. 轮询callbacks中的回调函数
4. 分析逻辑:去重 + 调度 + 批量更新
上面的例子中,queueWatcher
是核心调度函数,负责:
- 去重处理 :通过
has[id]
记录 watcher 是否已加入队列,避免重复更新。 - 批量调度 :借助
waiting
标志位,确保同一轮事件循环中只调用一次nextTick
。 - 延迟执行 :
flushScheduleQueue
被推入宏任务,等 DOM 更新完成后统一执行。
而 nextTick
则通过回调队列 callbacks
缓存所有回调,并使用 setTimeout
模拟异步更新机制,确保这些回调在 DOM 更新之后再执行。
总结
nextTick
是为了确保在 DOM 更新完成之后执行回调,用于访问最新的 DOM 状态。- 它的底层原理是利用事件循环机制,将回调函数推入微任务(或宏任务)队列中延迟执行。、
queueWatcher
通过去重与调度合并机制,避免重复更新,提升性能。- 本质上,Vue 利用
nextTick
实现了异步更新策略与高效的视图渲染调度。