vuex 基本入門和使用(三)-關於 mutation

edwin020020020發表於2018-01-17

vuex 版本為^2.3.1,按照我自己的理解來整理vuex。

關於 mutation

這裡應該很好理解。

更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation。Vuex 中的 mutation 非常類似於事件:每個 mutation 都有一個字串的 事件型別 (type) 和 一個 回撥函式 (handler)。這個回撥函式就是我們實際進行狀態更改的地方,並且它會接受 state 作為第一個引數

const store = new Vuex.Store({
  state: { // 類似 vue 的 data
    count: 1
  },
  mutations: { // 類似 vue 的 methods
    increment (state) { // 這是一個回撥函式
      // 變更狀態
      state.count++
    }
  }
})
複製程式碼

你不能直接呼叫一個 mutation handler。這個選項更像是事件註冊:“當觸發一個型別為 increment 的 mutation 時,呼叫此函式。”要喚醒一個 mutation handler,你需要以相應的 type 呼叫store.commit 方法:

// 相當於就是一個特殊的呼叫事件方式來呼叫
store.commit('increment')
複製程式碼

提交載荷(Payload)

可以向 store.commit 傳入額外的引數,即 mutation 的 載荷(payload)

mutations: {
 // 第一個引數是 state,第二個引數叫額外的引數,這裡是n
  increment (state, n) {
    state.count += n
  }
}
// 回撥函式 increment 和引數10,後者是作為額外引數傳入,n 就是10
store.commit('increment', 10)
複製程式碼

在大多數情況下,載荷應該是一個物件,這樣可以包含多個欄位並且記錄的 mutation 會更易讀:

mutations: {
  increment (state, payload) {
	  // payload 作為一個物件,更加可讀,統一物件形式呼叫
    state.count += payload.amount
  }
}
// 傳入的是物件(即將額外的 mutation 引數以物件的方式傳入)
store.commit('increment', {
  amount: 10
})
複製程式碼

這裡總的來說就是說 mutations 可以傳引數,並且引數最好以物件的方式來傳。

物件風格的提交方式

提交 mutation 的另一種方式是直接使用包含 type 屬性的物件:

// 這裡也是傳入一個物件,不過這個物件包含了 type 屬性
store.commit({
  type: 'increment',
  amount: 10
})
複製程式碼

這裡只是一種提交 mutations 的方式,不必深究。

當使用這種物件風格的提交方式,整個物件都作為載荷傳給 mutation 函式,因此 handler 保持不變:

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
// vuex 會將這個物件分解,除了 type 之外的,依然會是作為額外引數傳入
store.commit({
  type: 'increment',
  amount: 10
})
複製程式碼

將整個物件傳給 mutation後,vuex 會根據 type 引數識別到這是一個mutation 的載荷引數,然後自動填充 state 引數為第一位,第二位引數為傳入的這個物件的第二位引數。

這是 jsrun 的 demo 例子:jsrun.net/VvqKp

例子裡面會變成加11 !

Mutation 需遵守 Vue 的響應規則

既然 Vuex 的 store 中的狀態是響應式的,那麼當我們變更狀態時,監視狀態的 Vue 元件也會自動更新。這也意味著 Vuex 中的 mutation 也需要與使用 Vue 一樣遵守一些注意事項:

  • 最好提前在你的 store 中初始化好所有所需屬性。
  • 當需要在物件上新增新屬性時,你應該
    • 使用 Vue.set(obj, 'newProp', 123) (沿用 vue 的方式)
    • 以新物件替換老物件。例如,利用 stage-3 的物件展開運算子我們可以這樣寫:state.obj = { ...state.obj, newProp: 123 }(先用擴充套件符號解構物件,然後賦值到新物件,因為物件在 js 裡面是引用型別。)

使用常量替代 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
    }
  }
})
複製程式碼

備註:es2015的計算屬性名會使用中括號進行命名,中括號的方式允許我們使用變數或者在使用識別符號時會導致語法錯誤的字串直接量來定義屬性,例如person["first name"],僅此而已。

我覺得,及早適應這種寫法比較好,既能裝逼又可以學到別人的高階技能。

Mutation 必須是同步函式

一條重要的原則就是要記住 mutation 必須是同步函式。

實質上任何在回撥函式中進行的的狀態的改變都是不可追蹤的。所以需要在 actions 裡面進行非同步封裝 mutation 來實現非同步。

在元件中提交 Mutation

你可以在元件中使用 this.$store.commit('xxx')提交 mutation,或者使用 mapMutations 輔助函式將元件中的 methods 對映為 store.commit呼叫(需要在根節點注入 store)。

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    // mapMutations 工具函式會將 store 中的 commit 方法對映到元件的 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')`
    })
  }
}
複製程式碼
  • ...es2015的擴充套件運算子,能夠解構陣列或者物件,這裡是解構mapMutations物件。
  • mapMutations的寫法和引數:mapMutations(namespace?: string, map: Array<string> | Object): Object(官網 api 文件的格式)
    • 第一個引數是模組的空間名稱字串,可以不填
    • 第二個引數是一個 map結構的物件,也可以是字串陣列
    • 返回的是一個物件
    • 瞭解更多 jsdoc 的格式標註,請參考:Use JSDoc: @type

關於...mapMutations

首先:normalizeMap會將mutations格式化為一個陣列:

function normalizeMap (map) {
  // 判斷是否陣列,並且最終返回也是一個陣列
  return Array.isArray(map)
    // 是陣列就直接 map 迴圈
    ? map.map(key => ({ key, val: key }))
    // 是物件就將 key拿出來,然後再進行 map 迴圈
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}
複製程式碼

例如傳入的mutations 是一個陣列,如下:

// 轉換前
[
      // 這是沒額外引數的(沒載荷)
      'increment',
      // 這是有額外引數的(有載荷)
      'incrementBy' 
]
// 那麼被normalizeMap轉換後:
// 即轉換為{ key, val: key })
[
    { 
     key, // key 是increment
     val: key // val是increment
    },
    // 這裡雖然說有額外引數傳入,但是這個引數並沒有在轉換中處理
    { 
     key, // key 是incrementBy
     val: key // val是incrementBy
    },    
    //.....
]
複製程式碼

例如傳入的mutations 是一個物件,如下:

// 轉換前
{
      addAlias: function(commit, playload) {
           commit('increment') 
           commit('increment', playload)
      } 
}
// 那麼被normalizeMap轉換後:
// 即轉換為{ key, val: key })
{ 
    key, //  key 是addAlias
    val: map[key] // val 是物件的 key 屬性的值,就是 function().....
}
複製程式碼

然後看回去 vuex 的原始碼關於mapMutations的部分:

// 參考 vuex 的原始碼
var mapMutations = normalizeNamespace(function (namespace, mutations) {
  var res = {};
  // 被normalizeMap格式化後的mutations被 foreach 迴圈
  normalizeMap(mutations).forEach(function (ref) {
    var key = ref.key;
    var val = ref.val;

    res[key] = function mappedMutation () {
      // 拷貝載荷:複製額外引數到 args 陣列
      var args = [], len = arguments.length;
      while ( len-- ) args[ len ] = arguments[ len ];
        
      var commit = this.$store.commit;
      // 先不管名稱空間
      //......
      return typeof val === 'function'
        // 是函式,則直接執行該函式,並將comit作為其第一個引數,arg仍然作為後續引數。
        ? val.apply(this, [commit].concat(args))
        // 不是函式,則直接執行commit,引數是value和載荷組成的陣列。
        : commit.apply(this.$store, [val].concat(args))
    };
  });
  return res
});
複製程式碼
  • 和mapState的實現幾乎完全一樣,唯一的差別只有兩點:
    • 提交mutaion時可以傳遞載荷,也可以不傳,不管傳不傳,這裡都會進行拷貝載荷。
    • 對於函式就會執行這個函式,因為這個函式裡面其實就是一些 commit,而對於不是函式的內容就會直接進行 commit 操作,不過會繫結當前的this.$store作為作用域,也會傳載荷的引數。

那麼迴歸到實際轉換效果,如下:

// 需要引入mapMutations才可以使用
import { mapMutations } from 'vuex' 

export default {
  // ...
  methods: {
    ...mapMutations('moduleName', [
      // 將 `this.increment()` 對映為 `this.$store.commit('increment')`
      'increment',

      // `mapMutations` 也支援載荷:
      // 將 `this.incrementBy(amount)` 對映為 `this.$store.commit('incrementBy', amount)`
      'incrementBy' 
    ]),
    
    ...mapMutations('moduleName', {
      // 將 `this.add()` 對映為 `this.$store.commit('increment')`
      add: 'increment' 
    }),
    
    ...mapMutations('moduleName', {
      addAlias: function(commit) {
          //將 `this.addAlias()` 對映為 `this.$store.commit('increment')`
          commit('increment') 
      }
    })
  }
}
複製程式碼
  • increment和incrementBy其實是一樣的,只是為了區分所以起了兩個名字,可以看到他是直接轉為為this.$store.commit('increment')的,有引數的話會自動加引數而已。
  • 其他就比較好理解了,結合之前看到的原始碼,就知道他的轉換是分別處理

這是 jsrun 的例子:jsrun.net/U6qKp


參考:

相關文章