【VUE】nextTick的简易实现以及原理

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)

虽然 watch1watch2 都调用了 update 方法,但整个过程中只会触发一次 nextTick,这是因为 queueWatcher 函数中使用了 waiting 标志位,确保在同一轮事件循环中只安排一次 flushScheduleQueue 调用,从而避免重复调度。
在函数 nextTick 中,会将当前的回调函数放入到 callbacks 之中,然后等待宏任务 setTimeout 的执行。在这个例子之中,我们可以看到,只有一个回调函数 flushScheduleQueue 放入到 callbacks 之中。最后我们在宏任务 setTimeout 之中执行 flushCallbacks ,这里就将 callbacks 中的函数进行遍历执行。

5. callbacks.push(cb)
6. 轮询callbacks中的回调函数

4. 分析逻辑:去重 + 调度 + 批量更新

上面的例子中,queueWatcher 是核心调度函数,负责:

  1. 去重处理 :通过 has[id] 记录 watcher 是否已加入队列,避免重复更新。
  2. 批量调度 :借助 waiting 标志位,确保同一轮事件循环中只调用一次 nextTick
  3. 延迟执行flushScheduleQueue 被推入宏任务,等 DOM 更新完成后统一执行。

nextTick 则通过回调队列 callbacks 缓存所有回调,并使用 setTimeout 模拟异步更新机制,确保这些回调在 DOM 更新之后再执行

总结

  • nextTick 是为了确保在 DOM 更新完成之后执行回调,用于访问最新的 DOM 状态。
  • 它的底层原理是利用事件循环机制,将回调函数推入微任务(或宏任务)队列中延迟执行。、
  • queueWatcher 通过去重与调度合并机制,避免重复更新,提升性能。
  • 本质上,Vue 利用 nextTick 实现了异步更新策略与高效的视图渲染调度。
相关推荐
_一条咸鱼_几秒前
Android ARouter 基础库模块深度剖析(四)
android·面试·android jetpack
_一条咸鱼_26 分钟前
Android ARouter 核心路由模块原理深度剖析(一)
android·面试·android jetpack
BillKu28 分钟前
Vue3 + TypeScript中provide和inject的用法示例
javascript·vue.js·typescript
_一条咸鱼_32 分钟前
Android ARouter 编译器模块深度剖析(二)
android·面试·android jetpack
培根芝士33 分钟前
Electron打包支持多语言
前端·javascript·electron
Baoing_1 小时前
Next.js项目生成sitemap.xml站点地图
xml·开发语言·javascript
a东方青1 小时前
vue3学习笔记之属性绑定
vue.js·笔记·学习
沉默是金~1 小时前
Vue+Notification 自定义消息通知组件 支持数据分页 实时更新
javascript·vue.js·elementui
Pandaconda2 小时前
【新人系列】Golang 入门(十五):类型断言
开发语言·后端·面试·golang·go·断言·类型
在下千玦2 小时前
#去除知乎中“盐选”付费故事
javascript