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实现了异步更新策略与高效的视图渲染调度。