有兴趣阅读我们的下一本书吗?了解更多关于使用 React 构建大型 JavaScript Web 应用程序

设计模式

状态管理

Vue 组件是 Vue 应用程序的构建块,使我们能够在其中耦合标记(HTML)、逻辑(JS)和样式(CSS)。

以下是一个单文件组件的示例,它显示来自数据属性的一系列数字

<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 的值。

如果我们想要在多个组件之间共享 numbersnumbers 不仅成为组件级数据,成为应用程序级数据。这使我们进入了状态管理的话题 - 应用程序级数据的管理。

在我们探讨如何管理应用程序中的状态之前,我们将首先了解props如何在父组件和子组件之间共享数据。

Props

假设我们有一个假设应用程序,最初只包含一个父组件和一个子组件。Vue 使我们能够使用props将数据从父组件传递到子组件。

Props

使用 props 相当简单。我们本质上只需要将一个值绑定到子组件渲染位置的 prop 属性。以下是如何使用 props 结合 v-bind 指令将数组值传递下去的示例。

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 { buttonText } = defineProps(["numbers"]);
</script>

ParentComponentnumbers 数组作为同名 props 传递到 ChildComponentChildComponent 只是将 numbers 的值绑定到其模板上。

ParentComponent.vue
1<template>
2 <div>
3 <ChildComponent :numbers="numbers" />
4 </div>
5</template>
6
7<script setup>
8 import { ref } from "vue";
9 import ChildComponent from "./ChildComponent";
10
11 const numbers = ref([1, 2, 3]);
12</script>

组件事件

如果我们需要找到一种方法以相反的方向传递信息怎么办?这方面的示例可能允许用户在子组件中向上面示例中展示的数组中添加一个新的数字。

我们不能使用 props,因为 props 只能用于以单向格式传递数据(从父级到子级到孙子级……)。为了使子组件能够通知父组件有关某事,我们可以使用自定义事件。

Custom Events

Vue 中的自定义事件作为原生 CustomEvents 派发,用于组件之间的通信。

以下是如何使用自定义事件来使 ChildComponent 能够促进对 ParentComponentnumbers 数据属性的更改的示例

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>

ChildComponent 有一个输入框,用于捕获 number 值,还有一个按钮,用于发出 number-added 自定义事件,并带有捕获的 number 值。

ParentComponent 上,使用 @number-added 表示的自定义事件监听器在子组件渲染位置指定。当子组件中发出此事件时,它会将事件中的 number 值推送到 ParentComponentnumbers 数组中。

ParentComponent.vue
1<template>
2 <div>
3 <ChildComponent :numbers="numbers" @number-added="(n) => numbers.push(n)" />
4 </div>
5</template>
6
7<script setup>
8import { ref } from "vue";
9
10// eslint-disable-next-line no-unused-vars
11import ChildComponent from "./ChildComponent";
12
13// eslint-disable-next-line no-unused-vars
14const numbers = ref([1, 2, 3]);
15</script>

简单状态管理

我们可以使用 props 向下传递数据,使用自定义事件向上发送消息。我们如何能够在两个不同的兄弟组件之间传递数据或促进通信?

Sibling components communication

我们不能像上面那样使用自定义事件,因为这些事件是在特定组件的界面中发出的,因此需要在组件渲染位置声明自定义事件监听器。在两个独立的组件中,一个组件不会在另一个组件中渲染。

管理应用程序级状态的一个简单方法是创建存储模式,该模式涉及在组件之间共享数据存储。存储可以管理我们应用程序的状态以及负责更改状态的方法。

例如,我们可以有一个简单的存储,如下所示

import { reactive } from "vue";

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

存储包含一个 numbers 数组和一个 addNumber 方法,该方法接受一个有效负载并直接更新存储的 numbers 值。

注意使用 reactive() 函数定义状态对象?在 Vue 3.x 中,我们可以导入并使用 reactive() 函数从 JavaScript 对象声明响应式状态。当使用 addNumber() 方法更改此响应式状态时,任何使用此响应式状态的组件都将自动更新!

我们可以有一个负责从我们将称为 NumberDisplay 的存储中显示 numbers 数组的组件。

