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

Vue3里store的computed咋用?这些场景和技巧得搞懂!

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

为啥要在Vue3的store里用computed?

咱做项目时,store存的是全局状态,但很多场景需要“派生状态”——基于已有状态算出新值,比如购物车页面,商品列表是state里的数组,总价得把每个商品价格加起来;用户模块里,用户信息存了firstNamelastName,要显示全名就得拼接,要是每个用的地方都重复写计算逻辑,既冗余又难维护,而computed(在Pinia里是getter)能解决这些痛点:

  • 缓存性:只有依赖的state变化时,才会重新计算,比如购物车商品没新增/修改,总价不会重复计算,性能更优;
  • 复用性:一个store的computed属性,所有组件都能直接调用,不用每个组件重复写逻辑;
  • 响应式:和Vue的响应式系统深度绑定,state变了,computed值自动更新,页面也跟着刷新。

举个场景:做Todo应用时,store里存了todos数组,想知道“已完成的todo数量”,若不用computed,每个组件都得写todos.filter(t => t.done).length;万一以后过滤逻辑变了(比如还要排除已删除项),所有组件都得改,但把逻辑放store的computed里,只需要改一处,所有组件自动生效。

Pinia里咋给store加computed属性?

在Pinia中,store的computed逻辑通过getter实现(Pinia的getter本质是对Vuecomputed的封装),写的时候分两步:

定义store时配置getters

defineStore定义store,在配置对象里加getters字段,比如做个购物车store:

import { defineStore } from 'pinia'
// 定义购物车store
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [
      { id: 1, name: '键盘', price: 299 },
      { id: 2, name: '鼠标', price: 99 }
    ],
    discount: 0.1 // 10%折扣
  }),
  // getters就是store的computed区域
  getters: {
    // 计算未打折总价
    subTotal: (state) => {
      return state.items.reduce((sum, item) => sum + item.price, 0)
    },
    // 计算折后总价(依赖subTotal和discount)
    totalPrice: (state) => {
      return state.subTotal * (1 - state.discount)
    }
  }
})

组件中使用store的computed属性

在Vue3的setup语法中,引入并调用store即可:

<template>
  <div>
    <p>未打折总价:{{ cartStore.subTotal }}</p>
    <p>折后总价:{{ cartStore.totalPrice }}</p>
  </div>
</template>
<script setup>
import { useCartStore } from './stores/cart'
const cartStore = useCartStore()
</script>

需注意两点:

  • getter函数的参数是state,能直接拿到当前store的状态,TypeScript下还能自动推导类型;
  • 一个getter可以依赖另一个getter(比如totalPrice依赖subTotal),Pinia会自动跟踪依赖关系,只有依赖变化时才重新计算。

store的computed和组件里的computed有啥不一样?

两者核心都是“基于依赖的缓存计算”,但作用域、依赖对象、复用性完全不同:

作用域不同

组件的computed局部的,只在当前组件生效,比如TodoItem组件里写computed(() => props.todo.name.toUpperCase()),其他组件用不到;
store的computedgetter)是全局的,所有引入对应store的组件,都能访问这个派生状态,比如用户模块的userFullName getter,头部导航、个人中心、订单页都能直接用。

依赖对象不同

组件computed依赖组件自身的dataprops或其他局部响应式数据(比如组件内的count ref);
store的computed依赖store的state或其他store的state(比如订单总价依赖订单商品列表和用户会员折扣)。

复用性不同

组件computed无法跨组件复用,每个组件想用类似逻辑只能复制代码;
store的computed天然支持复用,写一次全项目组件都能调用,像“是否有未读消息”这种全局派生状态,放storegetter里,所有需要判断的组件直接调用即可。

写法和限制不同

组件用computed()函数,返回计算值;store在getters对象里写函数,参数是state(也支持this访问state,但TS下更推荐用参数),且store的getter默认缓存,组件computed也缓存,但组件销毁后computed失效,storegetter只要store存在就一直有效。

举个对比:做“用户全名”显示,组件computed和storegetter的区别:

