一、Vuex介紹
Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。
Vuex 也集成到 Vue 的官方調試工具devtools extension,提供了諸如零配置的 time-tavel 調試、狀態快照導入導出等高級調試功能。
1、狀態管理模式
一個簡單的 Vue 計數應用范例如下所示:
new Vue({ // state data () { return { count: 0 } }, // view template: ` <div>{{ count }}</div> `, // actions methods: { increment () { this.count++ } } })
狀態自管理應用包含如下部分:
- state,驅動應用的數據源;
- view,以聲明方式將state映射到視圖;
- actions,響應在view上的用戶輸入導致的狀態變化;
2、單向數據流理念
用下圖表示“單向數據流”理念的簡單示意:

但在應用遇到 多組件共享狀態 時,單向數據流的簡潔性很容易遭到破壞:
(1)問題一:多個視圖依賴於同一狀態。
傳參的方法對於多層嵌套的組件將會非常繁瑣,並且對於兄弟組件間的狀態傳遞無能為力。
(2)問題二:來自不同視圖的行為需要變更同一狀態。
經常會采用父子組件直接引用或者通過事件來變更和同步狀態的多份拷貝。以上的這些模式非常脆弱,通常會導致無法維護的代碼。
(3)解決思路
把組件的共享狀態抽取出來,以一個全局單例模式管理!!
在這種模式下,我們的組件樹構成了一個巨大的“視圖”,不管在樹的哪個位置,任何組件都能獲取狀態或者觸發行為!
通過定義和隔離狀態管理中的各種概念並通過強制規則維持視圖和狀態間的獨立性,我們的代碼將會變得更結構化且易維護。
以上也是Vuex背后的基本思想,借鑒了 Flux、Redux 和 The Elm Architecture。與其他模式不同的是,Vuex 是專門為 Vue.js 設計的狀態管理庫,以利用 Vue.js 的細粒度數據響應機制來進行高效的狀態更新。
3、Vuex圖解

首先通過vue組件觸發,比如執行了一個按鈕事件,修改一個頁面中的數據。然后通過dispatch方法分發到Vuex(類似一個商店,包含有Actions、Mutations、State),將數據共享給Vuex中的State(一定是關聯於某個組件的某個標簽上);當要更改數據時,先分發到Actions,再提交給Mutations,隨后修改State。最后通過Render方法將數據渲染到vue組件中。
需要特別注意:Dispatch這一步是異步的,而Commit這一步是同步的。
4、項目結構
(1)Vuex必須遵循的規則
Vuex不限制代碼結構,但規定了一些需要遵守的規則:
1)應用層級的狀態應集中到單個 store 對象中;
2)提交 mutation 是更改狀態的唯一方法,且這個過程是同步的;
3)異步邏輯都應該封裝在 action 里面。
只要遵循上述規則,可以任意組織代碼。如果遇到store文件過大的情況,只需將 action、mutation 和 getter 分割到單獨的文件。
(2)項目結構示例
對於大型應用,會希望將 Vuex 相關代碼分割到模塊中。如下例所示:
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API請求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我們組裝模塊並導出 store 的地方
├── actions.js # 根級別的 action
├── mutations.js # 根級別的 mutation
└── modules
├── cart.js # 購物車模塊
└── products.js # 產品模塊
二、Vuex實踐
1、安裝Vuex
(1)直接下載/CDN引用方式
CDN地址:https://unpkg.com/vuex,該鏈接一直指向NPM上發布的最新版本。
還可以使用 https://unpkg.com/vuex@2.0.0 這樣的方式指定特定的版本。
當使用全局script標簽引用 vuex 時,會進行自動安裝:
<script src="/path/to/vue.js"></script> <script src="/path/to/vuex.js"></script>
(2)NPM安裝
npm install vuex --save
在模塊化的打包系統中,必須顯式地通過 Vue.use()來安裝Vuex:
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex)
2、vuex-demo項目創建配置
創建測試項目如下所示:
$ vue init webpack vuex-demo ? Project name vuex-demo ? Project description A Vue.js project ? Author xiugeng <443614404@qq.com> ? Vue build standalone ? Install vue-router? No ? Use ESLint to lint your code? No ? Set up unit tests No ? Setup e2e tests with Nightwatch? No ? Should we run `npm install` for you after the project has been created? (recom mended) npm vue-cli · Generated "vuex-demo".
安裝vuex組件:
$ cd vuex-demo/ $ npm install vuex -S
3、Vuex應用核心——Store(倉庫)
“Store”基本上就是一個容器,它包含着應用中大部分的狀態(state)。
(1)Vuex和單純的全局對象的兩點不同
1)Vuex的狀態存儲是響應式的
當Vue組件從 Store 中讀取狀態的時候,若 Store 中狀態發送變化,那么相應的組件也會相應得到高效更新。
2)不能直接改變 Store 中的狀態
改變 Store 中的狀態的唯一途徑就是下顯式地提交(commit)mutation。這樣使得可以方便地跟蹤每個狀態的變化,從而能夠通過實現一些工具幫助更好地了解自己的應用。
(2)簡單 Store樣例
安裝 Vuex 后,創建一個 store。創建 src/store 目錄,再創建 src/store/index.js 文件,用作組裝模塊並導出store。
import Vue from 'vue' import Vuex from 'vuex' // 使用插件(如果在模塊化構建系統中,請確保在開頭調用了 Vue.use(Vuex)) Vue.use(Vuex); const store = new Vuex.Store({ // 五大將:state mutation action getter module state:{ count: 1 }, mutations:{ }, actions:{ } }); export default store;
在 src/main.js 文件中引入Store,將store保存在組件中,共享store狀態:
import Vue from 'vue' import App from './App' import store from './store/index' Vue.config.productionTip = false; /* eslint-disable no-new */ new Vue({ el: '#app', store, // store保存在組件中,可共享store狀態 components: { App }, template: '<App/>' })
在修改 src/components/HelloWorld.vue,使用computed實時監聽狀態對象:
<template>
<div class="hello">
<h2>{{myCount}}</h2>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
computed: {
myCount(){
// 通過 store.state 來獲取狀態對象
return this.$store.state.count;
}
}
}
</script>
由於 store 中的狀態是響應式的,在組件中調用 store 中的狀態簡單到僅需要在計算屬性中返回即可。
顯示效果:

(3)子組件共享store信息
創建組件 src/components/Child.vue,編碼內容如下所示:
<template>
<div>
<p>{{myCount}}</p>
</div>
</template>
<script>
export default {
name: "Child",
data() {
return {
};
},
computed: {
myCount(){
return this.$store.state.count;
}
}
};
</script>
在組件 src/components/HelloWorld.vue 中引入子組件,形成父子關系:
<template>
<div class="hello">
<h2>{{myCount}}</h2>
<!-- 渲染子組件 -->
<Child/>
</div>
</template>
<script>
// 引入子組件,形成父子關系
import Child from './Child'
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
computed: {
myCount(){
// 通過 store.state 來獲取狀態對象
return this.$store.state.count;
}
},
components: {
Child
}
}
</script>
顯示效果如下所示:

三、Vuex——mutation使用
1、 mutation 狀態變更
更改 Vuex 的 store 中的狀態(state)的唯一方法是提交(commit) mutation。Vuex 中的 mutation 非常類似於事件:每個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)。
修改 HelloWorld.vue 文件:
<template>
<div class="hello">
<h2>{{myCount}}</h2>
<!-- 渲染子組件 -->
<Child/>
<button @click="change">修改</button>
</div>
</template>
<script>
// 引入子組件,形成父子關系
import Child from './Child'
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
computed: {
myCount(){
// 通過 store.state 來獲取狀態對象
return this.$store.state.count;
}
},
components: {
Child
},
methods: {
change(){
// 修改狀態
this.$store.commit('addCount',3)
}
}
}
</script>
再在 store/index.js中聲明 mutations 方法 addCount:
const store = new Vuex.Store({ // 五大將:state mutation action getter module state:{ count: 1 }, mutations:{ // 聲明方法 // 只能做同步操作,不能直接commit addCount(state, val) { state.count += val; } }, actions:{ } });
顯示效果:

點擊修改按鈕,頁面上顯示的數字從1》4》7》10》13實時修改變換。
通過提交 mutation 的方式,而非直接改變 store.state.count,是因為我們想要更明確地追蹤到狀態的變化。這個簡單的約定能夠讓你的意圖更加明顯,這樣你在閱讀代碼的時候能更容易地解讀應用內部的狀態改變。此外,這樣也讓我們有機會去實現一些能記錄每次狀態改變,保存狀態快照的調試工具。有了它,我們甚至可以實現如時間穿梭般的調試體驗。
2、mutation中不能做異步操作
Mutation 重要的原則就是要記住 mutation 必須是同步函數。
每一條 mutation 被記錄,devtools 都需要捕捉到前一狀態和后一狀態的快照。然而,在上面的例子中 mutation 中的異步函數中的回調讓這不可能完成:因為當 mutation 觸發的時候,回調函數還沒有被調用,devtools 不知道什么時候回調函數實際上被調用——實質上任何在回調函數中進行的狀態的改變都是不可追蹤的。
(1)在HelloWorld.vue中添加異步操作按鈕
<template>
<div class="hello">
<h2>{{myCount}}</h2>
<!-- 渲染子組件 -->
<Child/>
<button @click="change">同步修改</button>
<button @click="asyncHandler">異步修改</button>
</div>
</template>
<script>
// 引入子組件,形成父子關系
import Child from './Child'
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
computed: {
myCount(){
// 通過 store.state 來獲取狀態對象
return this.$store.state.count;
}
},
components: {
Child
},
methods: {
change(){
// 修改狀態,更改 Vuex的store中的狀態state的唯一方法是提交(commit)mutation
this.$store.commit('addCount',3);
},
asyncHandler(){
this.$store.commit('asyncHandler',1);
}
}
}
</script>
(2)mutation處理提交的事件
在src/store/index.js中添加異步操作:
import Vue from 'vue' import Vuex from 'vuex' // 使用插件(如果在模塊化構建系統中,請確保在開頭調用了 Vue.use(Vuex)) Vue.use(Vuex); const store = new Vuex.Store({ // 五大將:state mutation action getter module state:{ count: 1 }, mutations:{ // 聲明方法 // 只能做同步操作,不能直接commit addCount(state, val) { state.count += val; // 同步操作 }, asyncHandler(state, val) { setTimeout(()=>{ // 異步操作 state.count += val; }, 2000); }, }, actions:{ } }); export default store;
(3)現象展示
1.使用Vue Devtool調試系統:

2.點擊同步修改:

3.點擊異步修改:

注意:異步操作執行后,頁面上的值發生了修改,但卻沒有修改state中的值。其他組件調用state值時會出現數據對不上。
四、vuex——Action 的使用
Action 類似於 mutation,不同在於:
- Action 提交的是 mutation,而不是直接變更狀態
- Action 可以包含任意異步操作
1、注冊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。
實踐中,通常使用 ES2015 的參數解構 來簡化代碼(尤其是需要多次調用 commit 的場景)。
src/store/index.js 修改如下所示:
import Vue from 'vue' import Vuex from 'vuex' // 使用插件(如果在模塊化構建系統中,請確保在開頭調用了 Vue.use(Vuex)) Vue.use(Vuex); const store = new Vuex.Store({ // 五大將:state mutation action getter module state:{ count: 1 }, mutations:{ // 聲明方法 // 只能做同步操作,不能直接commit addCount(state, val) { state.count += val; // 同步操作 }, asyncHandler(state, val) { // 只做同步操作 state.count += val; }, }, actions:{ // Action類似mutation,但Action提交的是mutation,不直接變更狀態,且可以包含任意異步操作 addCount({commit},val) { commit('addCount',val); }, asyncHandler({commit},val) { setTimeout(()=>{ commit('asyncHandler', val); },2000) } } }); export default store;
2、分發Action
Action 通過 store..dispatch 方法觸發,在組件中使用 this.$store.dispatch('xxx') 分發 action,修改HelloWorld.vue如下所示:
<template>
<div class="hello">
<h2>{{myCount}}</h2>
<!-- 渲染子組件 -->
<Child/>
<button @click="change">同步修改</button>
<button @click="asyncHandler">異步修改</button>
</div>
</template>
<script>
// 引入子組件,形成父子關系
import Child from './Child'
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
computed: {
myCount(){
// 通過 store.state 來獲取狀態對象
return this.$store.state.count;
}
},
components: {
Child
},
methods: {
change(){
// 修改狀態,更改 Vuex的store中的狀態state的唯一方法是提交(commit)mutation
this.$store.dispatch('addCount',3);
},
asyncHandler(){
this.$store.dispatch('asyncHandler',1);
}
}
}
</script>
異步修改state的值,問題解決,點擊兩次同步修改和一次異步修改后,顯示如下所示:

之所以不直接分發 mutation 而要多這一步操作,主要是因為 mutation必須同步執行這個限制。而Action則沒有這個約束,可以在action內部執行異步操作。
