Skip to content

状态管理 ​

Vue 组件是构建 Vue 应用的基本区块,它允许我们在内部编写标记语言(HTML)、逻辑(JS)和样式(CSS)。

下面是一个单文件组件的示例,用于从状态中展示一系列的数字。

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 是另一个组件中的值,在这里要被使用呢?例如,我们需要一个专门负责显示 numbers 的组件(就像上面这个),另一个组件负责操作 numbers 的值。

如果我们想要在多个组件中共享 numbers,此时 numbers 就不应该仅仅作为组件级状态了,应该是应用级的状态。这就是我们要讨论的话题,状态管理 —— 应用级的状态管理。

在讨论如何管理应用的状态之前,我们先了解一下父组件和子组件之间是如何通过 props 来共享数据的。

Props ​

假设我们有一个应用,一开始它只有一个父组件和一个子组件。Vue 给予了我们从父组件向子组件传递数据的能力。

alt text

Props 用起来相当简单。我们要做的就是在子组件的 prop 上绑定对应的值即可。这有一个例子,通过使用 v-bind 指令,向 props 传递一个数组。

vue
<template>
  <div>
    <ChildComponent :numbers="numbers" />
  </div>
</template>

<script setup>
  import { ref } from "vue";
  import ChildComponent from "./ChildComponent";

  const numbers = ref([1, 2, 3]);
</script>
vue
<template>
  <div>
    <h2>{{ numbers }}</h2>
  </div>
</template>

<script setup>
  const { buttonText } = defineProps(["numbers"]);
</script>

ParentComponent 把数组 numbers 当做 props 传递给了 ChildComponent。ChildComponent 把 numbers 绑定到它的模板上。

在演练场中查看代码

组件事件 ​

如果我们需要反向传值该怎么办呢?例如我们需要在上面例子中的子组件中加入一个新的数字。

我们不能直接修改 props,因为 props 只能单向传递(从祖先组件向后代组件传递)。要让子组件告诉父组件什么事情,我们可以使用自定义事件。

alt text

在 Vue 中,自定义事件实际上是通过原生的 CustomEvent 对象来实现的,这些事件主要用于组件之间的数据和行为的传递与协调。

下面是一个子组件 ChildComponent 使用自定义事件来改变父组件 ParentComponent’s 的 numbers 属性的例子:

vue
<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>
vue
<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>

ChildComponent 有一个输入框,用于获取输入的数字。还有一个按钮。按钮用于触发自定义事件 number-added,并将捕获的 number 值传递出去。

在 ParentComponent 中,在渲染子组件的时候指定一个自定义事件的监听器 @number-added。当这个事件在子组件中被触发,监听器对应的方法会将事件传递过来的 numbers 值放到 ParentComponent 的 numbers 数组中。

在演练场中查看代码

简单的状态管理 ​

我们可以使用 props 向下传递数据,使用自定义事件向上发送消息。我们如何在兄弟组件间传值或通信呢?

alt text

我们不能像上面那样使用自定义事件,因为这些事件是在特定组件的接口内部发出的,因此自定义事件的监听器需要在组件被渲染的位置声明。在两个独立的同级组件之间,不会有哪个组件在领一个组件中渲染。

译注

要不它们为什么叫兄弟组件呢。

有一种简单的管理应用级状态的方法,就是创建一个负责在组件间共享数据的 store。这个 store 可以管理应用的状态,并提供修改状态的方法。

例如,我们可以像下面这样创建一个简单的 store:

javascript
import { reactive } from "vue";

export const store = reactive({
  numbers: [1, 2, 3],
  addNumber(newNumber) {
    this.numbers.push(newNumber);
  },
});

这个 store 包含一个 numbers 数组和一个 addNumber 方法,addNumber 接收一个参数,直接将参数更新到 numbers 上。

你是否已经注意到,这里使用 reactive() 来创建状态对象。在 Vue 3.x 中,我们可以导入并使用 reactive() 函数,使用 JavaScript 对象来创建响应式状态。当 addNumber() 变更了这个响应式状态,任何使用了这个响应式状态的组件都将会自动更新!

我们创建一个负责显示 store 中 numbers 数组的组件,叫作 NumberDisplay:

vue
<template>
  <div>
    <h2>{{ store.numbers }}</h2>
  </div>
</template>

<script setup>
  import { store } from "../store.js";
</script>

我们现在创建另一个组件,叫作 NumberSubmit,它允许用户为我们的数组添加数字:

vue
<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>

组件 NumberSubmit 包含一个 addNumber() 方法,该方法调用了 store.addNumber() 并传递相应的参数。

Store 中的方法接收有效参数,并直接修改 store.numbers 数组。得益于 Vue 的响应式,每当 store 中的 numbers 数组发生改变,依赖该值的 DOM 元素(NumberDisplay 组件中的 <template>)都会自动更新。

在演练场中查看代码

