Code前端首页关于Code前端联系我们

一、nextTick是解决啥问题的?

terry 6小时前 阅读数 10 #Vue
文章标签 nextTick;DOM更新

想搞懂Vue2里的nextTick,得先从Vue的响应式和DOM更新逻辑说起,很多刚接触Vue的同学会碰到“数据改了,DOM却没立刻变化”的情况,这时候nextTick就成了破局的关键,接下来咱从问题场景、用法、原理到实际坑点,一步步把nextTick拆明白~

先想个常见场景:点击按钮显示一个弹框,同时想获取弹框的高度,代码大概长这样:

export default {
  data() { return { show: false } },
  methods: {
    openDialog() {
      this.show = true;
      console.log(document.querySelector('.dialog').offsetHeight); // 可能输出0?
    }
  }
}

为啥会拿到0?因为Vue为了性能,把DOM更新做成异步批量执行的,简单说,数据变化后,Vue不会立刻去改DOM,而是把这些“要更新DOM”的操作先攒起来,等当前所有同步代码跑完,再一次性更新DOM,这样能避免重复操作DOM,减少性能消耗(比如循环里改100次数据,只更1次DOM)。

回到例子,this.show = true触发了数据变化,但DOM更新还没执行(还在“攒操作”阶段),这时候直接去拿DOM高度,自然拿到的是旧状态(弹框还没显示),这时候nextTick的作用就是:把你的回调函数,放到“DOM更新完成后”再执行,保证能拿到最新的DOM状态。

改成nextTick后就正常了:

openDialog() {
  this.show = true;
  this.$nextTick(() => {
    console.log(document.querySelector('.dialog').offsetHeight); // 拿到真实高度
  });
}

nextTick的基本用法有哪些?

Vue里nextTick分两种调用方式,场景不同用法也不同:

组件内用 this.$nextTick

在Vue组件的方法、钩子(比如mounted、methods里),直接通过实例调用 this.$nextTick(回调),回调会在当前组件的DOM更新完成后执行,比如处理组件内部的DOM操作、第三方库初始化(像ECharts、Quill这类依赖DOM的库)。

全局环境用 Vue.nextTick

如果是在非组件文件(比如单独的工具函数、全局JS里操作Vue实例),就用全局方法 Vue.nextTick(回调),作用和实例方法一样,只是调用对象不同。

Promise风格调用(Vue2.1+支持)

除了传回调,nextTick还支持Promise写法,适合喜欢用async/await的同学:

async openDialog() {
  this.show = true;
  await this.$nextTick(); // 等待DOM更新
  const height = document.querySelector('.dialog').offsetHeight;
}

这种写法代码更简洁,在处理复杂异步逻辑时更顺手。

为啥Vue要搞“异步更新队列”?和nextTick有啥关系?

得先理解Vue的性能优化逻辑:避免重复操作DOM

比如循环里频繁改数据:

for (let i = 0; i < 100; i++) {
  this.count++;
}

如果每次this.count++都立刻更新DOM,相当于触发100次DOM重绘/回流,性能直接爆炸,Vue的解决办法是:把这些“要更新DOM”的操作丢进一个异步队列,等同步代码全执行完,再一次性处理队列里的操作(去重后只执行一次),这样不管循环改多少次,DOM只更新一次,性能拉满。

但这就带来一个问题:数据变化后,DOM不会立刻更新,如果我们想在DOM更新后做操作(比如拿最新高度、初始化第三方库),普通代码会因为“DOM还没更新”拿到错误结果,这时候nextTick就充当了“桥梁”——它让我们的回调,精准地等到“异步更新队列执行完毕(DOM更新后)”再执行。

Vue的异步更新队列是性能优化的核心,nextTick是开发者在这个优化逻辑下,操作最新DOM的“钥匙”。

nextTick的实现原理是啥?

核心逻辑就一句话:把回调延迟到下一个“事件循环周期”执行,保证DOM更新后再触发,但具体怎么延迟?Vue2里用了“微任务优先,宏任务兜底”的策略。

微任务 vs 宏任务

