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

Vue3项目里组件/页面强制刷新有哪些好用的方法?怎么选才不踩坑?

terry 1小时前 阅读数 32 #Vue

很多刚从Vue2转过来的开发者,或者刚接触Vue3不久的朋友,应该都遇到过这种场景:数据明明用ref/reactive存了,也看似改了值,但页面就是纹丝不动;或者是用了第三方UI组件库,比如Element Plus的表格,重置表单或者切换分页数据后,缓存的临时状态清不干净,非得手动刷新整个页?这时候就需要用到「强制刷新」的手段了,但Vue3里强制刷新的逻辑其实和Vue2有点不一样,要是随便用,可能会踩性能坑、甚至破坏组件的响应式设计初衷,今天咱们就一步步拆解,从原理到用法,再到每种方法的适用场景,帮你选出最靠谱的方案。

为什么Vue3有时候会「假死」不更新?

先别急着找强制刷新的代码,搞明白「为什么不更新」,能帮你从根源上避免大部分问题,也能让你更清楚什么时候才真的需要强制刷新——毕竟能用原生响应式解决的,就别用强制手段嘛。

首先回忆下Vue3的响应式核心原理:它是用Proxy代理对象/数组,Reflect去反射操作来实现的,那常见的「假死」情况有哪些呢?

  • 直接替换了整个响应式对象的根节点:比如你写了let user = reactive({name: '小明'}),然后突然赋值user = {name: '小红'}——这时候原来的Proxy就断了,新的user是普通对象,当然不会触发更新;
  • 修改了数组的长度、或者用索引直接赋值:虽然Vue3对数组的Proxy做了优化,但某些极端情况下(比如在深层嵌套数组里,通过非响应式的中间变量获取索引后赋值),还是可能失效;
  • 修改了对象的未预先定义的深层属性:虽然Vue3不用像Vue2那样用$set/$delete,但如果是非常深的嵌套结构,而且你是先创建空对象/数组,再一层层往里加属性,有时候Vue3的依赖收集可能没覆盖到;
  • 第三方组件库的内部状态封装:这个是最常见的,比如Element Plus的el-form,如果你只清空了绑定的form对象,但表单内部的输入框可能还有自己的local缓存状态,或者el-table的树形展开、行选中这些临时状态,用原生的响应式数据绑定可能清不干净;
  • 路由参数变化但组件复用:比如你从/user/1跳转到/user/2,Vue Router默认会复用UserDetail组件,这时候setup函数不会重新执行,数据自然不会跟着变。

这些情况,前三种其实都可以用原生的响应式写法修正,后面两种才可能需要用到强制刷新,比如第一种,你可以用Object.assign保留原Proxy:Object.assign(user, {name: '小红'});第二种,用数组的变异方法(push、pop、splice等)或者直接替换整个数组;第三种,确保一开始就给嵌套对象/数组预留结构,或者用toRaw转成普通对象修改后,再用reactive重新包一层,但别直接替换原来的响应式变量哦。

Vue3里有哪些强制刷新的具体方法?

说完了根源问题,现在来盘点真正能用的强制刷新方法,我会从「轻量级组件内部刷新」到「重量级页面/大组件树刷新」排序,每种方法都讲清楚用法、原理、适用场景和注意事项。

给组件绑定动态key值(推荐指数:★★★★★)

这个方法应该是目前Vue3强制刷新里最常用、性能最好、几乎没有副作用的方案了,不管是轻量级还是中量级的场景都能用,甚至路由复用组件的问题也能靠它解决。

具体用法:不管是子组件还是当前页面的根组件(根组件的话可以给App.vue里的router-view绑定key),给它加个key属性,然后让这个key的值在需要刷新的时候变化就行。

<!-- 子组件场景 -->
<template>
  <div class="parent">
    <button @click="refreshChild">强制刷新子组件</button>
    <ChildComponent :key="childKey" :data="childData" />
  </div>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childData = ref({ name: '小明', age: 18 })
// 用一个简单的数字当key,每次刷新加1就行
const childKey = ref(0)
const refreshChild = () => {
  // 可以同时更新数据和key,也可以只更新key
  childData.value.age++
  childKey.value++
}
</script>
<!-- 路由复用组件场景 -->
<!-- App.vue -->
<template>
  <div id="app">
    <nav>
      <router-link to="/user/1">用户1</router-link>
      <router-link to="/user/2">用户2</router-link>
    </nav>
    <!-- 用路由的fullPath当key,每次路径变化(包括参数)都会强制销毁重建组件 -->
    <router-view :key="$route.fullPath" />
  </div>
