Pinia Getters 和 Actions:状态管理的强大组合
大家好,今天我们来深入探讨 Pinia 中 getters
和 actions
的使用,以及它们如何共同构建强大且易于维护的状态管理解决方案。Pinia 作为 Vue.js 生态系统中备受欢迎的状态管理库,以其轻量级、类型安全和易于使用的特性而著称。getters
和 actions
是 Pinia store 中两个至关重要的组成部分,它们分别负责状态的派生和状态的修改。
理解 Getters:从状态中派生数据
getters
本质上是 store 的计算属性。它们接收 state 作为参数,并返回基于 state 的计算结果。 与直接在组件中使用计算属性相比,getters
的主要优势在于它们允许我们将计算逻辑集中在 store 中,从而实现代码的复用和逻辑的统一管理。 此外,Pinia 的 getters
会被缓存,只有当依赖的 state 发生变化时才会重新计算,从而提高性能。
Getters 的定义和使用
在 Pinia store 中定义 getters
非常简单。 我们只需要在 defineStore
方法的配置对象中添加 getters
属性,该属性是一个对象,其中每个键值对代表一个 getter。 每个 getter 都是一个函数,它接收 state 作为第一个参数,并返回计算结果。
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Eduardo'
}),
getters: {
doubleCount: (state) => state.count * 2,
doubleCountPlusOne(): number {
return this.doubleCount + 1
},
greeting: (state) => `Hello, ${state.name}!`,
// Getter 可以接收其他 getter 作为参数
evenOrOdd: (state): string => {
return state.count % 2 === 0 ? 'even' : 'odd'
}
},
})
在这个例子中,我们定义了四个 getters:
doubleCount
: 返回count
的两倍。doubleCountPlusOne
: 返回doubleCount
的值加一。 这个例子展示了 getter 之间如何互相调用,使用this
关键字。greeting
: 返回一个基于name
的问候语。evenOrOdd
: 根据count
的奇偶性返回 "even" 或 "odd"。
在组件中使用 getters
也很简单。 我们只需要通过 useStore
hook 获取 store 实例,然后就可以像访问 store 的属性一样访问 getters。
<template>
<p>Count: {{ counterStore.count }}</p>
<p>Double Count: {{ counterStore.doubleCount }}</p>
<p>Double Count Plus One: {{ counterStore.doubleCountPlusOne }}</p>
<p>Greeting: {{ counterStore.greeting }}</p>
<p>Even or Odd: {{ counterStore.evenOrOdd }}</p>
</template>
<script setup lang="ts">
import { useCounterStore } from './stores/counter'
const counterStore = useCounterStore()
</script>
Getter 的类型推断
Pinia 提供了强大的类型推断功能,可以自动推断 getter 的返回值类型。 这可以帮助我们避免类型错误,并提高代码的可维护性。 如果需要显式指定 getter 的返回值类型,可以使用类型注解,如上面的 doubleCountPlusOne(): number
。
Getter 的使用场景
getters
在状态管理中有很多有用的场景,以下是一些常见的例子:
- 数据转换: 将原始状态转换为更适合组件使用的格式。 例如,将日期格式化为特定的字符串。
- 数据过滤: 根据特定的条件过滤状态中的数据。 例如,筛选出所有已完成的任务。
- 数据聚合: 对状态中的数据进行聚合计算。 例如,计算所有订单的总金额。
- 权限控制: 根据用户的角色和权限,判断用户是否有权访问特定的功能。
Getter 的参数化
getter 也可以接收参数。这使得 getter 更加灵活,可以根据不同的参数返回不同的结果。
import { defineStore } from 'pinia'
export const useProductStore = defineStore('product', {
state: () => ({
products: [
{ id: 1, name: 'Apple', price: 1 },
{ id: 2, name: 'Banana', price: 0.5 },
{ id: 3, name: 'Orange', price: 0.75 }
]
}),
getters: {
// Getter 接收 state 作为第一个参数
// 函数接收第二个参数,它是一个函数,并且返回一个函数
productById: (state) => {
return (productId: number) => state.products.find(product => product.id === productId)
},
expensiveProducts: (state) => {
return (minPrice: number) => state.products.filter(product => product.price > minPrice)
}
}
})
在这个例子中,productById
getter 接收一个 productId
参数,并返回与该 ID 匹配的产品。 注意,getter 变成了一个返回函数的函数。
在组件中使用带参数的 getter:
<template>
<p>Product with ID 2: {{ productStore.productById(2)?.name }}</p>
<ul>
<li v-for="product in productStore.expensiveProducts(0.6)" :key="product.id">
{{ product.name }} - ${{ product.price }}
</li>
</ul>
</template>
<script setup lang="ts">
import { useProductStore } from './stores/product'
const productStore = useProductStore()
</script>
Getters 与计算属性的比较
特性 | Getter | 计算属性 |
---|---|---|
位置 | 定义在 Pinia store 中 | 定义在 Vue 组件中 |
状态访问 | 接收 state 作为参数 | 可以直接访问组件的 data 和 props |
复用性 | 可以在多个组件中复用 | 只能在当前组件中使用 |
缓存 | 会被缓存,只有依赖的 state 发生变化才会重新计算 | 会被缓存,只有依赖的 data 和 props 发生变化才会重新计算 |
类型安全性 | Pinia 提供类型推断,保证类型安全 | 需要手动指定类型,容易出错 |
适用场景 | 适用于需要在多个组件中共享的计算逻辑 | 适用于只在当前组件中使用的计算逻辑 |
总而言之,getters
适用于需要在多个组件中共享的计算逻辑,并且需要进行状态管理。而计算属性则适用于只在当前组件中使用的计算逻辑,并且不需要进行状态管理。
理解 Actions:修改状态的唯一途径
actions
是 Pinia store 中用于修改 state 的方法。 它们是修改 state 的唯一途径,确保了状态的可预测性和可追踪性。 Actions 可以包含任意的异步逻辑,例如发送 API 请求、处理用户输入等。
Actions 的定义和使用
在 Pinia store 中定义 actions
与定义 getters
类似。 我们只需要在 defineStore
方法的配置对象中添加 actions
属性,该属性是一个对象,其中每个键值对代表一个 action。 每个 action 都是一个函数,它可以接收任意数量的参数。
import { defineStore } from 'pinia'
import axios from 'axios'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Eduardo',
loading: false
}),
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
incrementBy(amount: number) {
this.count += amount
},
async fetchName() {
this.loading = true
try {
const response = await axios.get('https://api.example.com/name')
this.name = response.data.name
} catch (error) {
console.error('Failed to fetch name:', error)
} finally {
this.loading = false
}
},
reset() {
// You can mutate the state in multiple ways
Object.assign(this.$state, { count: 0, name: 'Initial Name' })
}
}
})
在这个例子中,我们定义了五个 actions:
increment
: 将count
的值加 1。decrement
: 将count
的值减 1。incrementBy
: 将count
的值加上指定的amount
。fetchName
: 从 API 获取name
,并更新 state。这个例子展示了如何处理异步操作。reset
: 将 state 重置为初始值。 这个例子展示了如何使用this.$state
来批量更新 state。
在组件中使用 actions
也很简单。 我们只需要通过 useStore
hook 获取 store 实例,然后就可以像调用 store 的方法一样调用 actions。
<template>
<p>Count: {{ counterStore.count }}</p>
<button @click="counterStore.increment()">Increment</button>
<button @click="counterStore.decrement()">Decrement</button>
<button @click="counterStore.incrementBy(5)">Increment by 5</button>
<button @click="counterStore.fetchName()">Fetch Name</button>
<p v-if="counterStore.loading">Loading...</p>
<p>Name: {{ counterStore.name }}</p>
<button @click="counterStore.reset()">Reset</button>
</template>
<script setup lang="ts">
import { useCounterStore } from './stores/counter'
const counterStore = useCounterStore()
</script>
Actions 中的 this
上下文
在 actions
中,this
指向 store 实例。 这使得我们可以访问 store 的 state 和 getters,以及调用其他的 actions。
Actions 的类型推断
Pinia 提供了强大的类型推断功能,可以自动推断 action 的参数类型和返回值类型。 这可以帮助我们避免类型错误,并提高代码的可维护性。
Actions 的使用场景
actions
在状态管理中有很多有用的场景,以下是一些常见的例子:
- 用户交互: 处理用户的输入,例如提交表单、点击按钮等。
- API 请求: 发送 API 请求,并更新 state。
- 数据持久化: 将 state 持久化到本地存储或服务器。
- 状态同步: 与其他 store 或组件同步 state。
订阅 Actions
Pinia 允许我们订阅 actions 的执行,以便在 action 执行前后执行一些额外的逻辑。 这对于记录日志、发送分析数据等场景非常有用。
可以使用 store.$onAction()
方法来订阅 action。 该方法接收一个回调函数作为参数,该回调函数会在 action 执行前后被调用。
import { useCounterStore } from './stores/counter'
import { onMounted } from 'vue';
export default {
setup() {
const counterStore = useCounterStore();
onMounted(() => {
const unsubscribe = counterStore.$onAction(
({
name, // action 的名称
store, // store 实例,`this`
args, // 传递给 action 的参数数组
onError, // 设置 action 抛出错误时的回调
onAfter, // 设置 action 成功执行后的回调
}) => {
console.log(`Action "${name}" started with args ${args.join(', ')}.`)
onError((error) => {
console.error(`Action "${name}" failed with error:`, error)
})
onAfter(() => {
console.log(`Action "${name}" finished.`)
})
}
)
// 在组件卸载时取消订阅
// unsubscribe()
})
return { counterStore }
},
template: `
<p>Count: {{ counterStore.count }}</p>
<button @click="counterStore.increment()">Increment</button>
`
}
Actions 与 Mutations 的比较 (Vuex)
如果你熟悉 Vuex,你可能会注意到 Pinia 没有 mutations 的概念。 在 Vuex 中,mutations 负责同步地修改 state,而 actions 负责提交 mutations。 Pinia 简化了这一过程,直接在 actions 中修改 state。 这使得代码更加简洁,并且更容易理解。
特性 | Action | Mutation (Vuex) |
---|---|---|
目的 | 修改 state | 同步地修改 state |
异步操作 | 可以包含异步操作 | 只能包含同步操作 |
调用方式 | 直接调用 | 通过 commit 方法提交 |
类型安全性 | Pinia 提供类型推断,保证类型安全 | 需要手动指定类型,容易出错 |
调试 | 更容易调试,可以直接追踪 action 的执行 | 需要追踪 mutation 的提交,相对复杂 |
总结 Actions 的最佳实践
- 保持 actions 的简洁性: 尽量将 actions 拆分成更小的、易于理解的函数。
- 使用异步操作: 如果 actions 涉及到异步操作,例如 API 请求,请使用
async/await
语法。 - 处理错误: 在 actions 中处理可能发生的错误,例如网络错误、服务器错误等。
- 避免直接修改 state: 虽然可以在 actions 中直接修改 state,但是建议使用
this.$patch
方法来批量更新 state,以提高性能。
组合 Getters 和 Actions:构建复杂的状态管理逻辑
getters
和 actions
可以一起使用,构建复杂的状态管理逻辑。 例如,我们可以使用 getters
来计算 derived state,然后使用 actions
来修改 state,从而更新 derived state。
import { defineStore } from 'pinia'
import axios from 'axios'
export const useTodosStore = defineStore('todos', {
state: () => ({
todos: [],
loading: false,
error: null
}),
getters: {
completedTodos: (state) => state.todos.filter(todo => todo.completed),
pendingTodos: (state) => state.todos.filter(todo => !todo.completed),
totalTodos: (state) => state.todos.length
},
actions: {
async fetchTodos() {
this.loading = true
this.error = null
try {
const response = await axios.get('https://jsonplaceholder.typicode.com/todos')
this.todos = response.data
} catch (error) {
this.error = error.message
} finally {
this.loading = false
}
},
async addTodo(title: string) {
try {
const response = await axios.post('https://jsonplaceholder.typicode.com/todos', {
title,
completed: false
})
this.todos.push(response.data)
} catch (error) {
this.error = error.message
}
},
async toggleTodo(id: number) {
const todo = this.todos.find(todo => todo.id === id)
if (todo) {
try {
await axios.patch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
completed: !todo.completed
})
todo.completed = !todo.completed // Optimistically update the state
} catch (error) {
this.error = error.message
// Revert the state change if the API call fails
todo.completed = !todo.completed
}
}
},
clearCompletedTodos() {
this.todos = this.todos.filter(todo => !todo.completed)
}
}
})
在这个例子中,我们定义了一个 todos
store,它包含以下 state、getters 和 actions:
- state:
todos
: 存储所有 todo 事项的数组。loading
: 表示是否正在加载 todo 事项。error
: 存储错误信息。
- getters:
completedTodos
: 返回所有已完成的 todo 事项。pendingTodos
: 返回所有未完成的 todo 事项。totalTodos
: 返回 todo 事项的总数。
- actions:
fetchTodos
: 从 API 获取 todo 事项,并更新 state。addTodo
: 添加一个新的 todo 事项,并更新 state。toggleTodo
: 切换 todo 事项的完成状态,并更新 state。clearCompletedTodos
: 清除所有已完成的 todo 事项,并更新 state。
这个例子展示了如何使用 getters
来派生状态,然后使用 actions
来修改状态,从而更新派生状态。
小结:Getters 和 Actions 的强大作用
总的来说,Pinia 的 getters
和 actions
为我们提供了一种清晰、简洁且易于维护的状态管理方式。 通过 getters
,我们可以从状态中派生数据,而通过 actions
,我们可以安全地修改状态。 它们是构建复杂 Vue.js 应用的重要组成部分。
一些建议:良好实践成就优秀代码
- 清晰命名: 使用清晰、描述性的名称来命名
getters
和actions
,以便于理解和维护。 - 单一职责: 每个
getter
和action
应该只负责一个特定的任务。 - 测试: 为
getters
和actions
编写单元测试,以确保它们的功能正确。 - 类型安全: 尽可能使用类型注解,以提高代码的类型安全性。