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

为什么Vue3里数据更新了页面不渲染?Vue3 nextTick到底怎么用才能解决这类问题?

terry 1小时前 阅读数 19 #Vue

之前用Vue2做项目的时候,偶尔会碰数据已经在控制台打印出来了,但界面上就是没变化的情况,当时找前辈问,前辈说加个this.$nextTick就行,后来转用Vue3,setup语法糖里不能用this了,有一次改完列表项的输入框内容,想自动把最后一个输入框聚焦,数据明明绑定上了,聚焦逻辑却总是失效,试了好几次才想起Vue3里也有nextTick,换成import的版本一用就好,最近刷技术社区,发现很多刚转Vue3的小伙伴还在踩类似的坑,那今天就把我踩坑+查资料+实战总结的关于Vue3 nextTick的所有内容,好好跟大家聊一聊。

Vue3数据更新后页面不渲染,真的是渲染失败吗?

遇到这个问题先别急着下结论,大概率不是Vue的渲染机制出问题了,而是你“看错了时机”。 我们得先搞懂Vue的响应式更新原理的核心:批量异步更新。 不管是Vue2还是Vue3,修改响应式数据的时候,Vue不会立刻一个数据改一次DOM,而是先把这次修改的所有相关的“更新任务”都存到一个队列里,等当前这一轮同步代码执行完,也就是事件循环的宏任务队列清空、微任务队列执行前或者执行过程中,再统一去处理队列里的任务,批量更新DOM。 举个具体的例子你就明白了:比如你在一个函数里连续修改了count值三次,从0到1到2到3,Vue不会每次都把页面上显示count的span标签改三次,而是直接跳过1、2,最终只把span改成3——这样做的好处很明显,减少了DOM操作次数,大大提升了页面的性能。 但有时候我们就是需要在DOM“真的”更新完之后再做一件事,比如刚才提到的自动聚焦最后一个输入框——当你在setup里执行完列表数据的push操作,只是把这个添加任务放进了更新队列,此时DOM里根本还没新增这个输入框呢,你去document.querySelector或者用ref找它,当然找不到,聚焦操作自然就失效了。 那这时候就需要nextTick出场了:nextTick可以帮我们把想要执行的逻辑,推到DOM批量更新完成之后的下一个微任务(或者当前微任务队列的末尾,根据Vue版本和浏览器环境有细微调整,但不用纠结细节,核心就是在“DOM更新完能访问到新节点”的时机)执行。

Vue3里nextTick有几种用法?各适合什么场景?

Vue3里的nextTick和Vue2相比,最大的变化就是没有this了,得通过ES6的import引入,但用法上其实更灵活,有两种主流方式: 第一种是回调函数方式,和Vue2的this.$nextTick用法逻辑完全一致; 第二种是Promise方式,因为Vue3的nextTick本身就返回一个Promise,所以我们可以用async/await来写,代码看起来更清晰,没有回调地狱的风险。

先来讲讲回调函数方式怎么用。 假设我们还是刚才那个列表加输入框自动聚焦的场景,用setup语法糖的话,首先得从vue里引入nextTick和ref:

import { ref, nextTick } from 'vue';

然后定义列表数据和用来绑定新增输入框的ref:

const list = ref(['苹果', '香蕉', '橙子']);
const lastInputRef = ref(null);

接着写添加和聚焦的逻辑:

const addItem = () => {
  // 先往列表里加一个空字符串的元素,触发更新任务
  list.value.push('');
  // 用nextTick把聚焦逻辑放在DOM更新后
  nextTick(() => {
    // 确保lastInputRef.current存在(比如用户快速点击多次可能有问题,但这里简化逻辑先不管)
    lastInputRef.value?.focus();
  });
};

最后在模板里对应绑定就行:

<div v-for="(item, index) in list" :key="index">
  <input 
    v-model="list[index]" 
    v-if="index === list.length - 1" 
    ref="lastInputRef"
  />
  <span v-else>{{ item }}</span>
</div>
<button @click="addItem">添加新水果</button>

这种写法的好处是和Vue2的习惯无缝衔接,适合刚转Vue3的小伙伴快速上手,也适合简单的场景。

接下来讲讲Promise+async/await的方式,这是官方更推荐的写法,尤其是在有多个异步操作需要按顺序执行的时候。 还是刚才的场景,我们把addItem改成async函数:

const addItem = async () => {
  list.value.push('');
  // 等nextTick的Promise resolve之后,再执行下面的聚焦逻辑
  await nextTick();
  lastInputRef.value?.focus();
};

是不是简洁多了?如果我们在addItem后面还要做另一件依赖DOM更新的事,比如给新的输入框设置一个默认的placeholder,直接在await nextTick()后面写就行,完全不会乱。