</template>

原理是什么? Vue的虚拟DOM Diff算法里,key是用来识别同一层级的节点是否是同一个元素/组件的唯一标识,如果前后两次渲染的key不一样,Vue就会直接销毁旧的组件实例,然后创建一个全新的实例——这样setup函数会重新执行,computed、watch会重新依赖收集,所有的生命周期钩子(onMounted、onUnmounted这些)都会重新走一遍,第三方组件的内部状态也会被彻底清空,相当于给组件做了一次「全身大换血」。

适用场景

  • 第三方UI组件临时状态清不干净(比如el-form的输入框缓存、el-table的树形展开/行选中);
  • 路由参数变化需要重新加载整个组件(比如从详情页1跳转到详情页2);
  • 组件内部有非常复杂的逻辑,重新写数据更新逻辑太麻烦;
  • 前三种原生响应式修正方法都试过了,还是不管用的极端情况。

注意事项

  • 别用随机数当key!虽然也能刷新,但每次渲染的key都不一样,Vue会频繁销毁重建组件,性能会非常差,尤其是大组件树;
  • 根组件绑定动态key的话,会把整个页面的组件树都销毁重建,这属于重量级操作,尽量别在全局常用的地方用(比如首页的router-view),除非迫不得已;
  • 如果是子组件,尽量只给那个需要刷新的子组件绑定key,别给父组件或者所有子组件都绑。

使用官方提供的forceUpdate方法(推荐指数:★★★☆☆)

这个方法是Vue3官方内置的,但注意哦,它和Vue2里的this.$forceUpdate有点不一样——Vue3的setup函数里没有this,所以得从vue包里引入getCurrentInstance来获取当前组件的实例,然后用instance?.proxy?.$forceUpdate()来调用。

具体用法

<template>
  <div class="component">
    <button @click="forceRefresh">强制刷新当前组件</button>
    <p>当前计数:{{ count }}</p>
    <!-- 这里用一个普通的数组,索引赋值可能失效 -->
    <ul>
      <li v-for="(item, index) in normalList" :key="index">{{ item }}</li>
    </ul>
  </div>
</template>
<script setup>
import { ref, getCurrentInstance } from 'vue'
const count = ref(0)
// 注意这里是普通数组,不是reactive
const normalList = ref(['苹果', '香蕉', '橘子'])
// 获取当前组件实例
const instance = getCurrentInstance()
const forceRefresh = () => {
  // 直接用索引赋值普通数组
  normalList.value[1] = '葡萄'
  // 调用forceUpdate强制触发当前组件的重新渲染
  instance?.proxy?.$forceUpdate()
  // 也可以同时改count
  count.value++
}
</script>

原理是什么? forceUpdate的作用是强制触发当前组件的「重新渲染」,但它不会销毁重建组件实例,setup函数也不会重新执行,computed、watch也不会重新收集依赖——它只会让Vue跳过虚拟DOM Diff的「数据变化检测」步骤,直接用当前的最新数据(不管有没有触发依赖收集)去更新DOM。

适用场景

  • 当前组件内部的某个数据,确实没有触发依赖收集(比如刚才例子里的普通数组索引赋值,或者深层嵌套未预留结构的对象),而且你不想修改数据的结构(比如不想把普通数组改成reactive,或者不想用变异方法);
  • 轻量级的组件刷新,不想销毁重建组件实例(因为销毁重建会触发onUnmounted和onMounted,可能会有额外的开销);
  • 不需要清空第三方组件内部状态的场景(因为forceUpdate不会销毁组件,内部状态还是保留的)。

注意事项

  • 这个方法只能强制刷新当前组件本身,不会刷新它的子组件!如果子组件的数据也没更新,得单独给子组件处理;
  • 别滥用!forceUpdate是破坏Vue响应式设计初衷的方法,尽量用原生的响应式写法修正数据变化问题;
  • getCurrentInstance只能在setup函数或者生命周期钩子函数里调用,别在异步回调(比如setTimeout、axios的then)里直接保存instance的引用,可能会导致内存泄漏,要是必须在异步回调里用,可以在setup函数里先保存const proxy = instance?.proxy,然后在异步回调里用proxy。

用provide/inject传递刷新函数(推荐指数:★★★★☆)

这个方法适合「跨层级组件刷新」的场景,比如爷爷组件想刷新孙子组件,或者兄弟组件之间想互相刷新——要是用动态key的话,可能需要在中间组件里一层层传key值,太麻烦;要是用forceUpdate的话,只能刷新当前组件,没法跨层级。