每当我们讨论组件之间互相影响时,我们都是使用较为宽泛的「interact」一词。实际上这些组件不会彼此影响,而是通过改变 store 中的状态来间接影响。

alt text

如果我们仔细观察所有与 store 交互的代码,我们可以得到一个模式:

  • NumberSubmit 方法具有直接操作 store 中方法的职责,所以我们认为它是一个 store action。

  • store 的方法也有一些要负责的事情 —— 直接改变 store 中的状态。我们会将这些方法称作 store mutation。

  • NumberDisplay 并不关心 store 中或者是 NumberSubmit 中有存在什么类型的方法,它只关心如何从 store 中获取信息。所以我们会说 NumberDisplay 是一种 store getter。

一个 action 会触发一个 mutation。Mutation 会改变 store 中的状态,从而影响视图或组件。视图/组件使用 getters 检索并获取 store 中的数据。我们现在距离以更结构化的方式处理应用级的数据越来越近了。

译注

这段涉及了状态管理中的五个概念:store、state、getter、action 和 mutation。

Pinia 的作者在 Pinia 文档 中保留了这五个概念原本的英文单词,没有翻译。可能是在中文中没有找到能精准对应这五个概念的词语吧。

因此,本文档中后面再出现的这五个概念一律保留原词,和官方文档保持一致。

译注

state 就是状态。

getter 是获取状态的方法,类似于计算方法 computed。

action 是方法,可以类比为 method。

mutation 是异步方法,由 action 触发,进行异步处理。在 Vuex 中使用,在 Pinia 中已经废弃了。

store 是上述四部分的集合。

Pinia ​

Pinia 是一个状态管理模式,同时也是一个 Vue.js 的状态管理库,它提供了一种结构化且可扩展的方式来管理应用级状态。

Pinia 是 Vuex 等其它状态管理解决方案的替代品,目前是 Vue 的官方状态管理库。它提供了一种简单有效的方式来创建和管理 store,其中封装了一些 state、action、和 getter。

在 Pinia 中,我们可以使用 defineStore() 来定义一个 store。Pinia 允许我们使用类似选项式 API 或是组合式 API 的语法来定义 store。下面是我们使用组合式 API 的语法来定义一个 useNumberStore() 函数,从而创建一个 numbers store。

javascript
import { ref } from "vue";
import { defineStore } from "pinia";

export const useNumbersStore = defineStore("numbers", () => {
  const numbers = ref([1, 2, 3]);

  function addNumber(newNumber) {
    this.numbers.push(newNumber);
  }

  return { numbers, addNumber };
});

在这个例子中,我们定义了一个叫作 numbers 的 store,它的初始状态包含一个 numbers 属性。我们同样定义了一个 action addNumber(),它可以修改 numbers 状态。

我们接下俩创建 Pinia 实例,将其安装到我们的 Vue 应用中。

javascript
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import "./styles.css";

const app = createApp(App);
const pinia = createPinia();

app.use(pinia);
app.mount("#app");

这时,我们就可以在组件中使用我们新创建的 store 了。在 NumberDisplay 组件中,我们从 store 文件中导入 useNumbersStore() 函数,并执行它,得到 store 实例。我们可以在组件的模板中直接引用 store 中 numbers 的值。

vue
<template>
  <div>
    <h2>{{ store.numbers }}</h2>
  </div>
</template>

<script setup>
  import { useNumbersStore } from "../store";

  const store = useNumbersStore();
</script>

在 NumberSubmit 组件中,我们可以使用像之前一样的方法来创建实例,然后执行 store 中的 addNumber() 方法,用于更新 store 中的 numbers 属性。

vue
<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 实现来说,使用 Pinia 来管理状态可能显得不是那么必要,并且看上去和使用 reactive() 创建的 store 非常相似。话虽如此,Pinia 还是可以在更复杂的情况中提供更多的功能,例如我们可以使用插件扩展 Pinia 的功能、获得 Vue 开发者工具提供的支持、更多的 TypeScript 支持 和 服务端渲染支持。

alt text

那么,正确的方法是? ​

每一种管理应用级状态的方案都有各自的优缺点。

简单的 Store ​

  • 优点:容易搭建。

  • 缺点:状态和状态变化没有明确的定义。

Pinia ​

  • 优点: 开发者工具支持、插件 + TypeScript + 服务端渲染支持

  • 缺点: 需要写很多额外的样板代码。

归根结底,我们需要了解我们的应用程序需哪种方法,并且需要知道哪个是最适合的。

扩展阅读 ​

最后更新于:

评论区
评论区空空如也
发送评论
名字
0 / 20
邮箱
0 / 100
评论内容
0 / 140
由于是非实名评论,所以不提供删除功能。如果你需要删除你发送的评论,或者是其他人的评论对你造成了困扰,请 发邮件给我 。同时评论区会使用 AI + 人工的方式进行审核,以达到合规要求。