NumberDisplay:

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

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

现在,我们可以创建另一个名为 NumberSubmit 的组件,它将允许用户向我们的数据数组中添加一个新的数字。

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>

NumberSubmit 组件有一个 addNumber() 方法,它调用 store.addNumber() 变异并传递预期的有效负载。

存储方法接收有效负载并直接更改 store.numbers 数组。由于 Vue 的响应式,每当存储状态中的 numbers 数组发生更改时,依赖于此值的相关 DOM(<template> of NumberDisplay)将自动更新

store.js
1import { reactive } from "vue";
2
3 export const store = reactive({
4 numbers: [1, 2, 3],
5 addNumber(newNumber) {
6 this.numbers.push(newNumber);
7 },
8 });

当我们说组件在此处相互交互时,我们使用“交互”一词比较宽泛。组件不会彼此做任何事情,而是会通过存储来调用对彼此的更改。

Simple reactive store

如果我们仔细观察直接与存储交互的所有部分,我们可以确定一个模式

  • NumberSubmit 中的方法负责直接作用于存储方法,因此我们可以将其标记为存储操作
  • 存储方法也有一定的职责 - 直接更改存储状态。因此,我们将说它是一个存储变异
  • NumberDisplay 不关心存储或 NumberSubmit 中存在哪种类型的方法,只关心从存储中获取信息。因此,我们将说 NumberDisplay 是一种存储获取器

一个操作会提交给一个变异变异会更改状态,然后影响视图/组件。视图/组件使用获取器检索存储数据。我们正在接近一种更结构化的方式来处理应用程序级状态。

Pinia

Pinia 是一个适用于 Vue.js 的状态管理模式和库,它提供了更结构化和可扩展的方式来处理应用程序级状态。

Pinia 是其他状态管理解决方案(如 Vuex)的替代方案,现在是 Vue 的官方状态管理库。它提供了一种简单高效的方式来创建和管理存储,存储封装了状态、操作和获取器。

在 Pinia 中,我们可以使用 defineStore() 函数定义一个存储。Pinia 允许我们使用模拟选项 API 或组合 API 的语法来定义一个存储。在这里,我们使用组合 API 语法来定义一个 useNumbersStore() 函数来创建一个 numbers 存储。

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 的存储,其初始状态包含一个 numbers 属性。我们还定义了一个操作 addNumber(),它会修改 numbers 状态。

然后,我们可以创建一个 Pinia 实例并将其安装到我们的 Vue 应用程序中。

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");

此时,我们能够在组件中使用新创建的存储。在 NumberDisplay 组件中,我们将从存储文件导入 useNumbersStore() 函数并调用它以访问存储实例。然后,我们可以引用组件模板中的存储 numbers 值。

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

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

  const store = useNumbersStore();
</script>

NumberSubmit 组件中,我们可以执行与上面相同的操作来访问将用于更新存储 numbers 属性的存储 addNumber() 方法。

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

有了这些更改,我们的应用程序的行为将与以前完全相同。

store.js
1import { defineStore } from "pinia";
2 import { ref } from "vue";
3
4 export const useNumbersStore = defineStore("numbers", () => {
5 const numbers = ref([1, 2, 3]);
6
7 function addNumber(newNumber) {
8 this.numbers.push(newNumber);
9 }
10
11 return { numbers, addNumber };
12 });

对于像这样的简单实现,Pinia 存储可能不是必需的,并且行为与使用 reactive() 函数创建的存储非常相似。也就是说,Pinia 为更复杂的用例提供了额外的功能,例如能够 使用插件扩展 Pinia 功能、具有开发工具支持、具有更合适的 TypeScript 支持服务器端渲染支持

Pinia | Vue devtools

哪种方法是正确的?

每种管理应用程序级状态的方法都有其优点和缺点。

简单存储

  • 优点:相对容易建立。
  • 缺点:状态和可能的状态更改没有明确定义。

Pinia

  • 优点:开发工具支持、插件 + TypeScript + 服务器端渲染支持
  • 缺点:额外的样板代码。

归根结底,我们应该了解我们的应用程序需要什么以及哪种方法可能最适合。

有用资源