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

Vue3里computed什么时候会触发?触发逻辑藏着哪些细节?

terry 2小时前 阅读数 5 #SEO
文章标签 Vue3 computed

computed 触发的核心条件是什么?

要理解 computed 啥时候触发,得先搞懂 Vue3 响应式系统的依赖追踪逻辑

Vue3 里,computed 本质是个“惰性求值”的响应式依赖消费者,当你定义 const fullName = computed(() => firstName.value + ' ' + lastName.value) 时,Vue 会做这几件事:

  1. 收集依赖:执行 computed 的 getter 函数(也就是 () => firstName.value + ' ' + lastName.value)时,会自动“盯住”里面用到的响应式数据(这里是 firstNamelastName 这两个 ref),这些被用到的响应式数据,computed 的“依赖”。

  2. 标记脏值(dirty):当任何一个依赖的 value 发生变化时,Vue 的响应式系统会给这个 computed 打上“脏”标记,这时候 computed 并没有立刻重新计算,只是记下来“我需要更新了”。

  3. 惰性触发:只有当你主动访问这个 computed 属性(比如模板里渲染 {{fullName}},或者 JS 里写 fullName.value)时,它才会检查自己是否“脏了”,如果脏了,就重新执行 getter 计算新值,同时清除脏标记;如果没脏,直接返回之前缓存的结果。

举个直观的例子:

<template>
  <div>{{ doubleCount }}</div>
  <button @click="changeCount">点我</button>
</template>
<script setup>
import { ref, computed } from 'vue'
const count = ref(1)
const doubleCount = computed(() => {
  console.log('computed 执行了')
  return count.value * 2
})
function changeCount() {
  count.value++ // count 变化,doubleCount 被标记为脏
  console.log('count 变了,但 doubleCount 还没重新计算')
}
</script>

点击按钮后,控制台先打印 count 变了,但 doubleCount 还没重新计算,因为此时只是标记脏值,只有当模板渲染时(或者 JS 里主动访问 doubleCount.value),才会触发 computed 执行了 的打印——这就是“依赖变化 + 被访问”才触发计算的逻辑。

依赖没变化时,computed 为啥坚决不重新计算?

这和 computed 的缓存机制直接相关。

想象一个场景:你做了个搜索组件,输入关键词后过滤列表,如果用 methods 写过滤函数,每次组件渲染(哪怕列表和关键词都没变化),函数都会重新执行;但用 computed 的话,只有列表或关键词变了,才会重新过滤。

这是因为 computed 会把计算结果缓存起来,只要依赖(比如列表和关键词)没变化,不管你访问多少次 computed 属性,它都直接返回缓存值,跳过重新计算。

缓存的好处很明显:减少不必要的计算,提升性能,尤其是当 computed 的 getter 里有复杂逻辑(比如遍历大数组、做数学运算)时,缓存能避免重复消耗性能。

再举个反例感受下:

<template>
  <div>{{ methodResult }}</div>
  <div>{{ computedResult }}</div>
  <button @click="forceRender">强制渲染</button>
</template>
<script setup>
import { ref, computed } from 'vue'
const num = ref(1)
// methods 版本:每次渲染都执行
function getMethodResult() {
  console.log('methods 执行了')
  return num.value * 2
}
// computed 版本:依赖不变时不执行
const computedResult = computed(() => {
  console.log('computed 执行了')
  return num.value * 2
})
function forceRender() {
  // 触发组件重新渲染(实际项目里可能是其他状态变化导致)
  num.value = num.value // 看似没变化,但会触发更新
}
</script>

点击“强制渲染”按钮后,methodResult 对应的 getMethodResult 每次都会打印“methods 执行了”,而 computedResult 只有在 num 真正变化时才会重新计算——这就是缓存的威力。

和 watch 比,computed 的触发逻辑有啥本质区别?

很多新手会混淆 computedwatch,但它们的触发逻辑、适用场景完全不同。

先看 watch 的触发逻辑

watch 是“主动监听”指定数据源,只要数据源变化(不管有没有被使用),就会执行回调,而且它支持异步操作深度监听(比如监听对象的所有属性变化)、立即执行等特性。

举个例子:

<template>
  <input v-model="searchKey" />
</template>
<script setup>
import { ref, watch } from 'vue'
const searchKey = ref('')
watch(searchKey, (newVal) => {
  console.log('searchKey 变了,发请求查数据')
  // 这里可以写异步请求逻辑
})
</script>

只要 searchKey 变化,不管模板有没有用这个值,watch 回调都会执行——这是“主动监听,数据变就触发”。

再看 computed 的触发逻辑

