認識Vuex

學前準備


本文主要內容來源于官網,為Vuex的基礎部分總結,部分案例也參考了互聯網上其他分享知識的大佬。本手記結合官網內容也額外添加了自己的一些的理解,希望能給你帶來一些參考價值,如果文章有理解不到位的地方,還請各位多批評指正!以下是本文中可能用到的參考資料:
點擊進入vuex官方教程

點擊進入Vue CLI官方教程

點擊了解Vuex Action中的參數解構為什么那么寫

點擊了解Action和Promise中Promise詳解

為什么使用Vuex


Vuex的官方解答:Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件的狀態,并以相應的規則保證狀態以一種可預測的方式發生變化。

在vue開發的過程中,我們經常遇到一個狀態可能會在多個組件之間使用,比如用戶信息、頭像、昵稱等,我們修改了這些信息,那么整個應用中凡是使用了該信息的部分都應該更新。想讓vue中所有組件都共享這些信息,那么就需要將這些信息進行集中管理,這個時候我們就需要用到Vuex

通過Vue CLI生成項目模板


日常開發中,我們大多都是用Vue CLI腳手架來生成一個vue項目,不太清楚腳手架怎么使用的可以移步Vue CLI官網自行查閱。在使用腳手架生成的項目時會讓我們選擇store,選擇后會在頁面中給我們生成store文件夾,自帶初始化倉庫的index.js,這就是最初的store,里面結構如下:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {}
})

在模板中我們可以看到statemutationsactionsmodules這幾個對象,而Vuex中幾乎所有的操作都是圍繞它們展開的,接下來我們就來逐一認識它們。

State


Vuex的核心倉庫是store,這個store實例會被注入到所有的子組件里面,而所有子組件中公用的數據信息和狀態就都存儲在State里面。我們來看一個簡單的小栗子。

export default new Vuex.Store({
  state: {
    count: 102
  }
})

vuex 中所有的狀態管理都會定義在 state 中,我們在 state中定義了 count ,那么我們就得到了一個集中管理的狀態 count ,所有的組件都可以直接訪問到這個 count

<template>
  <div class="index">
    {{count}}
  </div>
</template>

<script>
  export default {
    computed: {
      count () {
        return this.$store.state.count
      }
    }
  }
</script>

因為根實例中注冊 store 選項,該 store 實例會注入到根組件下的所有子組件中,且子組件能通過 this.$store 訪問到。通過計算屬性,我們就可以在模板里面使用模板語法來調用count了,當然我們也可以在模板中的直接寫入。有小伙伴會好奇為什么不能寫在data里或其它地方嗎,state寫在計算屬性中是因為模板文件需要響應倉庫中state的變化,而state變化之后如果寫在data中就不能及時響應渲染到頁面中,而computed身為計算屬性會監聽函數下面值得變化然后進行重新渲染。

<template>
  <div class="index">
    {{this.$store.state.count}}
  </div>
</template>
  • mapState

如果我們在store中存儲了很多個狀態,而在當前模板頁面中又要讀取store里面大量狀態,那么我們在當前頁面采用computed中來return store里面的狀態這種寫法就會變得很冗余。Vuex中針對state給我們提供了它的語法糖mapState,我們可以通過mapState()方法直接獲取state中存儲的狀態,如下栗子:

<template>
  <div class="index">
    {{count}}
    {{total}}
  </div>
</template>

<script>
  // 使用該語法糖我們需提前引入
  import { mapState } from 'vuex'
  export default {
    computed: {
      total () {
        return 10 + this.count
      },
      ...mapState(['count']) // 如需引入多個狀態可寫成...mapState(['count', 'name', 'six'])
    }
  }
</script>
// 102
// 112

Getter


有時候我們需要從 store 中的 state 中派生出一些狀態,例如如果state中有一個數組,我們要將數組中所有大于10的數字挑選出來然后在給組件使用,栗子如下:

<template>
  <div class="index">
    {{modiyArr}}
  </div>
</template>

<script>
  export default {
    computed: {
      modiyArr () {
        return this.$store.state.list.filter(item => item > 10)
      }
    }
  }
</script>
// [20, 40, 50, 66, 77, 88]