JS的事件循环里,任务分微任务(比如Promise.then、MutationObserver)和宏任务(比如setTimeout、setInterval),微任务的执行时机更早(当前宏任务执行完,立刻执行所有微任务,再渲染DOM),所以Vue优先用微任务来触发nextTick的回调,这样能更快拿到DOM更新后的结果。

Vue2的具体实现

Vue维护了一个回调队列(callbacks),每次调用nextTick,就把回调push到这个队列里,然后判断当前是否正在执行异步任务:如果没在执行,就启动一个异步任务(优先用Promise.then,不支持的话用MutationObserver,再不行用setTimeout这类宏任务),等这个异步任务触发时,把队列里的所有回调依次执行。

举个简化版流程:

  1. 调用this.$nextTick(cb) → cb被加入callbacks队列。
  2. Vue检查是否有正在运行的异步任务 → 没有的话,启动一个微任务(比如Promise.resolve().then(flushCallbacks))。
  3. 微任务触发时,执行flushCallbacks → 把callbacks里的所有cb依次执行。

这样就保证了:所有同步代码执行完 → 异步更新队列处理DOM → nextTick的回调执行(此时DOM已经更新)。

实际开发中,哪些场景必须用nextTick?

这几个场景如果不用nextTick,大概率会踩坑:

数据变化后,操作最新DOM

除了前面的“获取元素高度”,还有比如:

  • 修改列表数据后,滚动到新增项的位置(得等列表DOM更新后,再调scrollIntoView)。
  • 用v-html渲染富文本后,初始化里面的交互组件(比如给按钮绑事件,得等HTML渲染完)。

组件间通信,传递最新状态

子组件内部数据变化后,要通知父组件“我更新完了”,比如子组件用v-if控制一个弹框显示,弹框显示后要告诉父组件“可以做后续操作了”,这时候得在子组件的nextTick里发射事件,父组件收到的才是“弹框已经显示”的状态。

生命周期钩子中操作DOM

比如在created钩子(此时DOM还没挂载)里修改数据,想操作基于该数据渲染的DOM,必须用nextTick:

created() {
  this.list = [1,2,3]; // 渲染列表的数据
  this.$nextTick(() => {
    // 这里list对应的DOM已经渲染,可以操作列表项
    document.querySelectorAll('.list-item')[0].style.color = 'red';
  });
}

配合v-if/v-show切换DOM

v-if是“销毁/重建”DOM,v-show是“显示/隐藏”,比如点击按钮显示一个v-if控制的弹框,想在显示后获取弹框的DOM节点,必须等nextTick:

open() {
  this.dialogShow = true;
  this.$nextTick(() => {
    const dialog = document.querySelector('.dialog'); // 此时能拿到DOM
  });
}

不用nextTick会有啥坑?

举几个真实踩坑案例:

第三方库渲染异常

比如用ECharts做图表,数据变化后调用setOption,如果不等DOM更新就调用,ECharts拿到的是旧容器尺寸,图表会渲染错位,必须用nextTick等DOM更新后,再调setOption

逻辑判断错误

比如做一个“加载更多”功能,滚动到底部后请求数据,把新数据push到列表,如果不等DOM更新就计算滚动高度,会误以为“还没到底”,导致加载逻辑失效。

用户体验割裂

比如点击按钮后,先显示“加载中”弹窗,再发请求,如果不用nextTick,弹窗的DOM还没渲染,请求就发出去了,用户会看到“请求都完了,弹窗才出现”,体验很怪。

Vue3的nextTick和Vue2有啥区别?

虽然咱聊的是Vue2,但了解演进能更懂设计逻辑:

Vue3里nextTick的作用和用法和Vue2基本一致(还是等DOM更新后执行回调),但内部实现因为响应式系统换成了Proxy,异步更新的调度逻辑更高效了,不过对外的API(this.$nextTickVue.nextTick)没变化,Promise风格调用也保留着,所以学透Vue2的nextTick,迁移到Vue3基本无压力~

nextTick是Vue异步更新机制下的“补位工具”——既让框架能高效更新DOM,又给开发者留了操作最新DOM的口子,记住核心逻辑:数据变了别着急操作DOM,丢给nextTick兜底,很多奇怪的Bug就迎刃而解啦~如果还想深挖,可以去看Vue2源码里的nextTick模块,结合事件循环的知识,理解会更透彻~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门