关于 state-management
state-management 是面向 Vue 开发者的应用状态管理指南,介绍从 props 和自定义事件到共享 store 与 Pinia 的选择。它根据应用复杂度说明状态共享方式及各自的取舍。
主要功能
- 01说明 props 与自定义事件通信
- 02介绍基于 reactive() 的简单 store
- 03使用 Pinia 管理应用状态
- 04比较不同方案的适用取舍
适用场景
- 01在父子组件间传递数据和事件
- 02在兄弟或深层组件间共享响应式状态
- 03为较大型应用组织状态与操作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill state-management安装后在 Claude Code 中输入/state-management确认已加载。
详细介绍
状态管理
目录
Vue 组件是 Vue 应用的构建模块,它们让我们能够在组件中组合标记(HTML)、逻辑(JS)和样式(CSS)。
使用场景
- 需要在同级组件或深层嵌套的组件之间共享响应式数据时使用
- 除了简单的父子组件 prop 传递外,这也有助于管理全局应用状态
说明
- 使用 props 实现从父组件到子组件的数据流,并使用自定义事件实现从子组件到父组件的通信
- 对于小型应用,使用
reactive()创建简单的 store - 对于需要开发者工具、插件和 TypeScript 支持的大型应用,使用 Pinia(官方 Vue 状态管理库)
- 根据应用的复杂度选择状态管理方式——不要过度设计小型应用
详情
下面是一个单文件组件示例,它会显示一系列来自数据属性的数字:
<template>
<div>
<h2>The numbers are {{ numbers }}!</h2>
</div>
</template>
<script setup>
import { ref } from "vue";
const numbers = ref([1, 2, 3]);
</script>
ref() 函数会使组件具备_响应性_。如果模板中使用的响应式属性值发生变化,组件视图就会重新渲染以显示该变化。
如果 numbers 是一个需要在另一个组件中访问的数据值,该怎么办?如果我们想在多个组件之间共享 numbers,它就不再只是组件级数据,_而且_还是应用级数据。这就引出了状态管理这一主题——对应用级数据的管理。
Props
Vue 让我们能够使用 props 将数据从父组件传递给子组件。使用 props 相当简单。基本上,我们只需要在渲染子组件时,将一个值绑定到 prop 属性上即可。
ParentComponent:
<template>
<div>
<ChildComponent :numbers="numbers" />
</div>
</template>
<script setup>
import { ref } from "vue";
import ChildComponent from "./ChildComponent";
const numbers = ref([1, 2, 3]);
</script>
ChildComponent:
<template>
<div>
<h2>{{ numbers }}</h2>
</div>
</template>
<script setup>
const { numbers } = defineProps(["numbers"]);
</script>
组件事件
如果我们需要以相反的方向传递信息,该怎么办?我们不能使用 props,因为 props 只能用于单向传递数据(从父组件传到子组件)。为了让子组件能够通知父组件某件事情,我们可以使用自定义事件。
Vue 中的自定义事件以原生 CustomEvents 的形式派发,用于组件之间的通信。
ChildComponent:
<template>
<div>
<h2>{{ numbers }}</h2>
<input v-model="number" type="number" />
<button @click="$emit('number-added', Number(number))">
Add new number
</button>
</div>
</template>
<script setup>
const { numbers } = defineProps(["numbers"]);
</script>
ParentComponent:
<template>
<div>
<ChildComponent :numbers="numbers" @number-added="(n) => numbers.push(n)" />
</div>
</template>
<script setup>
import { ref } from "vue";
import ChildComponent from "./ChildComponent";
const numbers = ref([1, 2, 3]);
</script>
简单的状态管理
我们可以使用 props 向下传递数据,并使用自定义事件向上发送消息。那么,如何在两个不同的同级组件之间传递数据或进行通信呢?
一种简单的应用级状态管理方式是创建一种 store 模式,让组件共享数据存储。store 可以管理应用的状态,以及负责更改状态的方法。
import { reactive } from "vue";
export const store = reactive({
numbers: [1, 2, 3],
addNumber(newNumber) {
this.numbers.push(newNumber);
},
});
store 包含一个 numbers 数组和一个 addNumber 方法。该方法接收一个 payload,并直接更新 store 中的 numbers 值。
使用 Vue 3.x,我们可以导入并使用 reactive() 函数,从 JavaScript 对象声明响应式状态。当通过 addNumber() 方法更改此响应式状态时,任何使用该状态的组件都会自动更新!
NumberDisplay:
<template>
<div>
<h2>{{ store.numbers }}</h2>
</div>
</template>
<script setup>
import { store } from "../store.js";
</script>
NumberSubmit:
<template>
<div>
<input v-model="numberInput" type="number" />
<button @click="store.addNumber(numberInput)">Add new number</button>
</div>
</template>
<script setup>
import { ref } from "vue";
import { store } from "../store.js";
const numberInput = ref(0);
</script>
这里说组件彼此交互时,“交互”一词的含义比较宽泛。组件不会对彼此执行任何操作,而是通过 store 来触发彼此相关的更改。
仔细看看所有直接与 store 交互的部分,我们可以归纳出一种模式:
NumberSubmit中的方法负责直接调用 store 方法,因此我们可以称其为 store action。- store 方法也有特定的职责——直接修改 store 状态。因此,我们称其为 store mutation。
NumberDisplay并不关心 store 或NumberSubmit中有哪些类型的方法,它只关心从 store 获取信息。因此,我们可以说NumberDisplay可以视为 store getter。
action 提交 mutation。mutation 修改状态,进而影响视图/组件。视图/组件通过 getter 获取 store 数据。我们正逐渐接近一种更有条理的应用级状态管理方式。
Pinia
Pinia 是 Vue.js 的一种状态管理模式和库,提供了更有条理、可扩展性更强的应用级状态管理方式。
Pinia 是 Vuex 等其他状态管理解决方案的替代方案,现在也是 Vue 官方的状态管理库。它提供了一种简单高效的方式来创建和管理 store,用于封装状态、action 和 getter。
在 Pinia 中,我们可以使用 defineStore() 函数定义 store。这里使用 Composition API 语法定义 useNumbersStore() 函数,以创建一个 numbers store。
import { ref } from "vue";
import { defineStore } from "pinia";
export const useNumbersStore = defineStore("numbers", () => {
const numbers = ref([1, 2, 3]);
function addNumber(newNumber) {
numbers.value.push(newNumber);
}
return { numbers, addNumber };
});
接下来,我们可以创建一个 Pinia 实例,并将其安装到 Vue 应用中。
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount("#app");
在 NumberDisplay 组件中:
<template>
<div>
<h2>{{ store.numbers }}</h2>
</div>
</template>
<script setup>
import { useNumbersStore } from "../store";
const store = useNumbersStore();
</script>
在 NumberSubmit 组件中:
<template>
<div>
<input v-model="numberInput" type="number" />
<button @click="store.addNumber(numberInput)">Add new number</button>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useNumbersStore } from "../store";
const store = useNumbersStore();
const numberInput = ref(0);
</script>
对于这样简单的实现,Pinia store 可能并非必要,其行为与使用 reactive() 函数创建的 store 非常相似。尽管如此,Pinia 还提供了适用于更复杂使用场景的其他功能,例如通过插件扩展 Pinia 功能、支持 devtools,以及提供更完善的 TypeScript 支持和服务端渲染支持。
哪种方式才正确?
每种应用级状态管理方式都有各自的优缺点。
简单 Store
- 优点:相对容易建立。
- 缺点:状态及可能发生的状态更改没有明确定义。
Pinia
- 优点:支持 devtools、插件、TypeScript 和服务端渲染
- 缺点:需要编写额外的样板代码。
归根结底,我们需要了解应用的需求,并判断哪种方式最合适。