<!-- 组件computed(局部、复用性差) -->
<template>{{ fullName }}</template>
<script setup>
import { useUserStore } from './stores/user'
const userStore = useUserStore()
const fullName = computed(() => `${userStore.firstName} ${userStore.lastName}`)
</script>
<!-- store getter(全局、复用性好) -->
// userStore.js
export const useUserStore = defineStore('user', {
  state: () => ({ firstName: 'John', lastName: 'Doe' }),
  getters: {
    fullName: (state) => `${state.firstName} ${state.lastName}`
  }
})
<!-- 其他组件直接用 -->
<template>{{ userStore.fullName }}</template>
<script setup>
import { useUserStore } from './stores/user'
const userStore = useUserStore()
</script>

明显能看到,storegetter写一次,所有组件直接拿结果;后期要加中间名,只改userStore的fullName即可,所有组件自动更新。

复杂场景下,store的computed咋处理依赖?

实际项目中,store的computed常依赖其他store的state,甚至处理异步数据,这里分两种场景讲:

场景1:依赖其他store的state

比如电商项目,订单最终价格需结合订单商品金额和用户会员折扣(用户信息在userStore),此时在订单store的getter里引入userStore

// userStore.js
export const useUserStore = defineStore('user', {
  state: () => ({
    level: 2, // 会员等级(1-5级)
    levelDiscount: [0, 0.05, 0.1, 0.15, 0.2] // 对应等级的折扣
  })
})
// orderStore.js
export const useOrderStore = defineStore('order', {
  state: () => ({
    goods: [
      { id: 1, name: '卫衣', price: 159 },
      { id: 2, name: '裤子', price: 99 }
    ]
  }),
  getters: {
    totalWithDiscount: (state) => {
      const userStore = useUserStore() // 引入其他store
      const subTotal = state.goods.reduce((sum, g) => sum + g.price, 0)
      const discount = userStore.levelDiscount[userStore.level]
      return subTotal * (1 - discount)
    }
  }
})

关键点:Pinia会自动跟踪跨store的依赖,当userStorelevellevelDiscount变化时,orderStoretotalWithDiscount会自动重新计算——Pinia的响应式系统能检测到依赖变化。

场景2:处理异步或复杂逻辑的依赖

有时computed需基于异步数据(比如接口获取用户等级),但getter本身是同步的,不能直接写async/await(因为computed需返回值,而async函数返回Promise,无法直接渲染),这时候换思路:

  • 把异步逻辑放action里,拿到数据后更新state
  • 再让getter基于更新后的state计算。

举个例子:用户等级需接口获取,流程是:

  1. 调用action里的fetchUserLevel,发请求拿到等级并更新userStorestate
  2. 订单store的getter依赖userStore的等级,自动计算折扣后价格。
// userStore.js
export const useUserStore = defineStore('user', {
  state: () => ({ level: 1 }), // 初始等级
  actions: {
    async fetchUserLevel() {
      const res = await api.getUserLevel() // 假设api是封装的请求
      this.level = res.data.level // 更新state
    }
  }
})
// orderStore.js(和之前一样,getter依赖userStore.level)

组件里调用逻辑:

<template>
  <button @click="fetchLevel">获取用户等级</button>
  <p>折后总价:{{ orderStore.totalWithDiscount }}</p>
</template>
<script setup>
import { useUserStore, useOrderStore } from './stores'
const userStore = useUserStore()
const orderStore = useOrderStore()
const fetchLevel = () => {
  userStore.fetchUserLevel() // 调用action更新state
}
</script>

这样,action执行完后,userStore.level更新,orderStoretotalWithDiscount会自动重新计算,页面也随之更新。

咋给store的computed传参?

有时需让store的computed“动态”起来——比如根据商品ID查购物车数量,或根据分类ID过滤商品,但Pinia的getter默认无参(和Vue的computed一样,因为computed是响应式值,不是函数),这时候用“返回函数”的技巧:

// cartStore.js
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [
      { id: 1, name: '键盘', price: 299, count: 2 },
      { id: 2, name: '鼠标', price: 99, count: 1 }
    ]
  }),
  getters: {
    // 返回函数,接收itemId,返回对应商品数量
    itemCount: (state) => (itemId) => {
      const item = state.items.find(i => i.id === itemId)
      return item ? item.count : 0
    }
  }
})

组件里调用:

<template>
  <p>键盘数量:{{ cartStore.itemCount(1) }}</p>
  <p>鼠标数量:{{ cartStore.itemCount(2) }}</p>
</template>
<script setup>
import { useCartStore } from './stores/cart'
const cartStore = useCartStore()
</script>

