Vuex 概念篇
Vuex 是什麼?
Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。
什麼是“狀態管理模式”?
從軟體設計的角度,就是以一種統一的約定和準則,對全域性共享狀態資料進行管理和操作的設計理念。你必須按照這種設計理念和架構來對你專案裡共享狀態資料進行CRUD。所以所謂的“狀態管理模式”就是一種軟體設計的一種架構模式(思想)。
為什麼需要這種“狀態管理模式”應用到專案中呢?
現如今,流行元件化、模組化開發,多人開發各自元件的時候,不難保證各個元件都是唯一性的,多個元件共享狀態肯定是存在的,共享狀態又是誰都可以進行操作和修改的,這樣就會導致所有對共享狀態的操作都是不可預料的,後期出現問題,進行 debug 也是困難重重,往往我們是儘量去避免全域性變數。
但大量的業務場景下,不同的模組(元件)之間確實需要共享資料,也需要對其進行修改操作。也就引發軟體設計中的矛盾:模組(元件)之間需要共享資料 和 資料可能被任意修改導致不可預料的結果。
為了解決其矛盾,軟體設計上就提出了一種設計和架構思想,將全域性狀態進行統一的管理,並且需要獲取、修改等操作必須按我設計的套路來。就好比馬路上必須遵守的交通規則,右行斑馬線就是隻能右轉一個道理,統一了對全域性狀態管理的唯一入口,使程式碼結構清晰、更利於維護。
Vuex 是借鑑了 Flux 、Redux 和 The Elm Architecture 架構模式、設計思想的產物。
什麼情況下我應該使用 Vuex?
不打算開發大型單頁應用,使用 Vuex 可能是繁瑣冗餘的。應用夠簡單,最好不要使用 Vuex。一個簡單的 global event bus (父子元件通訊,父元件管理所需的資料狀態)就足夠您所需了。構建一箇中大型單頁應用,您很可能會考慮如何更好地在元件外部管理狀態,Vuex 將會成為自然而然的選擇。
個人見解,什麼時候用?管你小中大型應用,我就想用就用唄,一個長期構建的小型應用專案,誰能知道專案需求以後會是什麼樣子,畢竟在這浮躁的時代,需求就跟川劇變臉一樣快,對不對?畢竟學習了 Vuex 不立馬用到專案實戰中,你永遠不可能揭開 Vuex 的面紗。專案中使用多了,自然而然就會知道什麼時候該用上狀態管理,什麼時候不需要。老話說的好熟能生巧,你認為呢? (括弧 -- 先了解好Vuex 一些基本概念,然後在自己的專案中使用過後,再用到你公司專案上,你別這麼虎一上來就給用上去了~)
Vuex 基本使用篇
安裝
npm i vuex -S
複製程式碼
專案全域性中任何地方使用 Vuex, 需要將 Vuex 註冊到 Vue 例項中:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
複製程式碼
Vuex 上車前的一個?
上車規則:
- 每一個 Vuex 應用的核心就是 store(倉庫),一個專案中必須只有一個 store 例項。包含著你的應用中大部分的狀態 (state)。
- 不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation。
Vuex 和單純的全域性物件有以下兩點不同: (1) Vuex 的狀態儲存是響應式的。Vue 元件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那麼相應的元件也會相應地得到高效更新。 (2)不能直接改變 store 中的狀態。(重要的事情多來一遍)
上?:
<div id="app">
<p>{{ count }}</p>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
複製程式碼
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex) // Vuex 註冊到 Vue 中
// 建立一個 store
const store = new Vuex.Store({
// 初始化 state
state: {
count: 0
},
// 改變狀態唯一宣告處
mutations: {
increment: state => state.count++,
decrement: state => state.count--
}
})
new Vue({
el: '#app',
// 從根元件將 store 的例項注入到所有的子元件
store,
computed: {
count () {
// Vuex 的狀態儲存是響應式的,從 store 例項中讀取狀態最簡單的方法就是在計算屬性中返回某個狀態,
// 每當狀態 count 發生變化,都會重新求取計算
return this.$store.state.count
}
},
methods: {
increment () {
this.$store.commit('increment')
},
decrement () {
this.$store.commit('decrement')
}
}
})
複製程式碼
state
一個物件管理應用的所有狀態,唯一資料來源(SSOT, Single source of truth),必須前期初始化就定義好,不然後面在來修改設定 state
,程式就不會捕獲到 mutations(突變的意思)
所以資料也就不會又任何的更新,元件上也無法體現出來。
獲取 store 管理的狀態, 因為在 Vue 例項化的時候將 Vuex store 物件 注入了進來 ,所以任何地方都可以通過 this.$store
獲取到 store, this.$store.state
來獲取狀態物件, this.$store.commit
來觸發之前定義好的 mutations 中的方法
this.$store.state('count') // => 0
this.$store.commit('increment') // => 1
複製程式碼
通過提交 mutation
的方式,而非直接改變 store.state.count
, 使用 commit
方式可以讓 Vuex 明確地追蹤到狀態的變化,利於後期維護和除錯。
通過了解 state
(狀態,資料)和 mutations
(修改資料唯一宣告的地方,類似 SQL 語句)知道了 Vuex 最重要的核心兩部分,然後通過掌握 gttter、action、module 來讓 Vuex 更加的工程化、合理化來適應更大型的專案的狀態管理。
mapState 輔助函式
mapState
可以幹什麼呢?字面意思狀態對映,通過 mapState
可以更加快捷方便幫我們生成計算屬性,拿上面的例子進行演示:
computed: {
count () {
return this.$store.state.count
}
}
// 使用 mapState
import { mapState } from 'vuex' // 需要先匯入
computed: mapState([
// 箭頭函式方式
count: state => state.count ,
// or 傳字串引數方式, 'count' 等同於 state => state.count
countAlias: 'count'
// 獲取狀態後,你還需要和當前元件別的屬性值時,就必須使用常規函式的寫法了, 只有這樣才能獲取到當前元件的 this
countPlusLocalState (state) {
return state.count + this.localCount
}
])
複製程式碼
當前計算屬性名稱和狀態名稱相同時,可以傳遞一個字串陣列:
computed: mapState([
// 對映 `this.count` 為 `this.$store.state.count`
'count'
])
複製程式碼
以上使用 mapState
輔助函式後,整個 computed
計算屬性都成了 state
狀態管理聚集地了, 元件裡並不是所有的計算屬性都需要被狀態管理化,還是有很多計算屬性是不需要狀態管理的資料的,那如何將它與區域性計算屬性混合使用呢?
因為 mapState
函式返回的是一個物件。所以我們使用物件擴充套件運算子就可以把區域性計算屬性和 mapState
函式返回的物件融合了。
computed: {
...mapState({
count: state => state.count
}),
localComputed () {
/* ... */
}
}
複製程式碼
⚠️ 注意: 物件擴充套件運算子,現處於 ECMASCript 提案 stage-4 階段(將被新增到下一年度釋出),所以專案中要使用需要安裝 babel-plugin-transform-object-rest-spread 外掛 或 安裝 presets 環境為 stage 為 1的 env 版本 babel-preset-stage-1 和修改 babelrc 配置檔案
.babelrc
{
"presets": [
"env",
"stage-1" // 新增此項
],
"plugins": [
"transform-vue-jsx",
"syntax-dynamic-import"
]
}
複製程式碼
核心概念
上面以講述過 state 了,這裡就不過多的說明。
Getter
Getter
就是 Store
狀態管理層中的計算屬性,獲取源 State
後,希望在對其進行一些包裝,再返回給元件中使用。也是就將直接獲取到 State
後在 computed
裡進行再次的過濾、包裝邏輯統統提取出放到 Getter
裡進行,提高了程式碼的複用性、可讀性。
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
複製程式碼
提取到 Getter
:
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
// `Getter` 預設第一個引數為 `state`:
getters: {
doneTodos: state => {
return state.todos.filter( todo => todo.done )
}
}
})
// 元件中獲取,通過屬性訪問
computed: {
doneTodosCount () {
return this.$store.getters.doneTodos.length
}
}
複製程式碼
⚠️ 注意: getter 在通過屬性訪問時是作為 Vue 的響應式系統的一部分進行快取。
Getter 還接受其他 getter 作為第二個引數
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 )
},
// 第二個引數為 getters
doneTodosLength: (state, getters) => {
return getters.doneTodos.length
}
}
})
複製程式碼
還可以通過給 Getter 傳遞引數獲取特定的資料
getters: {
// ...
getTodoById: state => id => {
return state.todos.find( todo => todo.id === id )
}
}
複製程式碼
元件內呼叫方式
this.$store.getters.getTodoById(2) // => { id: 2, text: '...', done: false }
複製程式碼
⚠️ 注意:getter 在通過方法訪問時,每次都會去進行呼叫,而不會快取結果。
mapGetters 輔助函式
和前面 mapState
輔助函式作用和使用上基本相同。
import { mapGetters } from 'vuex'
// getter 名稱和 計算屬性名稱相同的情況下,可以傳遞字串陣列
export default {
// ...
computed: {
...mapGetters([
'doneTodos'
])
}
}
// 傳遞物件的方式
export default {
// ...
computed: {
...mapGetters({
doneTodos: 'doneTodos',
getTodoById: 'getTodoById' // 此處傳遞回來的是一個函式,所以在使用的時候 => {{ getTodoById(2) }}
})
}
}
複製程式碼
Mutation
不能直接修改狀態,需要通過 Vuex store 中宣告的 Mutations 裡的方法修改狀態。 更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation
。mutation
是一個物件, 含有一個字串的 事件型別 (type) 和 一個 回撥函式 (handler)。
回撥函式就是我們實際進行狀態更改的地方,預設接受 state 作為第一個引數。
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
// increment 事件型別(type)名稱,increment() 回撥函式
// increment: function (state) {} 原本寫法
increment (state) {
// 變更狀態
state.count++
}
}
})
複製程式碼
mutation handler
不能被直接呼叫,需要通過 store.commit()
來通知我需要觸發一個 mutation handler
。
this.$store.commit('increment')
複製程式碼
mutation
接收引數必須只能兩個,超出的都無法獲取;第二個引數推薦傳遞的是一個物件,來接收更多的資訊。
this.$store.commit('increment', 10)
// or
this.$store.commit('increment', { num: 10 })
複製程式碼
物件風格的提交方式
this.$store.commit({
type: 'increment',
num: 10
})
複製程式碼
Mutation 需要遵守 Vue 的響應規則
Vuex 的 store 中的狀態是響應式的,那麼當我們變更狀態時,監視狀態的 Vue 元件也會自動更新。這也意味著 Vuex 中的 mutation 也需要與使用 Vue 一樣遵守一些注意事項:
- 最好提前在你的 store 中初始化好所有所需屬性(state 中的屬性)。
- 當需要在物件上新增新屬性時,你應該返回的是一個新物件
* 使用 Vue.set(obj, 'newProp', 123),或者
* 以新物件替換老物件。例如:
Object.assgin({}, state.obj, newProps)
、物件擴充套件運算子state.obj = {...state.obj, newProp: 123 }
mapMutation 輔助函式
使用方式跟 mapState
和 mapGetters
基本相同。
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')`
})
}
}
複製程式碼
呼叫方式就跟 methods
其他普通方法一樣,通過 this.<methodName>
來呼叫。
⚠️ 注意: mutation 必須是同步函式,修改 state 必須是同步的、同步的、同步的。 如果是非同步的,當觸發 mutation 的時候,內部的回撥函式還沒有被呼叫,根本不知道實際執行在何處,很難追蹤起問題。(實質上任何在回撥函式中進行的狀態的改變都是不可追蹤的。 Vuex 也提供了非同步操作的解決方案, 需要將非同步操作提取出來放入到 Action 裡進行操作。而 Mutation 只負責同步事務。
Action
在之前也講述了,Action 是用來處理非同步操作的。這裡在詳細說明一下 Action 的基本使用。
Action 類似於 mutation, 不同在於:
- Action 提交的是 mutation,而不是直接變更狀態。(不直接修改狀態,修改狀態還是需要通過 mutation)
- Action 可以包含任意非同步操作。
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
// 實踐中,經常用到引數解構來簡化程式碼, increment ({commit}) { commit('') }
increment (context) {
context.commit('increment')
}
}
})
複製程式碼
Action 函式接受一個與 store 例項具有相同方法和屬性的 context 物件(並不是真正的 store 本身),因此可以呼叫 store.commit
進行提交 mutation, 或者通過 context.state
和 context.getters
來獲取 state
和 getters
。
觸發Action
Action 通過 store.dispatch
方法觸發:
this.$store.dispatch('increment')
// 以傳遞額外引數分發
store.dispatch('incrementAsync', {
amount: 10
})
// 以物件形式分發
store.dispatch({
type: 'incrementAsync',
amount: 10
})
複製程式碼
與伺服器資料非同步請求基本在 Action 裡進行, 然後通過 Mutation 來同步應用狀態state
mapAction 輔助函式
和 mapMutions
使用方式基本一致。
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 將 `this.increment()` 對映為 `this.$store.dispatch('increment')`
// `mapActions` 也支援載荷:
'incrementBy' // 將 `this.incrementBy(amount)` 對映為 `this.$store.dispatch('incrementBy', amount)`
]),
// 傳遞物件
...mapActions({
add: 'increment' // 將 `this.add()` 對映為 `this.$store.dispatch('increment')`
})
}
}
複製程式碼
組合 Action
Action 通常是非同步的,那麼如何知道 action 什麼時候結束呢?更重要的是,我們如何才能組合多個 action,以處理更加複雜的非同步流程?
通過返回一個 Promise 物件來進行組合多個 Action。
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
複製程式碼
然後:
store.dispatch('actionA').then(() => {
// ...
})
複製程式碼
利用 async / await
,我們可以如下組合 action:
// 假設 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
複製程式碼
Module
由於 Vuex 使用單一狀態樹模式,來統一管理應用所有的狀態,導致所有狀態會集中到一個比較大的物件,隨著後續不斷得迭代,這個物件就會越來越龐大,後期的程式碼可讀性、可維護性就會不斷加大。
解決以上問題,就需要對這個物件的內部進行拆分和細分化,對狀態進行分門別類,也就產生了模組(module) 這個概念。每個模組擁有自己的 state、mutation、action、getter、甚至是巢狀子模組——從上至下進行同樣方式的分割,將龐大的系統進行合理有效的職能劃分,遵循單一職責的理念,每個模組清晰明瞭的自己的職責和職能。
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的狀態
store.state.b // -> moduleB 的狀態
複製程式碼
宣告模組後,state、mutation、action、getter 等使用方式、作用和不在 modules
內宣告方式基本一樣,只是在細節上進行了一些細微的改變,比如: getter 裡預設接收一個引數 state,模組裡接收 state 就是本身模組自己的 state 狀態了,而不是全域性的了; 呼叫獲取上也多一道需要告知那個模組獲取狀態 等一些細節上的差異。
Module 裡 state、mutation、action、getter 上的一些差異
(1)模組內部的 mutation 和 getter,接收的第一個引數 state
是模組的區域性狀態物件。
(2)模組內部的 action,區域性狀態通過 context.state
暴露出來,根節點狀態則為 context.rootState
(3)模組內部的 getter,根節點狀態會作為第三個引數暴露出來
名稱空間
預設情況下,模組內部的 action、mutation 和 getter 是註冊在全域性名稱空間的——這樣使得多個模組能夠對同一 mutation 或 action 作出響應,所以必須防止模組裡屬性或方法重名。
為了模組具有更高的封裝度、複用性和獨立性,可以通過新增 namespaced: true
的方式使其成為帶名稱空間的模組。在呼叫上也就需要新增上宣告 getter、action 及 mutation 到底屬於那個模組了,以路徑的形式表示屬於那個模組。
const store = new Vuex.Store({
modules: {
account: {
namespaced: true, // 開啟名稱空間
// 模組內容(module assets)
state: { ... }, // 模組內的狀態已經是巢狀的了,使用 `namespaced` 屬性不會對其產生影響
getters: {
isAdmin () { ... } // -> getters['account/isAdmin'] 呼叫時以路徑的形式表明歸屬
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 巢狀模組
modules: {
// 繼承父模組的名稱空間
myPage: {
state: { ... },
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// 進一步巢狀名稱空間
posts: {
namespaced: true,
state: { ... },
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})
複製程式碼
在帶名稱空間的模組內訪問全域性內容
帶名稱空間的模組內訪問全域性 state 、getter 和 action, rootState
和 rootGetter
會作為第三和第四引數傳入 getter
,也會通過 context
物件的屬性傳入 action。
需要在全域性名稱空間內分發 action
或提交 mutation
,將 { root: true }
作為第三引數傳給 dispatch
或 commit
即可。
modules: {
foo: {
namespaced: true,
getters: {
// 在這個模組的 getter 中,`getters` 被區域性化了
// 全域性的 state 和 getters 可以作為第三、四個引數進行傳入,從而訪問全域性 state 和 getters
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
someOtherGetter: state => { ... }
},
actions: {
// 在這個模組中, dispatch 和 commit 也被區域性化了
// 他們可以接受 `root` 屬性以訪問根 dispatch 或 commit
someAction ({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'
dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
},
someOtherAction (ctx, payload) { ... }
}
}
}
複製程式碼
在帶名稱空間的模組註冊全域性 action
需要在帶名稱空間的模組註冊全域性 action
,你可新增 root: true
,並將這個 action 的定義放在函式 handler 中。例如:
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
}
複製程式碼
帶名稱空間的模組裡輔助函式如何使用?
將模組的空間名稱字串作為第一個引數傳遞給上述函式,這樣所有繫結都會自動將該模組作為上下文。
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo',
'bar'
])
}
複製程式碼
還可以通過使用 createNamespacedHelpers
建立基於某個名稱空間輔助函式。它返回一個物件,物件裡有新的繫結在給定名稱空間值上的元件繫結輔助函式:
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
// 在 `some/nested/module` 中查詢
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 `some/nested/module` 中查詢
...mapActions([
'foo',
'bar'
])
}
}
複製程式碼
模組動態註冊
在 store 建立之後,你可以使用 store.registerModule
方法註冊模組:
// 註冊模組 `myModule`
store.registerModule('myModule', {
// ...
})
// 註冊巢狀模組 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})
複製程式碼
模組動態註冊功能使得其他 Vue 外掛可以通過在 store 中附加新模組的方式來使用 Vuex 管理狀態。例如,vuex-router-sync 外掛就是通過動態註冊模組將 vue-router
和 vuex
結合在一起,實現應用的路由狀態管理。
你也可以使用 store.unregisterModule(moduleName)
來動態解除安裝模組。注意,你不能使用此方法解除安裝靜態模組(即建立 store 時宣告的模組)。
待更新~