Vuex 是一個專爲 Vue.js 應用程序開發的** 狀態管理模式** 。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化vue
首先,咱們先看一下下面這個例子vue-router
new Vue({
// state
data () {
return {
count: 0
}
},
// view
template: `
<div>{{ count }}</div>
`,
// actions
methods: {
increment () {
this.count++
}
}
})
複製代碼
這個狀態自管理應用包含如下幾個部分:vuex
如下是一個表示「單向數據流」理念的簡單示意:api
可是,當咱們的應用遇到多個組件共享狀態時,單向數據流的簡潔性很容易被破壞:緩存
Vuex 是專門爲 Vue.js 設計的狀態管理庫,以利用 Vue.js 的細粒度數據響應機制來進行高效的狀態更新。bash
Vuex 能夠幫助咱們管理共享狀態,並附帶了更多的概念和框架。這須要對短時間和長期效益進行權衡。 若是您不打算開發大型單頁應用,使用 Vuex 多是繁瑣冗餘的。確實是如此——若是您的應用夠簡單,您最好不要使用 Vuex。一個簡單的 store 模式就足夠您所需了。可是,若是您須要構建一箇中大型單頁應用,您極可能會考慮如何更好地在組件外部管理狀態,Vuex 將會成爲天然而然的選擇app
具體的代碼實現框架
const store = new Vuex.Store({
state: {
token: 'xxx12345'
},
mutations: {
changeToken(state, token) {
state.token = token
}
}
})
// 經過mutation去改變token的狀態值
store.commit('changeToken', 'xxxx12345555')
複製代碼
再次強調,咱們經過提交 mutation 的方式,而非直接改變 store.state.count,是由於咱們想要更明確地追蹤到狀態的變化。這個簡單的約定可以讓你的意圖更加明顯,這樣你在閱讀代碼的時候能更容易地解讀應用內部的狀態改變。此外,這樣也讓咱們有機會去實現一些能記錄每次狀態改變,保存狀態快照的調試工具。有了它,咱們甚至能夠實現如時間穿梭般的調試體驗。異步
因爲 store 中的狀態是響應式的,在組件中調用 store 中的狀態簡單到僅須要在計算屬性中返回便可。觸發變化也僅僅是在組件的 methods 中提交 mutation。
** 單一狀態樹 ** Vuex 使用單一狀態樹——是的,用一個對象就包含了所有的應用層級狀態。至此它便做爲一個「惟一數據源 (SSOT)」而存在。這也意味着,每一個應用將僅僅包含一個 store 實例。單一狀態樹讓咱們可以直接地定位任一特定的狀態片斷,在調試的過程當中也能輕易地取得整個當前應用狀態的快照。
單狀態樹和模塊化並不衝突——在後面的章節裏咱們會討論如何將狀態和狀態變動事件分佈到各個子模塊中
** 在 Vue 組件中得到 Vuex 狀態 **
那麼咱們如何在 Vue 組件中展現狀態呢?因爲 Vuex 的狀態存儲是響應式的,從 store 實例中讀取狀態最簡單的方法就是在計算屬性中返回某個狀態:
// 建立一個 tree 組件
const trees = {
template: `<div>{{ tree }}</div>`,
computed: {
tree () {
return store.state.tree
}
}
}
複製代碼
每當 store.state.tree 變化的時候, 都會從新求取計算屬性,而且觸發更新相關聯的 DOM。
然而,這種模式致使組件依賴全局狀態單例。在模塊化的構建系統中,在每一個須要使用 state 的組件中須要頻繁地導入,而且在測試組件時須要模擬狀態。
Vuex 經過 store 選項,提供了一種機制將狀態從根組件「注入」到每個子組件中(需調用 Vue.use(Vuex)):
const app = new Vue({
el: '#app',
// 把 store 對象提供給 「store」 選項,這能夠把 store 的實例注入全部的子組件
store,
components: { trees },
template: `
<div class="app">
<trees></trees>
</div>
`
})
複製代碼
經過在根實例中註冊 store 選項,該 store 實例會注入到根組件下的全部子組件中,且子組件能經過 this.$store 訪問到。讓咱們更新下 trees 的實現:
const trees = {
template: `<div>{{ tree }}</div>`,
computed: {
count () {
return this.$store.state.tree
}
}
}
複製代碼
有時候咱們須要從 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 在經過屬性訪問時是做爲 Vue 的響應式系統的一部分緩存其中的。
** 經過方法訪問 ** 你也能夠經過讓 getter 返回一個函數,來實現給 getter 傳參。在你對 store 裏的數組進行查詢時很是有用。
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
複製代碼
注意,getter 在經過方法訪問時,每次都會去進行調用,而不會緩存結果。
更改 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
})
複製代碼
** 對象風格的提交方式 **
store.commit({
type: 'increment',
amount: 10
})
複製代碼
當使用對象風格的提交方式,整個對象都做爲載荷傳給 mutation 函數,所以 handler 保持不變:
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
複製代碼
** Mutation 需遵照 Vue 的響應規則 ** 既然 Vuex 的 store 中的狀態是響應式的,那麼當咱們變動狀態時,監視狀態的 Vue 組件也會自動更新。這也意味着 Vuex 中的 mutation 也須要與使用 Vue 同樣遵照一些注意事項:
最好提早在你的 store 中初始化好全部所需屬性。
當須要在對象上添加新屬性時,你應該
使用 Vue.set(obj, 'newProp', 123), 或者
以新對象替換老對象。例如,利用 stage-3 的對象展開運算符咱們能夠這樣寫:
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')`
})
}
}
複製代碼
在 scrimba 上嘗試這節課 Action 相似於 mutation,不一樣在於:
Action 提交的是 mutation,而不是直接變動狀態。 Action 能夠包含任意異步操做。 讓咱們來註冊一個簡單的 action:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
複製代碼
Action 函數接受一個與 store 實例具備相同方法和屬性的 context 對象,所以你能夠調用 context.commit 提交一個 mutation,或者經過 context.state 和 context.getters 來獲取 state 和 getters。當咱們在以後介紹到 Modules 時,你就知道 context 對象爲何不是 store 實例自己了。
實踐中,咱們會常常用到 ES2015 的 參數解構 來簡化代碼(特別是咱們須要調用 commit 不少次的時候):
actions: {
increment ({ commit }) {
commit('increment')
}
}
複製代碼
** 分發 Action ** Action 經過 store.dispatch 方法觸發:
store.dispatch('increment') 乍一眼看上去感受畫蛇添足,咱們直接分發 mutation 豈不更方便?實際上並不是如此,還記得 mutation 必須同步執行這個限制麼?Action 就不受約束!咱們能夠在 action 內部執行異步操做:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
複製代碼
Actions 支持一樣的載荷方式和對象方式進行分發:
// 以載荷形式分發
store.dispatch('incrementAsync', {
amount: 10
})
// 以對象形式分發
store.dispatch({
type: 'incrementAsync',
amount: 10
})
複製代碼
來看一個更加實際的購物車示例,涉及到調用異步 API 和分發多重 mutation:
actions: {
checkout ({ commit, state }, products) {
// 把當前購物車的物品備份起來
const savedCartItems = [...state.cart.added]
// 發出結帳請求,而後樂觀地清空購物車
commit(types.CHECKOUT_REQUEST)
// 購物 API 接受一個成功回調和一個失敗回調
shop.buyProducts(
products,
// 成功操做
() => commit(types.CHECKOUT_SUCCESS),
// 失敗操做
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}
複製代碼
注意咱們正在進行一系列的異步操做,而且經過提交 mutation 來記錄 action 產生的反作用(即狀態變動)。
** 在組件中分發 Action ** 你在組件中使用 this.$store.dispatch('xxx') 分發 action,或者使用 mapActions 輔助函數將組件的 methods 映射爲 store.dispatch 調用(須要先在根節點注入 store):
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,以處理更加複雜的異步流程?
首先,你須要明白 store.dispatch 能夠處理被觸發的 action 的處理函數返回的 Promise,而且 store.dispatch 仍舊返回 Promise:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
複製代碼
如今你能夠:
store.dispatch('actionA').then(() => {
// ...
})
在另一個 action 中也能夠:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
複製代碼
最後,若是咱們利用 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())
}
}
複製代碼
一個 store.dispatch 在不一樣模塊中能夠觸發多個 action 函數。在這種狀況下,只有當全部觸發函數完成後,返回的 Promise 纔會執行。
因爲使用單一狀態樹,應用的全部狀態會集中到一個比較大的對象。當應用變得很是複雜時,store 對象就有可能變得至關臃腫。
爲了解決以上問題,Vuex 容許咱們將 store 分割成模塊(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 的狀態
複製代碼
** 模塊的局部狀態 **
const moduleA = {
state: { count: 0 },
mutations: {
increment (state) {
// 這裏的 `state` 對象是模塊的局部狀態
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}
複製代碼
一樣,對於模塊內部的 action,局部狀態經過 context.state 暴露出來,根節點狀態則爲 context.rootState:
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}
複製代碼
對於模塊內部的 getter,根節點狀態會做爲第三個參數暴露出來:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
複製代碼
** 命名空間 ** 默認狀況下,模塊內部的 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']
}
}
}
}
}
})
複製代碼
啓用了命名空間的 getter 和 action 會收到局部化的 getter,dispatch 和 commit。換言之,你在使用模塊內容(module assets)時不須要在同一模塊內額外添加空間名前綴。更改 namespaced 屬性後不須要修改模塊內的代碼。
** 在帶命名空間的模塊內訪問全局內容(Global Assets)** 若是你但願使用全局 state 和 getter,rootState 和 rootGetter 會做爲第三和第四參數傳入 getter,也會經過 context 對象的屬性傳入 action。
若須要在全局命名空間內分發 action 或提交 mutation,將 { root: true } 做爲第三參數傳給 dispatch 或 commit 便可。
modules: {
foo: {
namespaced: true,
getters: {
// 在這個模塊的 getter 中,`getters` 被局部化了
// 你可使用 getter 的第四個參數來調用 `rootGetters`
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'
}
}
}
}
}
複製代碼
** 帶命名空間的綁定函數 ** 當使用 mapState, mapGetters, mapActions 和 mapMutations 這些函數來綁定帶命名空間的模塊時,寫起來可能比較繁瑣:
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()
])
}
複製代碼
對於這種狀況,你能夠將模塊的空間名稱字符串做爲第一個參數傳遞給上述函數,這樣全部綁定都會自動將該模塊做爲上下文。因而上面的例子能夠簡化爲:
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.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'
])
}
}
複製代碼
** 給插件開發者的注意事項 ** 若是你開發的插件(Plugin)提供了模塊並容許用戶將其添加到 Vuex store,可能須要考慮模塊的空間名稱問題。對於這種狀況,你能夠經過插件的參數對象來容許用戶指定空間名稱:
// 經過插件的參數對象獲得空間名稱 // 而後返回 Vuex 插件函數
export function createPlugin (options = {}) {
return function (store) {
// 把空間名字添加到插件模塊的類型(type)中去
const namespace = options.namespace || ''
store.dispatch(namespace + 'pluginAction')
}
}
複製代碼
** 模塊動態註冊 ** 在 store 建立以後,你可使用 store.registerModule 方法註冊模塊:
// 註冊模塊 myModule
store.registerModule('myModule', { // ... }) // 註冊嵌套模塊 nested/myModule
store.registerModule(['nested', 'myModule'], { // ... }) 以後就能夠經過 store.state.myModule 和 store.state.nested.myModule 訪問模塊的狀態。
模塊動態註冊功能使得其餘 Vue 插件能夠經過在 store 中附加新模塊的方式來使用 Vuex 管理狀態。例如,vuex-router-sync 插件就是經過動態註冊模塊將 vue-router 和 vuex 結合在一塊兒,實現應用的路由狀態管理。
你也可使用 store.unregisterModule(moduleName) 來動態卸載模塊。注意,你不能使用此方法卸載靜態模塊(即建立 store 時聲明的模塊)。
** 保留 state ** 在註冊一個新 module 時,你頗有可能想保留過去的 state,例如從一個服務端渲染的應用保留 state。你能夠經過 preserveState 選項將其歸檔:store.registerModule('a', module, { preserveState: true })。
當你設置 preserveState: true 時,該模塊會被註冊,action、mutation 和 getter 會被添加到 store 中,可是 state 不會。這裏假設 store 的 state 已經包含了這個 module 的 state 而且你不但願將其覆寫。
** 模塊重用 ** 有時咱們可能須要建立一個模塊的多個實例,例如:
建立多個 store,他們公用同一個模塊 (例如當 runInNewContext 選項是 false 或 'once' 時,爲了在服務端渲染中避免有狀態的單例) 在一個 store 中屢次註冊同一個模塊 若是咱們使用一個純對象來聲明模塊的狀態,那麼這個狀態對象會經過引用被共享,致使狀態對象被修改時 store 或模塊間數據互相污染的問題。
實際上這和 Vue 組件內的 data 是一樣的問題。所以解決辦法也是相同的——使用一個函數來聲明模塊狀態(僅 2.3.0+ 支持):
const MyReusableModule = {
state () {
return {
foo: 'bar'
}
},
// mutation, action 和 getter 等等...
}
複製代碼