直接跳到內容

狀態管理

什麼是狀態管理?

理論上來說,每一個 Vue 組件實例都已經在“管理”它自己的響應式狀態了。我們以一個簡單的計數器組件為例:

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

// 狀態
const count = ref(0)

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

<!-- 視圖 -->
<template>{{ count }}</template>
vue
<script>
export default {
  // 狀態
  data() {
    return {
      count: 0
    }
  },
  // 動作
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>

<!-- 視圖 -->
<template>{{ count }}</template>

它是一個獨立的單元,由以下幾個部分組成:

  • 狀態:驅動整個應用的數據源;
  • 視圖:對狀態的一種聲明式映射;
  • 交互:狀態根據用戶在視圖中的輸入而作出相應變更的可能方式。

下面是“單向數據流”這一概念的簡單圖示:

state flow diagram

然而,當我們有多個組件共享一個共同的狀態時,就沒有這麼簡單了:

  1. 多個視圖可能都依賴於同一份狀態。
  2. 來自不同視圖的交互也可能需要更改同一份狀態。

對於情景 1,一個可行的辦法是將共享狀態“提升”到共同的父級組件上去,再通過 props 傳遞下來。然而在深層次的組件樹結構中這麼做的話,很快就會使得代碼變得繁瑣冗長。這會導致另一個問題:Prop 逐級透傳問題

對於情景 2,我們經常發現自己會直接通過模板引用獲取父/子實例,或者通過觸發的事件嘗試改變和同步多個狀態的副本。但這些模式的健壯性都不甚理想,很容易就會導致代碼難以維護。

一個更簡單直接的解決方案是抽取出組件間的共享狀態,放在一個全局單例中來管理。這樣我們的組件樹就變成了一個大的“視圖”,而任何位置上的組件都可以訪問其中的狀態或觸發動作。

用響應式 API 做簡單狀態管理

在選項式 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
<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
<template>
  <button @click="store.increment()">
    From B: {{ store.count }}
  </button>
</template>

TIP

請注意這裡點擊的處理函數使用了 store.increment(),帶上了圓括號作為內聯表達式調用,因為它並不是組件的方法,並且必須要以正確的 this 上下文來調用。

除了我們這裡用到的單個響應式對象作為一個 store 之外,你還可以使用其他響應式 API 例如 ref() 或是 computed(),或是甚至通過一個組合式函數來返回一個全局狀態:

js
import { ref } from 'vue'

// 全局狀態,創建在模塊作用域下
const globalCount = ref(1)

export function useCount() {
  // 局部狀態,每個組件都會創建
  const localCount = ref(1)

  return {
    globalCount,
    localCount
  }
}

事實上,Vue 的響應性系統與組件層是解耦的,這讓它變得非常靈活。

SSR 相關細節

如果你正在構建一個需要利用服務端渲染 (SSR) 的應用,由於 store 是跨多個請求共享的單例,上述模式可能會導致問題。這在 SSR 指引那一章節會討論更多細節

Pinia

雖然我們的手動狀態管理解決方案在簡單的場景中已經足夠了,但是在大規模的生產應用中還有很多其他事項需要考慮:

  • 更強的團隊協作約定
  • 與 Vue DevTools 集成,包括時間軸、組件內部審查和時間旅行調試
  • 模塊熱更新 (HMR)
  • 服務端渲染支持

Pinia 就是一個實現了上述需求的狀態管理庫,由 Vue 核心團隊維護,對 Vue 2 和 Vue 3 都可用。

現有用戶可能對 Vuex 更熟悉,它是 Vue 之前的官方狀態管理庫。由於 Pinia 在生態系統中能夠承擔相同的職責且能做得更好,因此 Vuex 現在處於維護模式。它仍然可以工作,但不再接受新的功能。對於新的應用,建議使用 Pinia。

事實上,Pinia 最初正是為了探索 Vuex 的下一個版本而開發的,因此整合了核心團隊關於 Vuex 5 的許多想法。最終,我們意識到 Pinia 已經實現了我們想要在 Vuex 5 中提供的大部分內容,因此決定將其作為新的官方推薦。

相比於 Vuex,Pinia 提供了更簡潔直接的 API,並提供了組合式風格的 API,最重要的是,在使用 TypeScript 時它提供了更完善的類型推導。

狀態管理已經加載完畢