vue學習11——狀態管理vuex02
開始
【安裝】
npm install vuex --save
在一個模組化的打包系統中,必須顯式地通過 Vue.use()
來安裝 Vuex
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex)
當使用全域性 script 標籤引用 Vuex 時,不需要以上安裝過程
【概述】
每一個 Vuex 應用的核心就是 store(倉庫)。“store”基本上就是一個容器,它包含著應用中大部分的狀態 (state)。Vuex 和單純的全域性物件有以下兩點不同:
1、Vuex 的狀態儲存是響應式的。當 Vue 元件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那麼相應的元件也會相應地得到高效更新
2、不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation。這樣使得可以方便地跟蹤每一個狀態的變化,從而能夠實現一些工具幫助更好地瞭解應用
【最簡單的store】
下面來建立一個 store。建立過程直截了當——僅需要提供一個初始 state 物件和一些 mutation
// 如果在模組化構建系統中,請確保在開頭呼叫了 Vue.use(Vuex) const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } } })
現在,可以通過 store.state
來獲取狀態物件,以及通過 store.commit
方法觸發狀態變更:
store.commit('increment') console.log(store.state.count) // -> 1
通過提交 mutation 的方式,而非直接改變 store.state.count
,是因為想要更明確地追蹤到狀態的變化。這個簡單的約定能夠讓意圖更加明顯,這樣在閱讀程式碼的時候能更容易地解讀應用內部的狀態改變。此外,這樣也有機會去實現一些能記錄每次狀態改變,儲存狀態快照的除錯工具。有了它,甚至可以實現如時間穿梭般的除錯體驗。
由於 store 中的狀態是響應式的,在元件中呼叫 store 中的狀態簡單到僅需要在計算屬性中返回即可。觸發變化也僅僅是在元件的 methods 中提交 mutation
下面是一個使用vuex實現的簡單計數器
const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment: state => state.count++, decrement: state => state.count--, } }) new Vue({ el: '#app', computed: { count () { return store.state.count } }, // view template: ` <div> <input type="button" value="-" @click="decrement"> <span>{{count}}</span> <input type="button" value="+" @click="increment"> </div> `, // actions methods: { increment () { store.commit('increment') }, decrement () { store.commit('decrement') }, } })
核心概念
state
【單一狀態樹】
Vuex 使用單一狀態樹——用一個物件就包含了全部的應用層級狀態。至此它便作為一個“唯一資料來源 (SSOT)”而存在。這也意味著,每個應用將僅僅包含一個 store 例項。單一狀態樹能夠直接地定位任一特定的狀態片段,在除錯的過程中也能輕易地取得整個當前應用狀態的快照
【在VUE元件中獲得VUEX狀態】
如何在 Vue 元件中展示狀態呢?由於 Vuex 的狀態儲存是響應式的,從 store 例項中讀取狀態最簡單的方法就是在計算屬性中返回某個狀態
// 建立一個counter 元件 const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return store.state.count } } }
每當 store.state.count
變化的時候, 都會重新求取計算屬性,並且觸發更新相關聯的 DOM
然而,這種模式導致元件依賴全域性狀態單例。在模組化的構建系統中,在每個需要使用 state 的元件中需要頻繁地匯入,並且在測試元件時需要模擬狀態。
Vuex 通過 store
選項,提供了一種機制將狀態從根元件“注入”到每一個子元件中(需呼叫 Vue.use(Vuex)
):
const app = new Vue({ el: '#app', // 把 store 物件提供給 “store” 選項,這可以把 store 的例項注入所有的子元件 store, components: { Counter }, template: ` <div class="app"> <counter></counter> </div> ` }
通過在根例項中註冊 store
選項,該 store 例項會注入到根元件下的所有子元件中,且子元件能通過this.$store
訪問到。下面來更新下 Counter
的實現:
const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return this.$store.state.count } }
【mapState輔助函式】
當一個元件需要獲取多個狀態時,將這些狀態都宣告為計算屬性會有些重複和冗餘。為了解決這個問題,可以使用mapState
輔助函式幫助生成計算屬性
// 在單獨構建的版本中輔助函式為 Vuex.mapState import { mapState } from 'vuex' export default { // ... computed: mapState({ // 箭頭函式可使程式碼更簡練 count: state => state.count, // 傳字串引數 'count' 等同於 `state => state.count` countAlias: 'count', // 為了能夠使用 `this` 獲取區域性狀態,必須使用常規函式 countPlusLocalState (state) { return state.count + this.localCount } }) }
當對映的計算屬性的名稱與 state 的子節點名稱相同時,也可以給 mapState
傳一個字串陣列
computed: mapState([ // 對映 this.count 為 store.state.count 'count' ])
【物件展開運算子】
mapState
函式返回的是一個物件。如何將它與區域性計算屬性混合使用呢?通常,需要使用一個工具函式將多個物件合併為一個,將最終物件傳給 computed
屬性。但是自從有了物件展開運算子,可以極大地簡化寫法:
computed: { localComputed () { /* ... */ }, // 使用物件展開運算子將此物件混入到外部物件中 ...mapState({ // ... }) }
【元件仍然保有區域性狀態】
使用 Vuex 並不意味著需要將所有的狀態放入 Vuex。雖然將所有的狀態放到 Vuex 會使狀態變化更顯式和易除錯,但也會使程式碼變得冗長和不直觀。如果有些狀態嚴格屬於單個元件,最好還是作為元件的區域性狀態
Getter
有時候需要從 store 中的 state 中派生出一些狀態,例如對列表進行過濾並計數:
computed: { doneTodosCount () { return this.$store.state.todos.filter(todo => todo.done).length } }
如果有多個元件需要用到此屬性,要麼複製這個函式,或者抽取到一個共享函式然後在多處匯入它——無論哪種方式都不是很理想
Vuex 允許在 store 中定義“getter”(可以認為是 store 的計算屬性)。就像計算屬性一樣,getter 的返回值會根據它的依賴被快取起來,且只有當它的依賴值發生了改變才會被重新計算
Getter 接受 state 作為其第一個引數:
const store = new Vuex.Store({ state: { todos: [ { id: 1, text: '...', done: true }, { id: 2, text: '...', done: false } ] }, getters: { doneTodos: state => { return state.todos.filter(todo => todo.done) } } })
Getter 會暴露為 store.getters
物件:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getter 也可以接受其他 getter 作為第二個引數:
getters: { // ... doneTodosCount: (state, getters) => { return getters.doneTodos.length } } store.getters.doneTodosCount // -> 1
可以很容易地在任何元件中使用它:
computed: { doneTodosCount () { return this.$store.getters.doneTodosCount } }
也可以通過讓 getter 返回一個函式,來實現給 getter 傳參。在對 store 裡的陣列進行查詢時非常有用
getters: { // ... getTodoById: (state, getters) => (id) => { return state.todos.find(todo => todo.id === id) } } store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
如果箭頭函式不好理解,翻譯成普通函式如下
var getTodoById = function(state,getters){ return function(id){ return state.todos.find(function(todo){ return todo.id === id }) } } store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
【mapGetters輔助函式】
mapGetters
輔助函式僅僅是將 store 中的 getter 對映到區域性計算屬性:
import { mapGetters } from 'vuex' export default { // ... computed: { // 使用物件展開運算子將 getter 混入 computed 物件中 ...mapGetters([ 'doneTodosCount', 'anotherGetter', // ... ]) } }
如果想將一個 getter 屬性另取一個名字,使用物件形式:
mapGetters({ // 對映 `this.doneCount` 為 `store.getters.doneTodosCount` doneCount: 'doneTodosCount' })
mutation
更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation。Vuex 中的 mutation 非常類似於事件:每個 mutation 都有一個字串的 事件型別 (type) 和 一個 回撥函式 (handler)。這個回撥函式就是實際進行狀態更改的地方,並且它會接受 state 作為第一個引數:
const store = new Vuex.Store({ state: { count: 1 }, mutations: { increment (state) { // 變更狀態 state.count++ } } })
不能直接呼叫一個 mutation handler。這個選項更像是事件註冊:“當觸發一個型別為 increment
的 mutation 時,呼叫此函式。”要喚醒一個 mutation handler,需要以相應的 type 呼叫 store.commit 方法:
store.commit('increment')
【提交載荷(Payload)】
可以向 store.commit
傳入額外的引數,即 mutation 的 載荷(payload)
// ... mutations: { increment (state, n) { state.count += n } } store.commit('increment', 10)
在大多數情況下,載荷應該是一個物件,這樣可以包含多個欄位並且記錄的 mutation 會更易讀:
// ... mutations: { increment (state, payload) { state.count += payload.amount } } store.commit('increment', { amount: 10 })
【物件風格的提交方式】
提交 mutation 的另一種方式是直接使用包含 type
屬性的物件
store.commit({ type: 'increment', amount: 10 })
當使用物件風格的提交方式,整個物件都作為載荷傳給 mutation 函式,因此 handler 保持不變:
mutations: { increment (state, payload) { state.count += payload.amount } }
【遵守響應規則】
既然 Vuex 的 store 中的狀態是響應式的,那麼當變更狀態時,監視狀態的 Vue 元件也會自動更新。這也意味著 Vuex 中的 mutation 也需要與使用 Vue 一樣遵守一些注意事項:
1、最好提前在store中初始化好所有所需屬性
2、當需要在物件上新增新屬性時,應該使用 Vue.set(obj, 'newProp', 123)
, 或者以新物件替換老物件
例如,利用物件展開運算子可以這樣寫:
state.obj = { ...state.obj, newProp: 123 }
【使用常量替代Mutation事件型別】
使用常量替代 mutation 事件型別在各種 Flux 實現中是很常見的模式。這樣可以使 linter 之類的工具發揮作用,同時把這些常量放在單獨的檔案中可以讓程式碼合作者對整個 app 包含的 mutation 一目瞭然
// mutation-types.js export const SOME_MUTATION = 'SOME_MUTATION' // store.js import Vuex from 'vuex' import { SOME_MUTATION } from './mutation-types' const store = new Vuex.Store({ state: { ... }, mutations: { // 可以使用 ES2015 風格的計算屬性命名功能來使用一個常量作為函式名 [SOME_MUTATION] (state) { // mutate state } } })
【Mutation必須是同步函式】
一條重要的原則就是mutation必須是同步函式
mutations: { someMutation (state) { api.callAsyncMethod(() => { state.count++ }) } }
假如正在debug 一個 app 並且觀察 devtool 中的 mutation 日誌。每一條 mutation 被記錄,devtools 都需要捕捉到前一狀態和後一狀態的快照。然而,在上面的例子中 mutation 中的非同步函式中的回撥讓這不可能完成:因為當 mutation 觸發的時候,回撥函式還沒有被呼叫,devtools 不知道什麼時候回撥函式實際上被呼叫——實質上任何在回撥函式中進行的狀態改變都是不可追蹤的
【在元件中提交Mutation】
可以在元件中使用 this.$store.commit('xxx')
提交 mutation,或者使用 mapMutations
輔助函式將元件中的 methods 對映為 store.commit
呼叫(需要在根節點注入 store
)
import { mapMutations } from 'vuex' export default { // ... methods: { ...mapMutations([ 'increment', // 將 `this.increment()` 對映為 `this.$store.commit('increment')` // `mapMutations` 也支援載荷: 'incrementBy' // 將 `this.incrementBy(amount)` 對映為 `this.$store.commit('incrementBy', amount)` ]), ...mapMutations({ add: 'increment' // 將 `this.add()` 對映為 `this.$store.commit('increment')` }) } }
action
在 mutation 中混合非同步呼叫會導致程式很難除錯。例如,當能呼叫了兩個包含非同步回撥的 mutation 來改變狀態,怎麼知道什麼時候回撥和哪個先回調呢?這就是為什麼要區分這兩個概念。在 Vuex 中,mutation 都是同步事務:
store.commit('increment') // 任何由 "increment" 導致的狀態變更都應該在此刻完成。
Action類似於mutation,不同之處在於:
1、Action 提交的是 mutation,而不是直接變更狀態
2、Action 可以包含任意非同步操作
下面來註冊一個簡單的action
const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } }, actions: { increment (context) { context.commit('increment') } } }
搜尋
複製