但要注意:这种写法会让缓存失效,因为每次调用itemCount(1)都是新函数执行,Pinia无法判断参数是否变化,所以每次都会重新计算,若场景是“低频使用、数据量小”,问题不大;若高频使用(比如列表渲染时每个项都调用),性能会受影响。

既传参又缓存的替代方案:把参数存为state的一部分,比如根据分类ID过滤商品,把“当前分类ID”存在state里,getter基于这个state字段计算:

// productStore.js
export const useProductStore = defineStore('product', {
  state: () => ({
    allProducts: [/* 所有商品 */],
    currentCategory: 'electronics' // 当前分类,由组件设置
  }),
  getters: {
    filteredProducts: (state) => {
      return state.allProducts.filter(p => p.category === state.currentCategory)
    }
  },
  actions: {
    setCategory(category) {
      this.currentCategory = category
    }
  }
})

组件通过action更新currentCategorygetter就会自动重新计算,且有缓存(只有currentCategoryallProducts变化时才重新过滤),这种方式更适合高频场景,性能更优。

store的computed性能咋优化?

getter默认有缓存,但写不好也会拖垮性能,分享几个实用技巧:

拆分复杂getter,利用依赖链缓存

若一个getter逻辑复杂(比如算总价、折扣、优惠券),拆成多个小getter

getters: {
  subTotal: (state) => state.items.reduce(/* 未打折总价 */),
  discountAmount: (state) => state.subTotal * state.discountRate,
  finalTotal: (state) => state.subTotal - state.discountAmount - state.couponValue
}

每个小getter只依赖少量state,只有自己的依赖变化时才重新计算,比如couponValue变化时,只有finalTotal重新计算,subTotaldiscountAmount不受影响,缓存效率更高。

避免在getter里做“重计算”

“重计算”指大循环、深拷贝、频繁DOM操作等耗时操作,比如别在getter里写state.items.map(item => deepClone(item))(深拷贝很耗时),这类逻辑应放action里处理完再存state,或在组件里按需处理。

反面例子:

// 反面教材:getter里深拷贝,性能差
getters: {
  clonedItems: (state) => {
    return deepClone(state.items) // 每次调用都深拷贝整个数组,巨慢
  }
}

改成action处理:

actions: {
  async fetchItems() {
    const res = await api.getItems()
    this.items = deepClone(res.data) // 只在获取时深拷贝一次
  }
}

控制getter的依赖范围

若一个getter依赖的state太多,只要其中一个变化,整个getter就会重新计算,所以尽量让getter依赖“最小化”,比如计算“已完成todo数量”,只依赖todosdone字段,就别让它依赖不相关的state(比如用户信息)。

谨慎使用“传参getter”(返回函数的情况)

前面说过,返回函数的getter会失去缓存,所以只在“低频使用、参数变化少”的场景用,若高频场景(比如列表渲染时每个项都调用),一定要换方案(比如把参数存state,用普通getter)。

和Vuex相比,Vue3(Pinia)的store computed有啥改进?

Vuex是Vue2时代的状态管理库,Pinia作为Vue3推荐方案,在“store computed”(即getter)这块做了不少优化,用过Vuex的同学能明显感知差异:

写法更简洁,和组件逻辑更融合

Vuex的getters需在store实例里配置,用选项式API写法:

// Vuex写法
const store = new Vuex.Store({
  state: { ... },
  getters: {
    total: state => state.items.reduce(...)
  }
})

Pinia用defineStoregetters字段,和stateactions平级,写法更像Vue3组合式API,且TS能自动推导state类型,不用额外写接口:

// Pinia写法
export const useCartStore = defineStore('cart', {
  state: () => ({ items: [], discount: 0.1 }),
  getters: {
    total: (state) => state.items.reduce(...) * (1 - state.discount)
  }
})

类型推导更友好(TS支持)

Vuex用TS时,getters的类型需手动声明,否则this类型不对,而Pinia的getter参数state会自动推导类型,甚至用this访问state时,类型也能正确识别(开启严格模式或合理配置后)。

比如Pinia里写:

export const useUserStore = defineStore('user', {
  state: () => ({ 
    firstName: 'Alice', 
    lastName: 'Smith' 
  }),
  getters: {
    fullName: (state) => `${state.firstName} ${state.lastName}`,
    // 用

版权声明

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

热门