我們在computed中完成了代碼邏輯,同時也暴露出了一些問題,就是如果我們需要在多個組件中都完成這個數組的過濾,就需要在各個組件處都復制這段代碼或者將其抽取到一個共享函數然后在多處導入它,無論哪一種方式都不是很理想。這個時候我們就可以使用vuex中的getter

Vuex 允許我們在 store 中定義“getter”(可以認為是 store 的計算屬性)。就像計算屬性一樣,getter 的返回值會根據它的依賴被緩存起來,且只有當它的依賴值發生了改變才會被重新計算。

<!-- 使用Getter的第一種用法 -->
<template>
  <div class="index">
    {{this.$store.getters.modiyArr}}
  </div>
</template>

<!-- 使用Getter的第二種用法 -->
<template>
  <div class="index">
    {{modiyArr}}
    {{getLength}}
  </div>
</template>

<script>
  export default {
    computed: {
      modiyArr () {
        return this.$store.getters.modiyArr
      },
      getLength () {
        return this.$store.getters.getLength
      }
    }
  }
</script>

// stoer.js
export default new Vuex.Store({
  state: {
    list: [1, 3, 4, 10, 20, 40, 50, 66, 77, 88]
  },
  getters: {
    // getters中的函數接收兩個參數(state, getters)
    modiyArr(state) {
      return state.list.filter(item => item > 10)
    },
    getLength(state, getters) {
        return getters.modiyArr.length // 可以通過getters直接調用它的其它方法
    }
  }
})
// [20, 40, 50, 66, 77, 88] 

上面的栗子基本都是通過屬性來訪問的,使用getters也可以支持通過方法來訪問,我們來看一個官網的小栗子

// 篩選state中的額todos,將todos數組中id為2的對象找出來
// store.js
const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => (id) => {
        return state.todos.find(todo  => todo.id === id)
    }
  }
})

<template>
  <div class="index">
    {{checkDoneTodos}}
  </div>
</template>

<script>
  import { mapState, mapGetter } from 'vuex'
  export default {
    computed: {
      checkDoneTodos () {
          return this.$store.getters.doneTodos(2)
      }
    }
  }
</script>
// { id: 2, text: '...', done: false }

注意,getter 在通過屬性訪問時是作為 Vue 的響應式系統的一部分緩存其中的。getter在通過方法訪問時,每次都會去進行調用,而不會緩存結果。

  • mapGetters

mapGetters也是Vuex幫我們封裝好的語法糖,具體用法其實和mapState差不多。

import { mapGetters } from 'vuex'
export default {
  // ...
  computed: {
  // 使用對象展開運算符將 getter 混入 computed 對象中
    ...mapGetters([
      'modiyArr',
      'getLength',
      // ...
    ])
  }
}

當然...mapGetters本身也是支持對象類寫法的

...mapGetters({
  // 把 `this.editArr` 映射為 `this.$store.getters.modiyArr`
  editArr: 'modiyArr',
  getLength: 'getLength'
})

Mutation


如果我們需要對store.js中的狀態進行修改,我們是不能在組件里面直接修改的,因為組件里面直接修改倉庫是不會進行全局響應的,這就違背了我們使用倉庫的初衷。唯一的方法就是通過提交mutation來進行修改,這樣倉庫中與之對應的狀態也會被修改進而影響全部組件的數據。Vuex 中的 mutation 非常類似于事件:每個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)。這個回調函數就是我們實際進行狀態更改的地方,并且它會接受 state 作為第一個參數:

<template>
  <div class="index">
    <button @click="add">增加</button>
    {{num}}
    <button @click="reduce">減少</button>
  </div>
</template>

<script>
  import { mapState, mapGetter } from 'vuex'
  export default {
    computed: {
      ...mapState(['num'])
    },
    methods: {
      add() {
        this.$store.commit('add')
      },
      reduce () {
        this.$store.commit('reduce')
      }
    }
  }
</script>

export default new Vuex.Store({
  state: {
    num: 10
  },
  mutations: {
    add (state) {
      // 變更狀態
      state.num++
    },
    reduce (state) {
      state.num--
    }
  }
})

上面栗子中,我們在state中定義了num,然后通過mutations添加add方法和reduce方法去對num進行修改,在模板文件中我們定義了click事件來改變num的值,模板文件中的事件去響應store中的mutations主要是通過commit()來實現的。

  • 提交載荷(Payload)