具体用法: 第一步,在需要触发刷新的「祖先组件」里,provide一个刷新函数,刷新函数的逻辑可以是控制动态key的变化,也可以是调用子孙组件的forceUpdate(不过推荐用动态key,更彻底); 第二步,在需要被刷新的「子孙组件」里,inject这个刷新函数,然后在需要的时候调用; 如果是兄弟组件之间,那就找它们的共同祖先组件provide刷新函数。

举个例子,爷爷组件想刷新孙子组件:

<!-- GrandParent.vue(共同祖先,provide刷新函数) -->
<template>
  <div class="grand-parent">
    <button @click="refreshGrandChild">爷爷刷新孙子</button>
    <ParentComponent />
  </div>
</template>
<script setup>
import { ref, provide } from 'vue'
import ParentComponent from './ParentComponent.vue'
// 给孙子组件准备动态key
const grandChildKey = ref(0)
// provide刷新函数,刷新key
const refreshGrandChildFn = () => {
  grandChildKey.value++
}
// 用provide传递,第一个参数是key,第二个参数是值
provide('refresh-grand-child', refreshGrandChildFn)
// 同时把key provide给ParentComponent,让它传给GrandChild
provide('grand-child-key', grandChildKey)
</script>
<!-- Parent.vue(中间组件,只负责传递key) -->
<template>
  <div class="parent">
    <GrandChildComponent :key="grandChildKey" />
  </div>
</template>
<script setup>
import { inject } from 'vue'
import GrandChildComponent from './GrandChildComponent.vue'
// inject爷爷传过来的key
const grandChildKey = inject('grand-child-key')
</script>
<!-- GrandChild.vue(孙子组件,也可以自己inject刷新函数自己调用) -->
<template>
  <div class="grand-child">
    <p>我是孙子组件,当前时间戳:{{ timestamp }}</p>
    <button @click="refreshMyself">孙子自己刷新自己</button>
  </div>
</template>
<script setup>
import { ref, inject, onMounted } from 'vue'
const timestamp = ref(0)
// inject爷爷传过来的刷新函数
const refreshGrandChildFn = inject('refresh-grand-child')
const refreshMyself = () => {
  // 自己调用刷新函数
  refreshGrandChildFn?.()
}
// 每次组件重新渲染(因为key变了),onMounted都会执行,更新timestamp
onMounted(() => {
  timestamp.value = Date.now()
})
</script>

原理是什么? 其实这个方法的核心还是动态key或者forceUpdate,provide/inject只是解决了「跨层级传递刷新触发信号」的问题——Vue的provide/inject机制可以让祖先组件给所有的子孙组件传递数据/函数,不管中间隔了多少层。

适用场景

  • 跨层级组件刷新(比如爷爷→孙子、太爷爷→曾孙子);
  • 兄弟组件、表亲组件之间互相刷新;
  • 不想在中间组件里写多余的props传递代码的场景。

注意事项

  • provide/inject的key要统一,最好用Symbol类型,避免和其他组件的key冲突;
  • 如果是调用子孙组件的forceUpdate,那得先在子孙组件里provide自己的实例或者forceUpdate函数,然后在祖先组件里inject,再调用——不过这种方式耦合度更高,推荐用动态key;
  • 传递的刷新函数最好是响应式的吗?不用,只要函数里的逻辑是操作响应式变量就行。

替换整个响应式对象/数组的根节点(但要保留变量的Proxy绑定)(推荐指数:★★☆☆☆)

这个方法其实不能算「强制刷新」,但它有时候能起到和强制刷新类似的效果,而且是原生的响应式写法,所以也提一下。

具体用法:刚才说过,直接替换整个响应式变量的根节点(比如let user = reactive({name: '小明'})然后user = {name: '小红'})会断了Proxy,但如果用ref包裹响应式对象/数组,然后替换ref.value的话,就不会断了——因为ref本身是一个响应式引用,ref.value的变化会触发依赖收集。

举个例子:

<template>
  <div class="component">
    <button @click="replaceAllData">替换所有数据</button>
    <p>用户信息:{{ JSON.stringify(user) }}</p>
    <ul>
      <li v-for="item in list" :key="item">{{ item }}</li>
    </ul>
  </div>