computed 是“被动推导”,只有两个条件同时满足才会触发:

  • 依赖的响应式数据发生了变化
  • 这个 computed 属性被访问了(比如模板渲染、JS 里读取 value)。

computed同步计算,getter 里不能写异步逻辑(否则返回值不是预期的衍生值)。

举个对比的例子:

需求是“把用户输入的姓和名拼成全名,同时在全名变化时记录日志”。

computed 处理“拼全名”(衍生值),用 watch 处理“记录日志”(副作用):

<template>
  <input v-model="firstName" placeholder="姓" />
  <input v-model="lastName" placeholder="名" />
  <div>全名:{{ fullName }}</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const firstName = ref('')
const lastName = ref('')
// computed:被动推导全名,依赖变化+被访问时触发
const fullName = computed(() => `${firstName.value}${lastName.value}`)
// watch:主动监听全名变化,变化就执行(不管有没有被访问)
watch(fullName, (newVal) => {
  console.log('全名变了:', newVal)
  // 这里可以写日志、埋点等副作用逻辑
})
</script>

总结区别:

  • 触发时机:watch 是“数据变就触发”,computed 是“数据变 + 被访问才触发”;
  • 执行逻辑:watch 支持异步/副作用,computed 只做同步衍生值;
  • 性能侧重:computed 靠缓存省性能,watch 更关注“响应变化做操作”。

实际项目里,怎么利用 computed 触发逻辑优化性能?

理解了触发逻辑,就能在项目里“四两拨千斤”地优化,分享几个实用技巧:

技巧1:只把必要数据作为依赖

假设有个组件,要根据“用户是否登录”和“会员等级”显示不同文案,如果写成这样:

const displayText = computed(() => {
  // 多余依赖:theme 根本不影响 displayText
  const theme = useTheme() 
  if (isLogin.value && userLevel.value > 3) {
    return 'VIP 专属文案'
  } else {
    return '普通文案'
  }
})

theme 是无关依赖,会导致只要 theme 变化(哪怕登录状态和会员等级没变),displayText 就会被标记为脏,可能触发不必要的计算。

优化:只保留和结果相关的响应式数据作为依赖:

const displayText = computed(() => {
  if (isLogin.value && userLevel.value > 3) {
    return 'VIP 专属文案'
  } else {
    return '普通文案'
  }
})

技巧2:拆分复杂 computed 为多个小 computed

如果一个 computed 的 getter 里有大量逻辑,购物车总价 = 商品金额 + 运费 - 优惠 + 税费”,全写在一个 computed 里,只要任何一个依赖变化,整个复杂逻辑都会重新计算。

优化:拆成多个小 computed,利用缓存减少重复计算:

// 商品金额(只依赖商品列表)
const goodsTotal = computed(() => {
  return cartGoods.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})
// 运费(依赖商品金额、地址)
const freight = computed(() => {
  return goodsTotal.value > 100 ? 0 : 10
})
// 最终总价(依赖多个小 computed)
const finalTotal = computed(() => {
  return goodsTotal.value + freight.value - discount.value + tax.value
})

这样,只有当“商品列表”变化时,goodsTotal 才会重新计算;“地址”变化时,只有 freight 重新计算……大大减少了重复计算量。

技巧3:结合浅响应式减少追踪开销

如果依赖是一个大对象(比如整个用户信息对象,但只有外层属性变化需要触发 computed),用 reactive 会默认深度追踪所有嵌套属性,导致性能浪费。

这时候可以用 shallowReactiveshallowRef,让 Vue 只追踪外层属性变化:

// 用户信息只有外层变化需要触发 computed(userInfo.isVip 变化)
const userInfo = shallowReactive({
  name: '张三',
  isVip: false,
  detail: { age: 18, address: '北京' } // 内层变化不追踪
})
const vipText = computed(() => {
  return userInfo.isVip ? 'VIP 欢迎您' : '普通用户'
})

这样,只有 userInfo.isVipuserInfo.name 变化时,vipText 才会触发;而 userInfo.detail.age 变化时,不会触发 computed——减少了不必要的依赖追踪。

技巧4:坚决避免在 computed 里写副作用

computed 的设计初衷是“纯函数式的衍生值计算”,如果在 getter 里写异步操作修改其他响应式数据调用接口等副作用,会让触发逻辑失控。

比如这样写就很危险:

const userInfo = ref(null)
const userNickname = computed(async () => {
  // 异步请求是副作用!computed 不支持异步 getter
  const res = await fetch('/api/user')
  userInfo.value = res.data // 修改其他状态,也属于副作用
  return res.data.nickname
})

优化:异步逻辑、状态修改交给 watchmethods