你可以向store.commit傳入額外的參數,即 mutation 的 載荷(payload),其實換種方式理解可能更容易,就是模板文件中的commit可以添加額外的參數,mutations中接收的時候接收一個形參payload,就代表模板文件中你傳進來的參數。vuex官網更建議我們payload應該是一個對象,這樣可以包含多個字段并且記錄的 mutation 會更易讀,我們看下面的小栗子:

export default {
  methods: {
    add() {
      //this.$store.commit('add', 100)直接傳遞額外參數,不建議,更建議下一種對象的寫法
      this.$store.commit('add', {
        addNum: 100
      })
    }
  }
}

export default new Vuex.Store({
  state: {
    num: 10
  },
  mutations: {
    add (state, payload) {
      state.num+=payload.addNum
    },
    reduce (state) {
      state.num--
    }
  }
})

也可以將所有參數寫到一個對象里面,那么type就對應mutations中要執行的函數名

export default {
  methods: {
    add() {
      this.$store.commit({
        type: 'add',
        addNum: 100
      })
    }
  }
}
  • Mutation 需遵守 Vue 的響應規則

這個通俗點說就是你在開發過程中需要向state里面添加額外數據時,需要遵循響應準則。官方文檔說既然 Vuexstore 中的狀態是響應式的,那么當我們變更狀態時,監視狀態的 Vue 組件也會自動更新。這也意味著 Vuex 中的 mutation 也需要與使用Vue一樣遵守一些注意事項: 1.最好提前在你的 store 中初始化好所有所需屬性。 2.當需要在對象上添加新屬性時,你應該使用Vue.set(obj, 'newProp', 123),或者以新對象替換老對象。例如,利用對象展開運算符我們可以這樣寫:

state.obj = { ...state.obj, newProp: 123 }

文字很枯燥,我們還是來看個小栗子

<template>
  <div class="index">
    <button @click="add">增加</button>
    {{num}}
    // add方法執行之后頁面會立即響應這個新的狀態
    <div class="new-num">{{this.$store.state.newNum || 0}}</div> 
  </div>
</template>

mutations: {
  add (state, payload) {
    state.num+=payload.addNum
    Vue.set(state, 'newNum', 1000) // 像倉庫中的state新增加一個newNum狀態,初始值為1000
    // state= {...state, newNum: 2000} 這個方法不管用了,用下面的replaceState()方法
    this.replaceState({...state, newNum: 2000})
  }
}
  • Mutation 必須是同步函數

下面這種寫法必須避免(直接官方例子加持):

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

我們在模板文件中通過事件去操作mutations時,如果mutations中為異步函數,那么當 mutation 觸發的時候,回調函數還沒有被調用,因為我們不知道什么時候回調函數實際上被調用——實質上任何在回調函數中進行的狀態的改變都是不可追蹤的。

  • mapMutations

其實這幾個語法糖的使用方法都差不多,這里就直接上官方栗子了。

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 可以包含任意異步操作。前面說過mutation只能包含同步事務,所以在處理異步事務就需要Action,通過Action控制了異步這一過程,之后再去調用mutation里面的方法來改變狀態。

先看官方的一個小栗子來認識它的基礎語法:

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.statecontext.getters 來獲取 stategetters。那為什么這里的參數不能直接是store本身呢,這就和Modules有關了,了解Modules之后就會發現store總倉庫下面可能會有很多個不同的模塊倉庫,而每一個不同的模塊倉庫都有自己的Actionstatemutationgetter,如果使用store那么store.state拿到的就不會是當前模塊的state,而context可以理解為當前模塊的store,這樣就不會引起沖突。

實踐中,我們會經常用到 ES2015 的 參數解構 來簡化代碼(特別是我們需要調用 commit 很多次的時候):

actions: {
  increment ({ commit }) { // 這里就是將context展開式寫法 { commit } = context.commit
    commit('increment')
  }
}
  • 分發 Action

Action 通過 store.dispatch 方法觸發:

store.dispatch('increment')

乍一眼看上去感覺多此一舉,我們直接分發 mutation 豈不更方便?實際上并非如此,還記得 mutation 必須同步執行這個限制么?Action 就不受約束!我們可以在 Action 內部執行異步操作。

