跳转到内容

状态管理

什么是状态管理?

技术上,每个Vue组件实例都已经“管理”了自己的响应式状态。以一个简单的计数器组件为例

vue
<script setup>
import { ref } from 'vue'

// state
const count = ref(0)

// actions
function increment() {
  count.value++
}
</script>

<!-- view -->
<template>{{ count }}</template>
vue
<script>
export default {
  // state
  data() {
    return {
      count: 0
    }
  },
  // actions
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

<!-- view -->
<template>{{ count }}</template>

它是一个自包含的单元,包括以下部分

  • 状态,驱动我们应用的真相来源;
  • 视图,对状态的声明性映射;
  • 动作,在视图对用户输入的反应中可能改变状态的方式。

这是“单向数据流”概念的简单表示

state flow diagram

然而,当我们有多个共享公共状态的组件时,这种简单性开始分解

  1. 多个视图可能依赖于同一状态片段。
  2. 来自不同视图的动作可能需要更改同一状态片段。

对于第一种情况,一个可能的解决方案是将共享状态“提升”到公共祖先组件,然后作为props传递下去。然而,在具有深层层次结构的组件树中,这很快就会变得繁琐,导致另一个被称为Prop Drilling的问题。

对于第二种情况,我们经常发现自己求助于直接通过模板引用访问父/子实例,或者尝试通过事件发射同步多个状态副本。这两种模式都很脆弱,很快就会导致难以维护的代码。

一个更简单、更直接的方法是将共享状态从组件中提取出来,并在全局单例中管理它。这样,我们的组件树就变成了一个大“视图”,无论它们在树中的位置如何,任何组件都可以访问状态或触发动作!

使用响应式API进行简单状态管理

在Options API中,使用data()选项声明响应式数据。内部,data()返回的对象通过reactive()函数变得响应式,该函数也作为公共API提供。

如果你有一块应该由多个实例共享的状态,你可以使用reactive()创建一个响应式对象,然后将其导入多个组件

js
// store.js
import { reactive } from 'vue'

export const store = reactive({
  count: 0
})
vue
<!-- ComponentA.vue -->
<script setup>
import { store } from './store.js'
</script>

<template>From A: {{ store.count }}</template>
vue
<!-- ComponentB.vue -->
<script setup>
import { store } from './store.js'
</script>

<template>From B: {{ store.count }}</template>
vue
<!-- ComponentA.vue -->
<script>
import { store } from './store.js'

export default {
  data() {
    return {
      store
    }
  }
}
</script>

<template>From A: {{ store.count }}</template>
vue
<!-- ComponentB.vue -->
<script>
import { store } from './store.js'

export default {
  data() {
    return {
      store
    }
  }
}
</script>

<template>From B: {{ store.count }}</template>

现在每当 store 对象被修改时,<ComponentA><ComponentB> 将会自动更新它们的视图 - 现在我们有一个单一的真实来源。

然而,这也意味着任何导入 store 的组件都可以随意修改它

模板
<template>
  <button @click="store.count++">
    From B: {{ store.count }}
  </button>
</template>

虽然在简单情况下这种方法是可行的,但任何组件都可以任意修改的全局状态在长期来看将难以维护。为了确保状态修改逻辑与状态本身一样集中,建议在 store 上定义具有表达动作意图的名称的方法

js
// store.js
import { reactive } from 'vue'

export const store = reactive({
  count: 0,
  increment() {
    this.count++
  }
})
模板
<template>
  <button @click="store.increment()">
    From B: {{ store.count }}
  </button>
</template>

提示

注意点击处理器使用了 store.increment() 带括号 - 由于它不是一个组件方法,所以这是调用方法时使用正确的 this 上下文的必要条件。

尽管这里我们使用单个响应对象作为 store,您也可以共享使用其他 响应性 API(如 ref()computed())创建的响应状态,甚至可以从 组合式 返回全局状态。

js
import { ref } from 'vue'

// global state, created in module scope
const globalCount = ref(1)

export function useCount() {
  // local state, created per-component
  const localCount = ref(1)

  return {
    globalCount,
    localCount
  }
}

Vue 的响应性系统与组件模型解耦,使其非常灵活。

SSR 考虑事项

如果您正在构建利用 服务器端渲染 (SSR) 的应用程序,上述模式可能会因为 store 是一个单例并在多个请求之间共享而引发问题。有关详细信息,请参阅 SSR 指南中的 更多内容

Pinia

虽然我们自制的状态管理解决方案在简单场景下可以满足需求,但在大规模生产应用程序中还有更多需要考虑的事情。

  • 更强的团队协作规范
  • 与 Vue DevTools 的集成,包括时间轴、组件内检查和时间旅行调试
  • 热模块替换
  • 服务器端渲染支持

Pinia 是一个状态管理库,它实现了上述所有功能。它由 Vue 核心团队维护,并与 Vue 2 和 Vue 3 兼容。

现有用户可能对 Vuex 已经很熟悉,它是 Vue 的前一官方状态管理库。由于 Pinia 在生态系统中扮演相同的角色,Vuex 现在处于维护模式。它仍然可以工作,但将不再接收新功能。建议为新应用程序使用 Pinia。

Pinia 最初是对 Vuex 的下一版本可能是什么样的探索,结合了核心团队对 Vuex 5 的许多想法。最终,我们意识到 Pinia 已经实现了我们在 Vuex 5 中想要的很多东西,并决定将其作为新推荐。

与 Vuex 相比,Pinia 提供了一个更简单、仪式感更少的 API,提供了 Composition-API 风格的 API,最重要的是,当与 TypeScript 一起使用时,具有坚实的类型推断支持。

状态管理已加载