想了解Vuex?一定先把這篇筆記碼住!
本文轉載自微信公眾號「新鈦云服」,作者林泓輝。轉載本文請聯系新鈦云服公眾號。
一、Vuex是干什么用的?
Vuex是用于對復雜應用進行狀態管理用的(官方說法是它是一種狀態管理模式)。
“殺雞不用宰牛刀”。對于簡單的項目,根本用不著Vuex這把“宰牛刀”。那簡單的項目用什么呢?用Vue.js官方提供的“事件總線”就可以了。
二、我們import進來的Vuex對象都包含些什么呢?
我們使用Vuex的時候怎么用呢?通常都是這樣:
- import Vue from 'vue';
- import Vuex from 'vuex';
- Vue.use(Vuex);
- new Vuex.Store({
- state: { //放置state的值
- count: 0,
- str:"abcd234"
- },
- getters: { //放置getters方法
- strLen: state => state.str.length
- },
- // mutations只能是同步操作
- mutations: { //放置mutations方法
- increment(state, payload) {
- //在這里改變state中的數據
- state.count = payload.number;
- }
- },
- // actions可以是異步操作
- actions: { //放置actions方法
- actionName({ commit }) {
- //dosomething
- commit('mutationName')
- },
- getSong ({commit}, id) {
- api.getMusicUrlResource(id).then(res => {
- let url = res.data.data[0].url;
- })
- .catch((error) => { // 錯誤處理
- console.log(error);
- });
- }
- }
- });
- new Vue({
- el: '#app',
- store,
- ...
- });
這里import進來的Vuex是個什么東西呢?我們用console.log把它輸出一下:
- console.log(Vuex)
通過輸出,我們發現其結構如下:
可見,import進來的Vuex它實際上是一個對象,里面包含了Store這一構造函數,還有幾個mapActions、mapGetters、mapMutations、mapState這幾個輔助方法(后面再講)。
除此之外,還有一個install方法。我們發現,import之后要對其進行Vue.use(Vuex)的操作。根據這兩個線索,我們就明白了,Vuex本質上就是一個Vue.js的插件。
三、創建好的store實例怎么在各個組件中都能引用到?
Vuex 通過 store 選項,提供了一種機制將狀態從根組件“注入”到每一個子組件中(需調用 Vue.use(Vuex)):
- const app = new Vue({
- el: '#app',
- // 把 store 對象提供給 “store” 選項,這可以把 store 的實例注入所有的子組件
- store,
- components: { Counter },
- template: `
- <div class="app">
- <counter></counter>
- </div>
- `
- })
通過在根實例中注冊 store 選項,該 store實例會注入到根組件下的所有子組件中,且子組件能通過 this.$store 訪問到。
四、Vuex中的幾大核心概念
1. State
這個很好理解,就是狀態數據。Vuex所管理的就是狀態,其它的如Actions、Mutations都是來輔助實現對狀態的管理的。Vue組件要有所變化,也是直接受到State的驅動來變化的。
可以通過this.$store.state來直接獲取狀態,也可以利用vuex提供的mapState輔助函數將state映射到計算屬性computed中去。
2. Getters
Getters本質上是用來對狀態進行加工處理。Getters與State的關系,就像Vue.js的computed與data的關系。 getter的返回值會根據它的依賴被緩存起來,且只有當它的依賴值發生了改變才會被重新計算。
可以通過this.$store.getters.valueName對派生出來的狀態進行訪問。或者直接使用輔助函數mapGetters將其映射到本地計算屬性中去。
3. Mutations
更改 Vuex的 store中的狀態的唯一方法是提交 mutation。Vuex中的 mutation非常類似于事件:每個 mutation都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)。這個回調函數就是我們實際進行狀態更改的地方。
你不能直接調用一個 mutation handler。這個選項更像是事件注冊:“當觸發一個類型為 increment 的 mutation時,調用此函數。”要喚醒一個mutation handler,你需要以相應的 type 調用store.commit方法,并且它會接受 state 作為第一個參數,也可以向 store.commit 傳入額外的參數,即 mutation 的 載荷(payload):
- const store = new Vuex.Store({
- state: {
- count: 1
- },
- mutations: {
- increment (state) {
- // 變更狀態
- state.count++
- }
- }
- })
- store.commit('increment')
- // ...
- 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
- })
- // ***或者以對象風格提交***
- mutations: {
- increment (state, payload) {
- state.count += payload.amount
- }
- }
- store.commit({
- type: 'increment',
- amount: 10
- })
Mutation需遵守 Vue的響應規則
既然 Vuex的 store中的狀態是響應式的,那么當我們變更狀態時,監視狀態的 Vue組件也會自動更新。這也意味著 Vuex中的 mutation也需要與使用 Vue一樣遵守一些注意事項:
- 最好提前在你的 store中初始化好所有所需屬性。
- 當需要在對象上添加新屬性時,你應該:
- 使用 Vue.set(obj, 'newProp', 123), 或者
- 以新對象替換老對象。例如,利用對象展開運算符 我們可以這樣寫:
- state.obj = { ...state.obj, newProp: 123 }
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映射為 this.$store.commit。例如:
- 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')`
- })
- }
- }
經過這樣的映射之后,就可以通過調用方法的方式來觸發其對應的(所映射到的)mutation commit了,比如,上例中調用add()方法,就相當于執行了this.$store.commit('increment')了。
4. Actions
Action類似于 mutation,不同在于:
- Action提交的是 mutation,而不是直接變更狀態。
- Action可以包含任意異步操作。
Action函數接受一個與 store實例具有相同方法和屬性的 context對象,因此你可以調用 context.commit 提交一個 mutation,或者通過 context.state 和 context.getters 來獲取 state和 getters。
分發 Action
Action 通過 store.dispatch 方法觸發:
- store.dispatch('increment')
Actions 支持同樣的載荷方式和對象方式進行分發:
- // 以載荷形式分發
- store.dispatch('incrementAsync', {
- amount: 10
- })
- // 以對象形式分發
- store.dispatch({
- type: 'incrementAsync',
- amount: 10
- })
另外,你需要知道, this.$store.dispatch 可以處理被觸發的 action 的處理函數返回的 Promise,并且 this.$store.dispatch 仍舊返回 Promise。
- actions: {
- actionA ({ commit }) {
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- commit('someMutation')
- resolve()
- }, 1000)
- })
- }
- }
- store.dispatch('actionA').then(() => {
- // ...
- })
5. Module
Module是什么概念呢?它實際上是對于store的一種切割。由于Vuex使用的是單一狀態樹,這樣整個應用的所有狀態都會集中到一個比較大的對象上面,那么,當應用變得非常復雜時,store對象就很可能變得相當臃腫!Vuex允許我們將 store分割成一個個的模塊(module)。每個模塊擁有自己的 state、mutation、action、getter、甚至是嵌套子模塊——從上至下進行同樣方式的分割。
(1)模塊的局部狀態
對于每個模塊內部的 mutation 和 getter,接收的第一個參數就是模塊的局部狀態對象,對于模塊內部的 getter,根節點狀態會作為第三個參數暴露出來。同樣,對于模塊內部的 action,局部狀態通過 context.state 暴露出來,根節點狀態則為 context.rootState:
- const moduleA = {
- state: () => ({
- count: 0
- }),
- mutations: {
- increment (state) {
- // 這里的 `state` 對象是模塊的局部狀態
- state.count++
- }
- },
- getters: {
- doubleCount (state) {
- return state.count * 2
- },
- sumWithRootCount (state, getters, rootState) {
- return state.count + rootState.count
- }
- },
- actions: {
- incrementIfOddOnRootSum ({ state, commit, rootState }) {
- if ((state.count + rootState.count) % 2 === 1) {
- commit('increment')
- }
- }
- }
- }
(2)命名空間
默認情況下,模塊內部的 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 屬性后不需要修改模塊內的代碼。
(3)在帶命名空間的模塊內訪問全局內容(Global Assets)
如果你希望使用全局 state和 getter,rootState 和 rootGetters 會作為第三和第四參數傳入 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) { ... }
- }
- }
- }
(4)在帶命名空間的模塊注冊全局 action
若需要在帶命名空間的模塊注冊全局 action,你可添加 root: true,并將這個 action 的定義放在函數 handler 中。例如:
- {
- actions: {
- someOtherAction ({dispatch}) {
- dispatch('someAction')
- }
- },
- modules: {
- foo: {
- namespaced: true,
- actions: {
- someAction: {
- root: true,
- handler (namespacedContext, payload) { ... } // -> 'someAction'
- }
- }
- }
- }
- }
(5)帶命名空間綁定函數
當使用 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()
- ])
- }
(6)模塊動態注冊
在 store創建之后,你可以使用 store.registerModule 方法注冊模塊:
- import Vuex from 'vuex'
- const store = new Vuex.Store({ /* 選項 */ })
- // 注冊模塊 `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時聲明的模塊)。
注意,你可以通過 store.hasModule(moduleName) 方法檢查該模塊是否已經被注冊到 store。
保留 state
在注冊一個新 module時,你很有可能想保留過去的 state,例如從一個服務端渲染的應用保留 state。你可以通過 preserveState 選項將其歸檔:store.registerModule('a', module, { preserveState: true })。
當你設置 preserveState: true 時,該模塊會被注冊,action、mutation和 getter會被添加到 store中,但是 state不會。這里假設 store的 state已經包含了這個 module的 state并且你不希望將其覆寫。
(7)模塊重用
有時我們可能需要創建一個模塊的多個實例,例如:
- 創建多個 store,他們共用同一個模塊 (例如當 runInNewContext 選項是 false 或 'once' 時,為了在服務端渲染中避免有狀態的單例 (opens new window))
- 在一個 store中多次注冊同一個模塊
如果我們使用一個純對象來聲明模塊的狀態,那么這個狀態對象會通過引用被共享,導致狀態對象被修改時 store 或模塊間數據互相污染的問題。
實際上這和 Vue組件內的 data 是同樣的問題。因此解決辦法也是相同的——使用一個函數來聲明模塊狀態(僅 2.3.0+ 支持):
- const MyReusableModule = {
- state: () => ({
- foo: 'bar'
- }),
- // mutation, action 和 getter 等等...
- }
五、Vuex中的表單處理
當在嚴格模式中使用 Vuex時,在屬于 Vuex的 state上使用 v-model 會比較棘手:
- <input v-model="obj.message">
假設這里的 obj 是在計算屬性中返回的一個屬于 Vuex store 的對象,在用戶輸入時,v-model 會試圖直接修改 obj.message。在嚴格模式中,由于這個修改不是在 mutation 函數中執行的, 這里會拋出一個錯誤。
用“Vuex 的思維”去解決這個問題的方法是:給 <input> 中綁定 value,然后偵聽 input 或者 change 事件,在事件回調中調用一個方法:
- <input :value="message" @input="updateMessage">
- computed: {
- ...mapState({
- message: state => state.obj.message
- })
- },
- methods: {
- updateMessage (e) {
- this.$store.commit('updateMessage', e.target.value)
- }
- }
下面是 mutation函數:
- // ...
- mutations: {
- updateMessage (state, message) {
- state.obj.message = message
- }
- }
必須承認,這樣做比簡單地使用“v-model + 局部狀態”要啰嗦得多,并且也損失了一些 v-model 中很有用的特性。另一個方法是使用帶有 setter 的雙向綁定計算屬性:
- <input v-model="message">
- computed: {
- message: {
- get () {
- return this.$store.state.obj.message
- },
- set (value) {
- this.$store.commit('updateMessage', value)
- }
- }
- }