上面這一塊內容基本來源于官網,感覺官網對于 Action 函數的傳參,分發等基本用法都說的比較詳細,在這里我們只要記住一點Action用來執行異步操作倉庫狀態的情況。我們還是來做個小栗子更直觀了解吧

<template>
  <div class="index">
    <button @click="add">增加</button>
    {{num}}
  </div>
</template>

<script>
  import { mapState, mapGetter } from 'vuex'
  export default {
    computed: {
      ...mapState(['num'])
    },
    methods: {
      add() {
        this.$store.dispatch('delayAdd')
      }
    }
  }
</script>

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    num: 10
  },
  mutations: {
    add (state, addNum) {
      state.num += addNum
    }
  },
  actions: {
    delayAdd (context) {
      setTimeout(() => {
        context.commit('add', 100)
      }, 1000)
    }
  }
})

上面是一個Action中最基礎的用法,因為Mutation中不能接受異步操作,所以我們先將要修改的東西放在Mutation中,然后在Action中等待1s之后去操作Mutation進行提交來更新數據。至于分發Action是我們在模板中通過 this.$store.dispatch('delayAdd')來執行,這樣我們就完成了一個基礎的Action閉環demo

  • Actions 支持同樣的載荷方式和對象方式進行分發:
// 以載荷形式分發
store.dispatch('delayAdd', {
  addNum: 100
})

actions: {
  delayAdd (context, payload) {
    setTimeout(() => {
      context.commit('add', payload.addNum)
    }, 1000)
  }
}

// 以對象形式分發
store.dispatch({
  type: 'delayAdd',
  addNum: 100
})

這里copy一個官方購物車示例,涉及到調用異步 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 產生的副作用(即狀態變更)。

  • mapActions

Action也有類似的語法糖,這里就不多贅述了,其實用法都是差不多的,直接官方栗子

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結合Promise

Action 通常是異步的,那么如何知道 Action 什么時候結束呢?我們這里假設一種情況,模板上有一個數據,同時有這個數據當前的狀態說明,我們如果通過Action去操作這個數據,那么數據改變之后我們希望這個數據的當前狀態說明就馬上更新。舉個小栗子

<template>
  <div class="index">
    <div class="index-name">姓名:{{userinfo.name}}</div>
    <div class="index-six">性別:{{userinfo.six}}</div>
    <div class="number-hint">狀態:{{defalutText}}</div>
    <div class="click-btn" @click.stop="handleClickChange">
      <button>{{buttonText}}</button>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetter, mapAction } from "vuex";
export default {
  data () {
    return {
      defalutText: "信息暫未修改" ,
      buttonText: "點擊改變信息",
      time: 5
    }
  },
  computed: {
    ...mapState(["userinfo"]),
  },
  methods: {
    handleClickChange () {
      this.changeButton()
      this.$store.dispatch({
        type: 'changeInfo',
        name: '李四',
        six: '女',
        time: this.time
      }).then(res => {
        console.log(res) // 執行完畢,可以開始改變狀態啦
        this.defalutText = '信息更新成功'
      })
    },
    changeButton () {
      this.buttonText = this.time + 's后信息將被改變'
      let t = setInterval(() => {
        this.time--
        if (this.time != 0) {
          this.buttonText = this.time + 's后信息將被改變'
        } else {
            this.buttonText = '成功改變啦'
            clearInterval(t)
        }
      }, 1000)
    },
  },
};
</script>

// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    userinfo: {
      name: '張三',
      six: '男'
    }
  },
  mutations: {
    changeInfo (state, payload) {
      state.userinfo.name = payload.name
      state.userinfo.six = payload.six
    }
  },
  actions: {
    // 這里直接用參數解構的寫法
    changeInfo ({commit, state}, payload) { // 接收context(解構寫法)和載荷payload
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          commit('changeInfo', {
            name: payload.name,
            six: payload.six
          })
          resolve("執行完畢,可以開始改變狀態啦") // 成功之后即可直接在then的回調中接收
        }, payload.time * 1000)
      })
    }
  }
})