const userInfo = ref(null)
const userNickname = ref('')
watch(someTrigger, async () => {
  const res = await fetch('/api/user')
  userInfo.value = res.data
  userNickname.value = res.data.nickname
})

遇到 computed 不触发的“坑”,怎么快速排查?

开发中偶尔会遇到“明明数据变了,computed 却没更新”的情况,这类问题大多和依赖的响应式特性有关,分享几个排查步骤:

步骤1:检查依赖是否是“响应式数据”

Vue 的响应式系统只对 refreactive 包裹的数据生效,computed 的依赖是普通变量非响应式对象,变化时不会触发 computed。

错误示例:

<script setup>
let normalVar = 1 // 普通变量,非响应式
const wrongComputed = computed(() => normalVar * 2)
function changeVar() {
  normalVar++ // 变化不会被追踪,computed 不触发
}
</script>

修复:用 ref 包裹普通变量:

const normalVar = ref(1) // 响应式变量
const rightComputed = computed(() => normalVar.value * 2)

步骤2:检查深层依赖的追踪方式

如果依赖是对象的深层属性,要注意响应式的“深浅”:

  • reactive 默认是深度响应式,修改 obj.deepProp 会触发依赖;
  • shallowReactive浅层响应式,只有修改 obj 本身(obj = {})才会触发,修改 obj.deepProp 不会;
  • 如果用 ref 包裹对象,修改 obj.value.deepProp 会触发(因为 ref.value 是响应式的)。

错误示例(用了 shallowReactive 却改深层属性):

const user = shallowReactive({
  info: { name: '张三' }
})
const userName = computed(() => user.info.name)
function changeName() {
  user.info.name = '李四' // shallowReactive 下,修改深层属性不触发
}

修复

  • 改用 reactive(深度响应式);
  • 或者修改外层对象(user.info = { name: '李四' });
  • 或者用 ref 包裹整个对象:const user = ref({ info: { name: '张三' } }),修改 user.value.info.name 会触发。

步骤3:检查是否重复定义导致覆盖

setup 里,如果同时用 refcomputed 定义了同名变量,会导致后者覆盖前者,触发逻辑失效。

错误示例:

<script setup>
const count = ref(1)
const count = computed(() => count.value * 2) // 重复定义,覆盖了之前的 ref
</script>

修复:确保变量名唯一。

步骤4:用 Vue DevTools 辅助分析

Vue 官方的 DevTools 插件能直观看到 computed 的依赖列表和触发状态:

  1. 打开 DevTools,切换到组件面板,找到对应的组件;
  2. 查看 Computed 标签页,看目标 computed 的“Dependencies”(依赖)是否包含预期的响应式数据;
  3. 触发数据变化后,看 computed 的“Last evaluated”时间是否更新,判断是否真的触发。

computed 的触发逻辑在 Vue3 升级后有啥变化?

Vue3 对响应式系统做了重构(从 Object.defineProperty 换成 Proxy),这直接影响了 computed 的触发逻辑:

变化1:对数组/集合的响应更及时

Vue2 里,修改数组的索引(arr[0] = 1)或长度(arr.length = 0)不会触发响应式更新,导致 computed 也不触发,Vue3 用 Proxy 可以精准捕获这些操作,只要数组是 reactiveref 包裹的,修改索引、长度、调用 push/pop 等方法都会触发 computed。

示例(Vue3 支持,Vue2 不支持):

const list = reactive([1, 2, 3])
const firstItemDouble = computed(() => list[0] * 2)
function changeFirst() {
  list[0] = 5 // Vue3 里会触发 computed,Vue2 里不会
}

变化2:组合式 API 下的触发更“隐形”

Vue2 的选项式 API(computed 写在 computed 选项里)和 Vue3 的组合式 API(computed 作为函数调用),触发逻辑本质一致,但组合式 API 更灵活,也更容易“踩坑”——比如忘记用 ref 包裹普通变量,导致依赖非响应式。

变化3:性能和触发精度提升

Vue3 的响应式系统通过 Proxy 实现了懒追踪精准依赖收集:只有真正被访问的属性才会被追踪,减少了不必要的依赖收集,反映在 computed 上,就是触发更精准,性能更好(尤其是大型项目里)。


Vue3 里 computed 的触发逻辑围绕“响应式依赖变化 + 被访问”展开,缓存机制和惰性求值是核心特点,理解这些逻辑,不仅能避免“不触发”“重复触发”的坑,还能在项目里通过合理设计依赖、拆分计算逻辑等方式优化性能。

如果你在开发中遇到 computed 相关的问题,不妨回到“依赖是否响应式?是否被访问?”这两个核心点排查——大部分问题都能迎刃而解~

版权声明

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

热门