</template>
<script setup>
import { ref } from 'vue'
// 用ref包裹响应式对象/数组
const user = ref({ name: '小明', age: 18, gender: '男' })
const list = ref(['苹果', '香蕉', '橘子'])
const replaceAllData = () => {
  // 直接替换ref.value,不会断Proxy,会触发依赖收集和重新渲染
  user.value = { name: '小红', age: 20, gender: '女', address: '北京' }
  list.value = ['葡萄', '西瓜', '草莓']
}
</script>

原理是什么? ref的本质是一个只有value属性的普通对象,Vue会用Proxy代理这个普通对象,当你修改ref.value的时候,Proxy会检测到这个变化,然后触发依赖收集和重新渲染——不管ref.value原来是普通对象、reactive对象还是数组,只要替换了整个ref.value,就会生效。

适用场景

  • 需要一次性替换整个组件的所有数据(比如重置表单);
  • 原来的响应式对象/数组结构太乱,不想一个个修改属性;
  • 前三种原生响应式修正方法都不管用,但又不想用动态key或者forceUpdate的场景。

注意事项

  • 这个方法只能刷新和当前ref绑定的组件部分,不会销毁重建组件实例,第三方组件的内部状态还是保留的;
  • 如果是reactive包裹的对象/数组,不能直接替换整个变量,得用Object.assign或者把reactive改成ref。

用v-if控制组件的显示/隐藏(推荐指数:★☆☆☆☆)

这个方法其实和动态key的原理有点像,但更暴力——v-if会直接控制组件的挂载/卸载,和动态key不同的是,动态key是在同一位置销毁旧组件创建新组件,v-if是先卸载旧组件,等下次条件为true的时候再在同一位置创建新组件,中间可能会有一个「空档期」。

具体用法

<template>
  <div class="component">
    <button @click="toggleComponent">强制刷新组件</button>
    <ChildComponent v-if="isShow" />
  </div>
</template>
<script setup>
import { ref, nextTick } from 'vue'
import ChildComponent from './ChildComponent.vue'
const isShow = ref(true)
const toggleComponent = () => {
  // 先把isShow设为false,卸载组件
  isShow.value = false
  // 然后用nextTick,等下一次DOM更新周期,再把isShow设为true,重新挂载组件
  nextTick(() => {
    isShow.value = true
  })
}
</script>

原理是什么? v-if的作用是根据条件控制组件的挂载/卸载——条件为true时挂载,条件为false时卸载,重新挂载的时候会创建全新的组件实例,和动态key一样。

适用场景

  • 没有其他办法了,只能用这个;
  • 想让组件在刷新的时候有一个「消失再出现」的视觉效果(不过可以用过渡动画实现,没必要用v-if强制刷新)。

注意事项

  • 必须用nextTick!因为Vue的DOM更新是异步的,如果你直接isShow.value = false; isShow.value = true,Vue会合并这两次更新,最终条件还是true,组件不会被卸载;
  • 性能比动态key差,因为中间多了一个卸载和等待nextTick的过程;
  • 会有一个「空档期」,用户体验可能不好,除非加过渡动画。

这么多方法,到底怎么选?

给大家总结一个简单的「选择指南」,按照优先级来:

  1. 先试试原生响应式修正:检查数据是不是用ref/reactive存的?有没有直接替换根节点?有没有用索引直接赋值普通数组?有没有深层嵌套未预留结构?如果能修正,就别用强制刷新;
  2. 如果是第三方组件临时状态清不干净、路由复用组件、极端原生修正无效:选「动态key值」,这是最优解;
  3. 如果是轻量级当前组件刷新、不想销毁重建、第三方组件状态不用清:选「官方forceUpdate」;
  4. 如果是跨层级组件刷新、兄弟组件刷新:选「provide/inject传递动态key刷新函数」;
  5. 如果是一次性替换所有数据、又不想用动态key:选「用ref包裹后替换整个value」;
  6. 万不得已才用:选「v-if控制显示/隐藏」。

最后再提醒几个避坑点

  • 别滥用强制刷新!Vue的响应式设计已经很完善了,99%的问题都能用原生响应式写法解决;
  • 别用随机数当动态key!性能会炸;
  • 别在全局常用的地方给根组件绑定动态key!会导致整个页面频繁刷新;
  • getCurrentInstance别在异步回调里直接保存引用!会内存泄漏;
  • v-if强制刷新必须用nextTick!否则不会生效。

好啦,关于Vue3强制刷新的所有内容就讲到这里,希望能帮到你——以后再遇到页面「假死」的情况,别着急刷新整个页,先按照上面的步骤来排查,选对方法,既快又不踩坑!

版权声明

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

热门