上面的代碼基本就是先展示state中存好的userinfo信息,點擊按鈕之后會有一個time的倒計時來告訴你幾秒之后改變數據,改變之后再將文字改成"成功改變啦"。

其實上面這段代碼是有小瑕疵的,因為我們在分發actionchangeInfo函數中給的是一個定時器,沒有任何延遲信息,所以5s之后返回來的參數和我們在changeButton()中寫好的改變button狀態的文字能正好對應上。而日常開發中通常這里會寫請求函數到后端然后等待返回值,這個過程如果耽誤1s那么們changeButton()中就會出現問題,它會優先執行this.buttonText = '成功改變啦'

而實際因為請求有時間延遲,可能多出1s就會出現文字改變而上面的信息并未更新。這里特意留給我們自己思考,如何完善這個小bug,其實答案很簡單,就是在then()的回調中去處理這段邏輯,具體的實現可以自行去了解哦,對Promise不太了解的可以點擊了解Promise

  • 組合Action
利用Promise來實現組和Action

我們先直接上官網的demo小栗子,通過栗子看更明白

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

其實上面的這個小栗子在我們上一塊 Action結合Promise 中就說的很明白了,它就是想告訴我們通過Promise.resolve()之后我們就知道這個時候actionA就執行完畢了,我們就可以通過鏈式調用的then()方法來走下一個流程啦。看官方給的actionB

actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
}

vue模板中調用

store.dispatch('actionA').then((res) => {
  // ... 這里執行actionA中異步結束之后的程序,可以接受res為actionA中resolve("將返回結果返回出去")
})
利用 async / await來實現組和Action

asyncawait是ES7中推出的新語法,直接看官方的栗子,簡單明了

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}
一個 store.dispatch 在不同模塊中可以觸發多個 action 函數。在這種情況下,只有當所有觸發函數完成后,返回的 Promise 才會執行。

Module


由于使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象。當應用變得非常復雜時,store 對象就有可能變得相當臃腫。

為了解決以上問題,Vuex 允許我們將 store 分割成模塊(module)。每個模塊擁有自己的statemutationactiongetter、甚至是嵌套子模塊——從上至下進行同樣方式的分割,我們先來做個簡單的栗子,這里我們就不直接在index.js里面新建module了,因為實際開發中我們也是將模塊單獨拎出來,這樣更有利于代碼維護。如下栗子:

// moduleA.js
const moduleA = {
  state: {
    userInfo: {
      name: 'alen',
      age: 20
    }
  },
  mutations: { ... },
  getters: { ... },
  actions: { ... }
}
export default moduleA

// moduleB.js
const moduleB = {
  state: {
    userInfo: {
      name: 'nemo',
      age: 32
    }
  },
  mutations: { ... },
  getters: { ... },
  actions: { ... }
}
export default moduleB

store倉庫的跟文件index.js中引入模塊A和模塊B

import Vue from 'vue'
import Vuex from 'vuex'
import moduleA from './modules/moduleA'
import moduleB from './modules/moduleB'

Vue.use(Vuex)

export default new Vuex.Store({
  state: { ... },
  modules: {
    ma: moduleA,
    mb: moduleB
  }
})

上面的代碼就讓我們輕松完成了模塊的新建和引入,如果我們想在組件里訪問模塊A中的name

<template>
  <div class="index">
    <div class="modulea-name">{{this.$store.state.ma.userInfo.name}}</div> // alen
    <div class="moduleb-name">{{this.$store.state.mb.userInfo.name}}</div> // nemo
  </div>
</template>

上面的代碼我們不難猜出,起始mambstate都是掛載在根節點的state下面。所以我們在組件中直接訪問模塊中的state需要先去訪問根節點的state然后在加上模塊名及對應的參數。

  • 模塊的局部狀態

對于模塊內部的 mutationgetter,接收的第一個參數是模塊的局部狀態對象。直接官方栗子加持:

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: {
    // 這里用的解構寫法,實際上是context.state, context.commit, context.rootState
    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
    }
  }
}

這里我們直接舉一個組件操作模塊B進行mutation提交的小栗子

<template>
  <div class="index">
    <div class="moduleb-name">{{this.$store.state.mb.userInfo.name}}</div>
    <div class="moduleb-age">{{this.$store.state.mb.userInfo.age}}</div>
    <button @click="handleClickChangeAge(60)">修改年齡</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from "vuex";
export default {
  methods: {
    handleClickChangeAge(newAge) {
      this.$store.commit({
        type: 'editAge',
        age: newAge
      })
    }
  }
}
</script>

// moduleB.js
const moduleB = {
  state: {
    userInfo: {
      name: 'nemo',
      age: 32,
      hobby: ['football', 'basketball', 'badminton', 'volleyball']
    }
  },
  mutations: {
    editAge (state, payload) {
      state.userInfo.age = payload.age
    }
  }
}

export default moduleB

看上面的代碼會發現我們通過 commit 提交模塊B里面的內容并沒有使用 mb 這個模塊名,而是直接全局提交就能進行修改,我們再來看看 getters 是不是也是直接可以全局提交修改。

<template>
  <div class="index">
    <div class="moduleb-hobby">
      <div v-for="(item, index) in hobby" :key="index">{{item}}</div>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from "vuex";
export default {ddd
  computed: {
    modeulaName () {
      console.log(this.$store.state.ma.userInfo.name)
    },
    filteHobby () {
      console.log(this.$store.getters.filteHobby)
      return this.$store.getters.filteHobby
    }
    // 使用語法糖來寫
    // ...mapGetters({
      // hobby: 'filteHobby'
    // })
  }
};
</script>

const moduleB = {
  state: {
    userInfo: {
      name: 'nemo',
      age: 32,
      hobby: ['football', 'basketball', 'badminton', 'volleyball']
    }
  },
  getters: {
    filteHobby (state, getters, rootState) {
      return state.userInfo.hobby.filter(item => item != 'football')
    }
  }
}
export default moduleB

果然 getters 也可以直接通過 this.$store.getters 來操作,而不需要再加上它所在的模塊名來進行調用,這里我們來看看官方是這樣說的:

默認情況下,模塊內部的 actionmutationgetter 是注冊在全局命名空間的——這樣使得多個模塊能夠對同一 mutationaction 作出響應。
  • 命名空間

如果希望你的模塊具有更高的封裝度和復用性,你可以通過添加 namespaced: true 的方式使其成為帶命名空間的模塊。當模塊被注冊后,它的所有 getteractionmutation 都會自動根據模塊注冊的路徑調整命名。直接來看小栗子:

// 新建模塊C
const moduleC = {
  namespaced: true,
  state: {
    userInfo: {
      name: 'kity',
      age: 10,
      list: [1, 2, 3, 4]
    }
  },
  getters: {
    filterList (state) {
      return state.userInfo.list.filter(item => item != 1)
    }
  },
  mutations: {
    changeAge (state, payload) {
      state.userInfo.age = payload.age
    }
  },
  actions: {...}
}
export default moduleC

// index.js中導入
import moduleC from './modules/moduleC'
export default new Vuex.Store({
  modules: {
    mc: moduleC
  }
})

// 模板文件
<template>
  <div class="index">
    <div class="modulec-name">{{this.$store.state.mc.userInfo.name}}</div>
    <div class="modulec-age">{{this.$store.state.mc.userInfo.age}}</div>
    <div class="modulec-list">
      <div v-for="(item, index) in list" :key="index">{{item}}</div>
    </div>
    <button @click="handleClickChangeAge(20)">修改年齡</button>
  </div>
</template>

通過 commit 提交 moduleC 中的 changeAge() ,這時候的寫法如下:

<script>
import { mapState, mapGetters, mapActions } from "vuex";
export default {
  methods: {
    handleClickChangeAge(newAge) {
      this.$store.commit('mc/changeAge',{
        age: newAge
      })
      this.$store.commit({
        type: 'mc/changeAge',
        age: newAge
      })
    }
  }
};
</script>

通過 getters 獲取 moduleC 中的 filterList(),一種是返回值直接通過 this.$store 去寫,一種是語法糖的寫法

<script>
import { mapState, mapGetters, mapActions } from "vuex";
export default {
  computed: {
    list () {
      return this.$store.getters['mc/filterList']
    },
    // ...mapGetters({
    //   list: 'mc/filterList'
    // })
  }
};
</script>

當然模塊里面再嵌套模塊也可以,路徑要不要多走一層主要看你的 namespaced: true 有沒有聲明,這里貼一下官方的代碼:

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']
          }
        }
      }
    }
  }
})

啟用了命名空間的 getteraction 會收到局部化的 getterdispatchcommit。換言之,你在使用模塊內容(module assets)時不需要在同一模塊內額外添加空間名前綴。更改 namespaced 屬性后不需要修改模塊內的代碼。

  • 在帶命名空間的模塊內訪問全局內容

如果你希望使用全局 stategetterrootStaterootGetters 會作為第三和第四參數傳入 getter,也會通過 context 對象的屬性傳入 action

若需要在全局命名空間內分發 action 或提交 mutation,將 { root: true } 作為第三參數傳給 dispatchcommit 即可。這里直接官方栗子加持

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') // 通過dispatch直接調用不需要加上命名空間
    }
  },
  modules: {
    foo: {
      namespaced: true,

      actions: {
        someAction: { // 全局的action
          root: true,
          handler (namespacedContext, payload) { ... } // -> 'someAction'
        }
      }
    }
  }
}
  • 在模塊里面使用輔助函數mapState、mapGetters、mapMutations和mapActions

由于存在命名空間,在組件里面采用上面的寫法會出現問題,這里要想使用輔助函數來映射模塊里面的東西需要指定空間名稱來告訴輔助函數應該去哪兒找這些。 這兒我以上面我的C模塊為例,首先對于 mapSatate 函數可以這樣玩,我在全局的 modules 里面聲明了 mc,那我的空間名稱就是 mc

computed: {
  ...mapState('mc', ['name', 'desc']) // 這里模塊里面要使用輔助函數的話要多傳一個參數才行
}

然后在模版里面寫 namedesc 即可,或者可以這樣:

computed: {
  ...mapState('mc', {
    name(state) {
      return state.name;
    },
    desc(state) {
      return state.desc;
    }
  })
}

mapActionsmapMutationsmapGetter都可以向上面一樣類似寫法,這里我們寫一個mapMutations的栗子參考

<script>
import { mapState, mapGetters, mapActions, mapMutations } from "vuex";
export default {
  methods: {
    handleClickChangeAge(newAge) {
      // 通過commit提交的寫法
      // this.$store.commit({
      //   type: 'mc/changeAge',
      //   age: newAge
      // })
      // 使用語法糖的寫法
      this.changeAge({
        age: newAge
      })
      // 當...mapMutations中第二個參數使用對象寫法時,this后面接的函數名應該是該對象的鍵
      this.editAge({
        age: newAge
      })
    },
    // 語法糖中第一個參數是對應的路徑,第二個參數為數組時的寫法
    ...mapMutations('mc', ['changeAge'])
    // 第二個參數為對象時的寫法
    ...mapMutations('mc', {
      editAge: 'changeAge' // 特意區分了鍵和值,值代表mutations中的函數,鍵代表了模板中this調用的函數
    })
  }
};
</script>

如果你確實不想在每個輔助函數里寫空間名稱,Vuex 也提供了其它辦法,使用createNamespacedHelpers 創建基于某個命名空間輔助函數,它返回一個對象,對象里有新的綁定在給定命名空間值上的組件綁定輔助函數:

import { createNamespacedHelpers } from 'vuex';
const { mapState, mapMutations } = createNamespacedHelpers('mc');

這樣你在寫輔助函數的時候就不需要單獨指定空間名稱了。 其它類似,就不再贅述了!其實 vuex 官網中對于 Module 這個板塊還有幾個知識點,只是對于了解基礎的話過多的深入可能還會影響自己的消化進度,如果當我們做一個項目龐大到需要建立很多個模塊,然后模塊中又進行嵌入,那么相信我們對 vuex 已經基本都了解了,到時候再去查閱相關的進階資料也很容易理解。

結語


vuex 的幾個核心概念的基本認識就都在這里了,本文也主要是參考了官網的文檔進行歸納總結。當然本篇相當于基礎入門篇,實際開發中使用 vuex 肯定遠遠比這個復雜,但是萬丈高樓平地起,希望對大家有所幫助,至于其他進階內容大家有興趣進官網瀏覽,也可查閱相關的資料進行學習。如果文章有理解不到位的地方,還請各位多批評指正!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375