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 传递给了 ChildComponentChildComponentnumbers 绑定到它的模板上。

在演练场中查看代码

组件事件

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

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

alt text

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

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

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 值放到 ParentComponentnumbers 数组中。

在演练场中查看代码

简单的状态管理

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

译注

这段涉及了状态管理中的五个概念:storestategetteractionmutation

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 + 人工的方式进行审核,以达到合规要求。

© thebestxt.cc
辽ICP备16009524号-8
本站所有文章版权所有,转载请注明出处