Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。
在 Vue 之後引入 vuex
會進行自動安裝:
<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>
可以通過 https://unpkg.com/vuex@2.0.0
這樣的方式指定特定的版本。
NPM:npm install vuex --save
State
在 Vue 元件中獲得 Vuex 狀態
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({
// ...
})
}
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) => (id) => {
return state.todos.find(todo => 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++
}
}
})
store.commit(`increment`)
當使用物件風格的提交方式,整個物件都作為載荷傳給 mutation 函式,因此 handler 保持不變:
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
使用常量替代 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
你可以在元件中使用 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
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
。
實踐中,我們會經常用到 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)
)
}
}
在元件中分發 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`)`
})
}
}
Module
由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的物件。當應用變得非常複雜時,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 的狀態
一般專案結構
Vuex 並不限制你的程式碼結構。但是,它規定了一些需要遵守的規則:
- 1.應用層級的狀態應該集中到單個 store 物件中。
- 2.提交 mutation 是更改狀態的唯一方法,並且這個過程是同步的。
- 3.非同步邏輯都應該封裝到 action 裡面。
只要你遵守以上規則,如何組織程式碼隨你便。如果你的 store 檔案太大,只需將 action、mutation 和 getter 分割到單獨的檔案。
對於大型應用,我們會希望把 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
npm install --save vuex
<!--這裡假定你已經搭好vue的開發環境了-->
配置vuex
1、首先建立一個js檔案,假定這裡取名為store.js
2、在main.js檔案中引入上面建立的store.js
//main.js內部對store.js的配置
import store from `"@/store/store.js`
//具體地址具體路徑
new Vue({
el: `#app`,
store, //將store暴露出來
template: `<App></App>`,
components: { App }
});
store.js中的配置
import Vue from `vue`; //首先引入vue
import Vuex from `vuex`; //引入vuex
Vue.use(Vuex)
export default new Vuex.Store({
state: {
// state 類似 data
//這裡面寫入資料
},
getters:{
// getters 類似 computed
// 在這裡面寫個方法
},
mutations:{
// mutations 類似methods
// 寫方法對資料做出更改(同步操作)
},
actions:{
// actions 類似methods
// 寫方法對資料做出更改(非同步操作)
}
})
使用vuex
我們約定store中的資料是以下形式
state:{
goods: {
totalPrice: 0,
totalNum:0,
goodsData: [
{
id: `1`,
title: `好吃的蘋果`,
price: 8.00,
image: `https://www.shangdian.com/static/pingguo.jpg`,
num: 0
},
{
id: `2`,
title: `美味的香蕉`,
price: 5.00,
image: `https://www.shangdian.com/static/xiangjiao.jpg`,
num: 0
}
]
}
},
gettles:{ //其實這裡寫上這個主要是為了讓大家明白他是怎麼用的,
totalNum(state){
let aTotalNum = 0;
state.goods.goodsData.forEach((value,index) => {
aTotalNum += value.num;
})
return aTotalNum;
},
totalPrice(state){
let aTotalPrice = 0;
state.goods.goodsData.forEach( (value,index) => {
aTotalPrice += value.num * value.price
})
return aTotalPrice.toFixed(2);
}
},
mutations:{
reselt(state,msg){
console.log(msg) //我執行了一次;
state.goods.totalPrice = this.getters.totalPrice;
state.goods.totalNum = this.getters.totalNum;
},
reduceGoods(state,index){
//第一個引數為預設引數,即上面的state,後面的引數為頁面操作傳過來的引數
state.goodsData[index].num-=1;
let msg = `我執行了一次`
this.commit(`reselt`,msg);
},
addGoods(state,index){
state.goodsData[index].num+=1;
let msg = `我執行了一次`
this.commit(`reselt`,msg);
/**
想要重新渲染store中的方法,一律使用commit 方法
你可以這樣寫 commit(`reselt`,{
state: state
})
也可以這樣寫 commit({
type: `reselt`,
state: state
})
主要看你自己的風格
**/
}
},
actions:{
//這裡主要是操作非同步操作的,使用起來幾乎和mutations方法一模一樣
//除了一個是同步操作,一個是非同步操作,這裡就不多介紹了,
//有興趣的可以自己去試一試
//比如你可以用setTimeout去嘗試一下
}
好了,簡單的資料我們就這樣配置了,接下來看看購物車頁面吧;
第一種方式使用store.js中的資料(直接使用)
<template>
<div id="goods" class="goods-box">
<ul class="goods-body">
<li v-for="(list,index) in goods.goodsData" :key="list.id">
<div class="goods-main">
<img :src="list.image">
</div>
<div class="goods-info">
<h3 class="goods-title">{{ list.title }}</h3>
<p class="goods-price">¥ {{ list.price }}</p>
<div class="goods-compute">
<!--在dom中使用方法為:$store.commit()加上store.js中的屬性的名稱,示例如下-->
<span class="goods-reduce" @click="$store.commit(`reduceGoods`,index)">-</span>
<input readonly v-model="list.num" />
<span class="goods-add" @click="$store.commit(`addGoods`,index)">+</span>
</div>
</div>
</li>
</ul>
<div class="goods-footer">
<div class="goods-total">
合計:¥ {{ goods.totalPrice }}
<!--
如果你想要直接使用一些資料,但是在computed中沒有給出來怎麼辦?
可以寫成這樣
{{ $store.state.goods.totalPrice }}
或者直接獲取gettles裡面的資料
{{ $store.gettles.totalPrice }}
-->
</div>
<button class="goods-check" :class="{activeChecke: goods.totalNum <= 0}">去結賬({{ goods.totalNum }})</button>
</div>
</div>
</template>
<script>
export default {
name: `Goods`,
computed:{
goods(){
return this.$store.state.goods;
}
}
}
</script>
如果上面的方式寫引數讓你看的很彆扭,我們繼續看第二種方式
第一種方式使用store.js中的資料(通過輔助函式使用)
<!--goods.vue 購物車頁面-->
<template>
<div id="goods" class="goods-box">
<ul class="goods-body">
<li v-for="(list,index) in goods.goodsData" :key="list.id">
<div class="goods-main">
<img :src="list.image">
</div>
<div class="goods-info">
<h3 class="goods-title">{{ list.title }}</h3>
<p class="goods-price">¥ {{ list.price }}</p>
<div class="goods-compute">
<span class="goods-reduce" @click="goodsReduce(index)">-</span>
<input readonly v-model="list.num" />
<span class="goods-add" @click="goodsAdd(index)">+</span>
</div>
</div>
</li>
</ul>
<div class="goods-footer">
<div class="goods-total">
合計:¥ {{ goods.totalPrice }}
<!--
gettles裡面的資料可以直接這樣寫
{{ totalPrice }}
-->
</div>
<button class="goods-check" :class="{activeChecke: goods.totalNum <= 0}">去結賬({{ goods.totalNum }})</button>
</div>
</div>
</template>
<script>
import {mapState,mapGetters,mapMutations} from `vuex`;
/**
上面大括弧裡面的三個引數,便是一一對應著store.js中的state,gettles,mutations
這三個引數必須規定這樣寫,寫成其他的單詞無效,切記
畢竟是這三個屬性的的輔助函式
**/
export default {
name: `Goods`,
computed:{
...mapState([`goods`])
...mapGetters([`totalPrice`,`totalNum`])
/**
‘...’ 為ES6中的擴充套件運算子,不清楚的可以百度查一下
如果使用的名稱和store.js中的一樣,直接寫成上面陣列的形式就行,
如果你想改變一下名字,寫法如下
...mapState({
goodsData: state => stata.goods
})
**/
},
methods:{
...mapMutations([`goodsReduce`,`goodsAdd`]),
/**
這裡你可以直接理解為如下形式,相當於直接呼叫了store.js中的方法
goodsReduce(index){
// 這樣是不是覺得很熟悉了?
},
goodsAdd(index){
}
好,還是不熟悉,我們換下面這種寫法
onReduce(index){
//我們在methods中定義了onReduce方法,相應的Dom中的click事件名要改成onReduce
this.goodsReduce(index)
//這相當於呼叫了store.js的方法,這樣是不是覺得滿意了
}
**/
}
}
</script>
Module
const moduleA = {
state: { /*data**/ },
mutations: { /**方法**/ },
actions: { /**方法**/ },
getters: { /**方法**/ }
}
const moduleB = {
state: { /*data**/ },
mutations: { /**方法**/ },
actions: { /**方法**/ }
}
export default new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
//那怎麼呼叫呢?看下面!
//在模組內部使用
state.goods //這種使用方式和單個使用方式樣,直接使用就行
//在元件中使用
store.state.a.goods //先找到模組的名字,再去呼叫屬性
store.state.b.goods //先找到模組的名字,再去呼叫屬性
參考地址:《震驚!喝個茶的時間就學會了vuex》