技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

this.$nextTick你真的理解了吗?

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/05-this $nextTick你真的理解了吗?
本文目录8 个章节
  1. 一、nextTick 等待的是什么
  2. 二、created + nextTick 不是访问 ref 的生命周期保证
  3. 三、Vue 2 的 nextTick
  4. 四、Vue 3 的 nextTick 与 currentFlushPromise
  5. 五、nextTick 不等于浏览器 paint
  6. 六、事件循环示例
  7. 总结
  8. 官方参考

this.$nextTick你真的理解了吗?

Category(分类): Vue Status: 已校订(Vue 2 与 Vue 3)

nextTick 常被概括为“等 DOM 更新完再执行”,但这句话容易被扩大成三个错误结论:created + nextTick 一定能访问 ref、nextTick 会主动触发渲染、回调执行时浏览器一定已完成绘制。本文保留原文的源码学习路线,并给出更精确的版本边界。

一、nextTick 等待的是什么

Vue 会把同一轮同步代码中的多次响应式变更缓冲、去重,再批量更新组件 DOM。nextTick() 用于等待 Vue 当前待处理的 DOM update flush 完成。

Vue 3 的典型用法:

<script setup lang="ts">
import { nextTick, ref } from 'vue'

const count = ref(0)
const counter = ref<HTMLElement | null>(null)

async function increment() {
  count.value++

  // 此时 Vue 通常尚未 patch DOM
  await nextTick()

  // 此时可以读取本次 Vue 更新后的 DOM
  console.log(counter.value?.textContent)
}
</script>

<template>
  <button @click="increment">增加</button>
  <p ref="counter">{{ count }}</p>
</template>

必须先发生状态变更,再 await nextTick()。如果没有待处理更新,nextTick 不会主动制造一次渲染。

二、created + nextTick 不是访问 ref 的生命周期保证

原文用下面这组完整实验形成了自己的结论。先保留实验,便于对照:

<template>
  <div ref="test1">created:{{ message }}</div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello World!',
    }
  },
  created() {
    this.message = '你好,世界!'

    // 为了在同一次复现实验中继续执行,用 try/catch 记录原来的直接读取错误
    try {
      console.log(this.$refs.test1.innerText)
    } catch (error) {
      console.error(error) // TypeError:test1 此时是 undefined
    }

    // 原实验在普通同步客户端挂载中成功打印“created:你好,世界!”
    this.$nextTick(() => {
      console.log('test1 nextTick:', this.$refs.test1.innerText)
    })
  },
}
</script>

直接读取报错的现象正确:created 时组件尚未挂载,模板 ref 尚不可用。同一钩子中的 $nextTick 回调在原作者特定的普通同步客户端挂载流程里,可能恰好排在首次挂载完成之后,所以该次实验能读取 DOM。

但这只是特定调度路径下的观察结果,不能形成“created + nextTick 一定能访问 ref”的生命周期保证。在尚未 mount、SSR、异步组件、Suspense 或 ref 位于未满足的 v-if 分支时,仍可能拿到 undefined/null

首次访问模板 ref 的可靠写法是 mounted / onMounted

// Options API
export default {
  mounted() {
    console.log(this.$refs.test1?.innerText)
  },
}
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue'

const test1 = useTemplateRef<HTMLElement>('test1')

onMounted(() => {
  console.log(test1.value?.innerText)
})
</script>

<template>
  <div ref="test1">已挂载</div>
</template>

结论应是:

  • 首屏 DOM/ref 初始化:使用 mounted / onMounted
  • 已挂载组件中,状态变化后读取新 DOM:在变化之后使用 nextTick
  • ref 受 v-if、异步组件等控制时,还要等待相应条件真正成立。

三、Vue 2 的 nextTick

Vue 2 会把回调加入 callbacks 队列,并用一个 pending 标记避免重复安排 flush。Vue 2.7 源码选择异步机制的大致顺序是:

  1. 原生 Promise.then
  2. 非 IE 且满足检测条件的 MutationObserver
  3. 原生 setImmediate
  4. setTimeout(flushCallbacks, 0)

截图版本提示: 05-01 标注的是 Vue 2.6.14 历史源码截图,不是下文文字基准 Vue 2.7.16;调度主体相近,细节应以相应版本源码为准。

Vue 2.6.14 历史源码截图(05-01)

原文配图 05-02

3.1 Promise 分支

优先项要求通过源码的原生能力检测,不是任意 Promise polyfill。Vue 2 自身不提供 Promise polyfill;早期 iOS UIWebView 还包含额外空 setTimeout 的兼容 workaround。

原文配图 05-03

3.2 MutationObserver 分支

MutationObserver 用于异步观察 DOM 变更。Vue 2 的 fallback 会创建文本节点并切换其内容,以安排回调刷新;目的不是“操作 DOM 来保证浏览器已经绘制”,而是利用 MutationObserver 的异步通知机制。

childListattributescharacterDatasubtreeattributeOldValuecharacterDataOldValueattributeFilter 是调用 observe(target, options) 时的观察选项字段,不是 MutationObserver 实例的“七个属性”。