除了这两种主流方式,还有一种非常非常少用的全局方式,通过app.config.globalProperties.$nextTick来绑定,但setup语法糖里访问全局属性还要用getCurrentInstance,不仅麻烦,还不符合Vue3的Composition API的设计理念,一般不推荐大家用。

除了刚才说的自动聚焦,Vue3 nextTick还有哪些高频实战场景?

刚才的自动聚焦是最典型的,但其实nextTick的应用场景远不止这个,我列几个我平时做项目遇到最多的,大家可以记下来,以后碰到直接用:

获取更新后的DOM尺寸或位置

这个场景太常见了,比如做瀑布流布局、滚动加载、弹窗拖拽定位的时候,都需要获取元素的offsetWidth、offsetHeight、scrollTop这些属性。 比如做一个弹窗,弹窗里的内容是动态加载的,加载完之后需要让弹窗居中显示,如果我们直接在请求接口成功的回调里获取弹窗的高度,那此时弹窗里的内容可能还没渲染,高度就是0或者上一次的旧值,居中肯定会失败,这时候就需要用nextTick,等内容渲染完DOM尺寸更新了,再计算位置。

修复v-if和v-for切换时的样式或行为异常

有时候我们用v-if或者v-for来切换一些有特殊样式(比如动画、过渡)或者绑定了第三方库(比如ECharts、地图、富文本编辑器)的元素,切换的时候会出现样式错乱、第三方库初始化失败、事件绑定失效的问题。 比如你用v-if控制一个ECharts图表容器的显示隐藏,第一次显示的时候没问题,但关闭再打开,图表就不显示了,这是因为第二次打开的时候,Vue虽然重新创建了DOM元素,但之前绑定的ECharts实例还没完全销毁,或者新的DOM元素刚创建但还没挂载到页面上,ECharts初始化找不到尺寸,这时候你可以在v-if的条件变为true之后,加一个nextTick,然后在里面重新初始化ECharts。

在watch或watchEffect里获取更新后的DOM

有时候我们会用watch监听一个数据的变化,然后在回调里做一些和DOM相关的操作,但watch的回调默认是在数据变化之后、DOM更新之前执行的(除非你设置了flush: 'post',这个后面会讲),所以直接在回调里获取DOM也是旧的。 比如你监听一个图片的src变化,然后在回调里获取图片的自然宽度和高度,图片加载可能需要时间,但就算是缓存好的图片,有时候watch回调执行的时候,DOM里的图片标签可能还没替换src,或者替换了但还没渲染出尺寸,这时候就需要用nextTick,或者在watch里设置flush: 'post'。

批量修改多个DOM相关的属性或事件

虽然Vue会自动批量更新DOM,但有时候我们需要手动批量修改一些DOM的属性或事件(比如修改多个class、绑定多个addEventListener),如果直接改的话,可能会触发多次浏览器的重绘或重排,影响性能,这时候我们可以先把这些修改都放在同步代码里执行,然后用nextTick把最终的渲染触发出来(不过这个场景现在用得比较少了,因为Vue的批量更新已经做得很好了,手动优化的空间不大,但知道总没坏处)。

watch和watchEffect的flush选项,和nextTick有什么关系?

刚才提到了watch的flush选项,这里展开讲一下,因为它和nextTick的功能有点重叠,但用法不一样。 watch和watchEffect的flush选项有三个值:'pre'(默认)、'post'、'sync'。

  • 'pre':在数据变化之后、DOM更新之前执行,这是默认值,适合做一些不需要访问更新后DOM的操作,比如修改另一个响应式数据、发起请求前的参数准备;
  • 'post':在DOM更新之后执行,和nextTick的作用完全一样,这时候就不需要再手动加nextTick了;
  • 'sync':同步执行,也就是数据一变化立刻执行回调,完全不经过批量更新队列,这种方式会破坏Vue的性能优化,一般不推荐用,只有在极少数极其特殊的场景(比如需要阻止默认事件但又等不及批量更新)才会用到。

那什么时候用flush: 'post',什么时候用nextTick呢? 如果你的watch或watchEffect的整个回调都是依赖更新后的DOM的,那就用flush: 'post',代码更简洁;如果你的回调里只有一小部分依赖更新后的DOM,前面的部分不需要,那就还是用nextTick,把依赖的那部分放在回调里。

举个例子,刚才的监听图片src变化获取自然宽高的场景: 用nextTick的写法:

import { ref, watch, nextTick } from 'vue';
const imgSrc = ref('https://example.com/old.jpg');
const imgRef = ref(null);
watch(imgSrc, () => {
  // 前面可以做一些不需要访问DOM的操作,比如记录日志
  console.log('图片src变化了');
  // 等DOM更新后再获取宽高
  nextTick(() => {
    console.log('图片自然宽度:', imgRef.value?.naturalWidth);
    console.log('图片自然高度:', imgRef.value?.naturalHeight);
  });
});

用flush: 'post'的写法:

import { ref, watch } from 'vue';
const imgSrc = ref('https://example.com/old.jpg');
const imgRef = ref(null);
watch(imgSrc, () => {
  // 前面的日志也会在DOM更新后执行
  console.log('图片src变化了');
  // 直接获取就行,不需要nextTick
  console.log('图片自然宽度:', imgRef.value?.naturalWidth);
  console.log('图片自然高度:', imgRef.value?.naturalHeight);
}, {
  flush: 'post'
});

这两种写法效果是一样的,大家可以根据自己的喜好和场景选择。

使用Vue3 nextTick的常见误区,你踩过几个?

虽然nextTick看起来很简单,但我在做项目和带新手的时候,发现还是有很多人会踩坑,这里列几个最常见的:

滥用nextTick

很多人一碰到页面不渲染或者DOM操作失效的问题,就不管三七二十一先加个nextTick,这是不对的,nextTick虽然好用,但毕竟是一个异步操作,会增加代码的复杂度,而且如果滥用的话,可能会导致代码的执行顺序难以理解,甚至出现一些难以排查的bug。 所以在加nextTick之前,先想想是不是真的需要:比如是不是数据没有定义成响应式的?是不是ref的.value忘记写了?是不是v-for的key值有问题?是不是操作的DOM元素还没挂载?先排查这些问题,确定是“时机不对”的问题,再加nextTick。

在nextTick的回调里又修改响应式数据

nextTick的回调里修改响应式数据,会触发新一轮的批量更新,虽然Vue能处理,但如果频繁这样做,可能会影响性能,而且代码逻辑也会很乱。 比如你在nextTick的回调里修改了count,然后又修改了name,接着又修改了age,这时候Vue会把这三个修改都放进新的更新队列,然后再批量更新,虽然结果是对的,但不如把这些修改都放在nextTick外面的同步代码里,一次性触发批量更新。

在同步代码里等待nextTick的Promise

这个问题主要出现在刚学async/await的小伙伴身上,比如这样写:

const addItem = () => {
  list.value.push('');
  // 这里没有await,nextTick的Promise会直接返回,但不会等它resolve
  nextTick();
  // 所以这里的代码还是会在DOM更新前执行,聚焦还是会失败
  lastInputRef.value?.focus();
};

一定要记住,如果用Promise方式,要么加await,要么把依赖的代码放在.then()里,否则nextTick就白加了。

以为nextTick能解决所有响应式问题

刚才说了,nextTick只能解决“时机不对”的问题,不能解决其他响应式问题,比如你在Vue3里直接修改数组的索引(比如list.value[0] = '西瓜'),或者直接给对象添加新的属性(比如user.value.age = 18),这些操作在Vue3里虽然用Proxy实现响应式后比Vue2好很多,但有时候还是会出问题(不过Vue3.2之后已经基本解决了数组索引和对象新增属性的响应式问题,但如果是深层的对象或数组,还是推荐用ref或者reactive的API来修改,比如list.value.splice(0, 1, '西瓜'),或者用Vue.set的替代品Vue3里没有Vue.set了,直接用响应式API的方法就行,或者如果是reactive定义的对象,直接赋值就可以)。

总结一下Vue3 nextTick的核心知识点

今天聊了这么多,最后给大家总结一下核心的,方便大家记忆:

  1. Vue的响应式更新是批量异步的,修改数据后不会立刻更新DOM,而是等当前同步代码执行完再统一更新;
  2. nextTick的作用是把逻辑推到DOM批量更新完成之后的时机执行;
  3. Vue3里nextTick有两种主流用法:回调函数方式(适合刚转Vue3的)和Promise+async/await方式(官方推荐,更简洁);
  4. nextTick的高频实战场景有:自动聚焦、获取更新后的DOM尺寸或位置、修复v-if/v-for切换时的异常、在watch/watchEffect里获取更新后的DOM;
  5. watch/watchEffect的flush: 'post'选项和nextTick作用一样,可以根据场景选择;
  6. 使用nextTick要注意避免滥用、不要在回调里频繁修改响应式数据、不要忘记加await或.then()、不要以为它能解决所有问题。

好了,关于Vue3 nextTick的内容就聊到这里啦,希望能帮到正在踩坑的小伙伴,如果你还有其他关于Vue3的问题,欢迎在评论区留言讨论,我们一起学习一起进步~

版权声明

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

热门