跳轉到內容

狀態管理

什麼是狀態管理?

技術上,每個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 一起使用時,具有堅實的型別推斷支援。

狀態管理已載入