截图纠错: 05-04 同属 Vue 2.6.14 历史截图,其中“DOM 任意变化即可保证拿到最新 DOM”等原注释不严谨。MutationObserver 分支只是安排异步 flush,不保证任意 DOM、Vue patch 或浏览器 paint 均已完成;正文以对应版本源码边界为准。

Vue 2.6.14 MutationObserver 历史截图(05-04,原注释有误)

3.3 setImmediate / setTimeout 分支

这两项是前面机制不可用时的宏任务 fallback。setImmediate 不是“立刻同步执行”,setTimeout(fn, 0) 也只表示最小延迟请求,并不保证零毫秒执行。

原文配图 05-05

3.4 Vue 2 用法

this.$nextTick(() => {
  // this 绑定为当前组件实例
})

await this.$nextTick()

this.$nextTick().then(() => {
  // 注意必须先调用 $nextTick()
})

Vue 2 全局 API Vue.nextTick([callback, context]) 有历史上的 context 参数;实例 $nextTick 会绑定当前实例。无 callback 时,仅在环境支持 Promise 的前提下返回 Promise。因此不能无条件说“Vue 2 nextTick 就是 Promise”。

四、Vue 3 的 nextTick 与 currentFlushPromise

Vue 3 当前实现的核心非常简洁:

export function nextTick<T = void, R = void>(
  this: T,
  fn?: (this: T) => R,
): Promise<Awaited<R>> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(this ? fn.bind(this) : fn) : p
}

理解重点不是“它封装成了 Promise”,而是 currentFlushPromise

  • 有正在等待的 scheduler flush 时,nextTick 链到该 Promise,等待主 job 队列和 post-flush callbacks 等本轮调度工作;
  • 没有待处理 flush 时,使用已 resolved 的 Promise,只把后续代码放到一个 microtask,不会主动更新 DOM;
  • 多次同步状态变更通常由同一个 flush 批处理。

Vue 3 全局 nextTick(callback?) 没有 Vue 2 的公开 context 参数;Options API 的 this.$nextTick 是实例绑定版本。

import { nextTick } from 'vue'

state.message = '你好,世界!'
await nextTick()
// 读取 Vue 本次 patch 后的 DOM

五、nextTick 不等于浏览器 paint

await nextTick() 通常意味着 Vue 已完成当前批次的 DOM patch,但不保证:

  • 浏览器已经把新画面绘制到屏幕;
  • CSS 布局、过渡或动画已经完成;
  • 图片、字体、网络请求已经加载;
  • 异步组件或未成立的 v-if 已出现;
  • 第三方库自己的异步任务已完成。

requestAnimationFrame 回调是在下一次绘制之前执行,适合在浏览器准备下一帧时读取布局或安排视觉更新;单次 rAF 不表示该帧已经 paint 到屏幕。若只需要进入下一帧的绘制前回调,可这样写:

await nextTick()
await new Promise((resolve) => requestAnimationFrame(resolve))
// 已进入下一帧绘制前回调;不能据此宣称该帧已 paint

若目标是让一次绘制有机会发生后再继续,实践中常见双 rAF:

await nextTick()
await new Promise((resolve) => {
  requestAnimationFrame(() => requestAnimationFrame(resolve))
})

双 rAF 也不是所有环境和业务目标的绝对保证,后台标签页还可能被节流,仍需按场景验证。等待 CSS 过渡应监听 transitionend 或使用 Vue Transition 钩子;等待图片、字体或第三方库则要等待各自的完成信号。

六、事件循环示例

下面的输出顺序仍适合帮助理解微任务与宏任务:

console.log('同步代码 1')

setTimeout(() => {
  console.log('setTimeout')
}, 0)

Promise.resolve().then(() => {
  console.log('promise.then')
})

console.log('同步代码 2')

输出:

同步代码 1
同步代码 2
promise.then
setTimeout

不过“nextTick 属于微任务”只适合作为 Vue 3 或 Vue 2 命中 Promise/MutationObserver 分支时的简化理解。Vue 2 fallback 到 setImmediate/setTimeout 时是宏任务。真正重要的是 Vue 的调度边界,而不是背一句固定的事件循环口诀。

总结

  • nextTick 等待 Vue 当前批次的 DOM update flush,不主动触发更新。
  • created 时不能直接访问模板 ref;首次 DOM/ref 初始化应使用 mounted / onMounted,不能把 created + nextTick 当作保证。
  • Vue 3 nextTick 基于 currentFlushPromise || resolvedPromise;没有待处理更新时只是等待后续 microtask。
  • nextTick 不保证浏览器已 paint,也不等待网络、动画或异步组件。
  • Vue 2 fallback 顺序是原生 Promise → MutationObserver → 原生 setImmediate → setTimeout,并有环境检测与兼容细节。
  • Vue 2 和 Vue 3 API 签名不同;this.$nextTick().then(...) 中的括号不能省略。

官方参考

作者:路灯下的光 原文链接:https://juejin.cn/post/7177681326861418556 来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS