[Vuex]Vuex學習手記

Reference_Error發表於2019-09-08

一、序言

本篇文章更像是我學習vuex的一個筆記,學習的資源主要是來自官方文件教程,官方教程已經講的比較細緻了,部分地方也有自己不理解的地方,所以也查過其他資料來輔助自己理解,本手記在官方的教程上加了一些自己的補充內容,希望能給你帶來一些參考價值,另外也感謝網際網路上其他分享知識的大佬,讓我少走了些彎路!如果文章有理解不到位的地方,還請各位多批評指正!

二、Vuex之初體驗

1、為何使用Vuex

使用Vue開發的過程中,我們經常會遇到一個狀態可能會在多個元件之間使用,比如我們在做專案時使用到的使用者的資訊,什麼暱稱、頭像這些,這些資訊會在不同的元件用到,一旦改變這些狀態,我們希望其他元件也跟隨變化,比如使用者充值了100元,或者改變了暱稱,所以這個時候就需要狀態管理模式來集中管理,關於Vuex的詳細介紹可以移步到官網。

2、學習之前的準備

本次我的學習都是在官方提供的腳手架搭建的專案下學習的,關於腳手架的使用本次就不再贅述,可以移步到Vue CLI,在使用Vue CLI生成的專案時會讓你選擇store,選擇了後會在頁面給你生成一個store.js,這就是最初的store,裡面結構如下:

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {

  },
  mutations: {

  },
  actions: {

  }
})
複製程式碼

三、State

Vuex的核心就是倉庫store,這個store例項會被注入到所有子元件裡面,裡面的state屬性儲存著我們的狀態,比如我們定義一個狀態count:

export default new Vuex.Store({
  state: {
    count: 10
  },
})
複製程式碼

這樣我們就有一個集中管理的狀態count,那其他元件如何取到這個count呢,可以計算屬性來獲得:

export default {
  data() {
    
  },
  computed: {
    count() {
      return this.$store.state.count;
    }
  }
}
複製程式碼

因為根例項中註冊 store 選項,該 store 例項會注入到根元件下的所有子元件中,且子元件能通過 this.$store 訪問到。通過計算屬性,我們就可以在模板裡面使用模板語法來呼叫count了,如下:

<template>
  <div>
    <p>{{ count }}</p>
  </div>
</template>
複製程式碼

mapState

有時候需要獲取多個狀態,但是使用計算屬性會呼叫多次,顯得麻煩,這裡藉助mapState方法來獲取state。 使用mapState需要引入該方法

import { mapState } from 'vuex';
複製程式碼

注意:這裡使用了mapState方法後,computed的寫法有點區別,比如預設情況你的computed屬性是這樣寫的:

data(){
  return{
    msg: 'hello '
  }
}
computed: {
  msg() {
    return this.msg + 'world!';
  }
}
複製程式碼

那麼你使用了mapState後需要這樣寫computed,把msg()放入mapState,不然會報錯。

data(){
  return{
    msg: 'hello ',
    localCount: 20
  }
}
computed: mapState({
  msg() {  // 最初的
    return this.msg + 'world!';
  },
  // 使用mapState從store中引入state
  count(state) {
    return state.count;
  },
  name(state) {
    return state.firstName + ' ' + state.lastName;
  },
  mixCount(state) { // 結合store和元件狀態進行計算
    return state.count + this.localCount;
  },
})
複製程式碼

如果你使用了展開運算子...,那麼computed屬性不需要改造,按正常寫法寫

computed: { // 使用展開的話可以按這種方式寫,否則要使用另外一種方式,不然要報錯
  msg() {
    return this.$store.state.msg;
  },
  // 這裡返回一個狀態count,
  // 返回多個你可以這樣寫...mapState(['count', 'firstName', 'lastName'])
  ...mapState(['count'])
},
複製程式碼

四、Getter

getter就是對狀態進行處理的提取出來的公共部分,當狀態要進行篩選這些操作時,我們可以通過getter處理過後再返回給元件使用,比如我們在state部分定義了一個list陣列:

export default new Vuex.Store({
  state: {
    list: [1, 2, 3, 4, 5, 6, 7, 8]
  },
});
複製程式碼

我們想要篩選出陣列裡面的偶數然後再在元件裡面使用,那麼篩選的這個工作可以放在getter裡面來完成。

export default new Vuex.Store({
  state: {
    list: [1, 2, 3, 4, 5, 6, 7, 8]
  },
  getters: { //  這個主要是對狀態的處理,相當於把狀態處理的方法抽成公共部分來管理了
    modifyArr(state) { // 一般化getter
      return state.list.filter((item, index, arr) => {
        return item % 2 == 0;
      })
    },
    getLength(state, getter) { // 方法裡面傳getter,呼叫modifyArr來計算長度
      return getter.modifyArr.length;
    }
});
複製程式碼

之後再在其他元件的computed裡面去呼叫getter來獲取想要的狀態

computed: {
    list() {
      return this.$store.getters.modifyArr;
    },
}
複製程式碼

mapGetters

mapGetters 輔助函式僅僅是將 store 中的 getter 對映到區域性計算屬性,當我們想在元件裡面引入多個getter時,可以使用mapGetter:

import {mapGetters} from 'vuex';
複製程式碼

比如像剛才在在上面定義的modifyArr,getLength。我們想引入這個兩個並獲取其值:

computed: {
  ...mapGetter(['modifyArr', 'getLength'])
}
複製程式碼

你當然可以為其指定別名,不一定非得用store裡面getters定義的名字:

computed: {
  mapGetter({
    arr: 'modifyArr',   // 把 `this.arr` 對映為 `this.$store.getters.modifyArr`,下面同理
    length: 'getLength'
  })
}
複製程式碼

如果你的computed屬性包含其他計算方法,那你就只能使用展開運算子的寫法,這裡跟mapState有點區別,其他計算屬性如果寫在mapGetter裡面會報錯,說不存在的getter,所以用以下的寫法:

computed: {
  msg() {
    return this.num * 10;
  },
  ...mapGetters([
    'modifyArr',
    'getLength'
  ])
}
複製程式碼

或者指定別名

computed: { 
  msg() {
    return this.num * 10;
  },
  ...mapGetters({
    getList: 'modifyArr',
    length: 'getLength'
  })
}
複製程式碼

然後再模板裡面呼叫:

<template>
  <div>
    <h2>mapGetters的使用演示</h2>
    <p>你的數字:{{ msg }}</p>
    <p>你的陣列長度為:{{ length }}</p>
    <ul>
      <li v-for="(item, index) in getList" :key="index">{{ item }}</li>
    </ul>
  </div>
</template>
複製程式碼

五、Mutation

當我們需要修改store裡面的狀態時,我們不是在元件裡面直接去修改它們,而是通過mutation裡面的方法來進行修改,這樣有利於追蹤狀態的改變。 比如state裡面有一個count變數,我們點選加減按鈕來控制它的值:

mutations: {
  add(state) {
    state.count++;
  },
  reduce(state) {
    state.count--;
  }
},
複製程式碼

在其他元件裡面,我們通過定義methods並繫結時間來觸發改變,這裡需要使用commit:

methods: {
  add() {
    this.$store.commit('add');
  },
  reduce() {
    this.$store.commit('reduce');
  }
}
複製程式碼

提交載荷

這個就是在commit時提交額外的引數,比如我傳了額外的值加到count上面:

mutations: {
  loadAdd(state, payload) {  // 提交載荷,額外引數
    state.count += payload;
  }
},
複製程式碼

然後再元件裡面使用:

methods: {
  loadAdd() {
    this.$store.commit('loadAdd', 100); // 傳遞額外引數
  }
}
複製程式碼

再這裡官方文件建議載荷(也就是那個額外引數)最好使用物件來傳,這樣可以包含多個欄位並且記錄的 mutation 會更易讀,像這樣:

this.$store.commit('loadAdd', {
  extraCount: 100
}); // 傳遞額外引數
複製程式碼

呼叫commit時我們也可以把所有引數寫在一個物件裡面:

this.$store.commit( {
  type: 'addLoad'
  extraCount: 100
}); // 傳遞額外引數
複製程式碼

Mutation 需遵守 Vue 的響應規則

這個主要是說你再開發過程中需要向state裡面新增額外資料時,需要遵循響應準則。 這裡我直接照搬官方文件的說明: Vuex 中的 mutation 也需要與使用 Vue 一樣遵守一些注意事項:

  • 最好提前在你的 store 中初始化好所有所需屬性。

  • 當需要在物件上新增新屬性時,你應該使用 Vue.set(obj, 'newProp', 123), 或者以新物件替換老物件。例如,利用 stage-3 的物件展開運算子

我們可以這樣寫:

state.obj = { ...state.obj, newProp: 123 }
複製程式碼

還是舉個例子: 我在mutation裡面宣告瞭一個方法

addNewState(state, payload) { // 我打算再這兒新增新的屬性到state
  // Vue.set(state, 'newProp', '新增一個新值!'); // 這是一種寫法
  // 這種寫法用新物件替換老物件
  // state= {...state, newProp: '新增一個新值!'} // 這個玩意兒不管用了,用下面的replaceState()方法
  this.replaceState({...state, newProp: '新增一個新值!'})
}
複製程式碼

然後再元件裡面去呼叫,定義一個method:

addNewProp() {
  this.$store.commit('addNewState', {});
}
複製程式碼

這樣再執行了這個方法後,會及時更新到state,再元件的computed屬性裡面定義:

newMsg() {
  return this.$store.state.newProp || '還沒新增新值';
}
複製程式碼

在模板裡面即時展示,並且不會影響其他狀態:

<p>新增的新值:{{ newMsg }}</p>
<div><button @click="addNewProp">新增新值</button></div>
複製程式碼

Mutation 必須是同步函式

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

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}
複製程式碼

mapMutations

這個跟前面的那幾個函式一個道理,都是為了簡化呼叫,使用方法如下:

import {mapMutations} from 'vuex';
複製程式碼

然後在元件的methods裡面使用,這裡使用官方程式碼:


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裡面的方法來改變狀態。 這裡我直接貼程式碼來一目瞭然,首先我定義了一個狀態product:
state: {
  product: 'car'
}
複製程式碼

然後再mutation中定義一個方法:

changeProduct(state, payload) {
  state.product = payload.change;
}
複製程式碼

在action中定義:

actions: {
  changeProduct(context, payload) { // 這個context是一個與 store 例項具有相同方法和屬性的物件
    // 呼叫mutation裡的changeProduct方法
    // context.commit('changeProduct', {change: 'ship'});
    // 改成非同步方式
    // setTimeout(() => {
    //   context.commit('changeProduct', {change: 'ship'});
    // }, 1500)
    // 使用載荷
    let temp = 'ship+' + payload.extraInfo; 
    setTimeout(() => {
      context.commit('changeProduct', {change: temp});
    }, 1500)
  }
}
複製程式碼

在元件methods中定義事件觸發分發:

methods: {
  selectProduct() {
    // this.$store.dispatch('changeProduct')
    // 載荷方式分發
    // this.$store.dispatch('changeProduct', {
    //   extraInfo: 'sportcar'
    // })
    // 或者這種
    this.$store.dispatch({
      type: 'changeProduct',
      extraInfo: '->sportcar'
    })
  }
},
複製程式碼

這樣一個簡易的action就完成了!

mapActions

這裡就不再贅述了,看名字就知道跟前面幾個叫map開頭的輔助函式類似,用來對映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來實現。這裡在state裡面宣告一個狀態:

state: {
  userInfo: { // 這個變數用來測試組合變數
    name: 'lee',
    age: 23
  }
}
複製程式碼

接著宣告mutation:

mutations: {
    // 以下用來測試組合action
    changeInfo(state, payload) {
      state.userInfo.name = 'lee haha';
    }
}
複製程式碼

宣告action:

actions: {
  changeInfo(context, payload) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        context.commit('changeInfo');
        resolve();
      }, 2000)
    })
  }
}
複製程式碼

這時我們在元件裡面定義方法去派發這個action:

data() {
  return {
    status: '資訊還沒修改!'
  }
}
methods: {
  modifyInfo() {
    this.$store.dispatch('changeInfo').then(() => {
      this.status = '資訊修改成功';
    });
  }
}
複製程式碼

模板展示:

<template>
  <div>
    <h2>組合action</h2>
    <p>{{ status }}</p>
    <p>{{ info.name }}</p>
    <div><button @click="modifyInfo">修改資訊</button></div>
  </div>
</template>

複製程式碼

當我們點選修改資訊後,就會派發action,當修改成功的時候會同步修改上面說的展示資訊。 當然其他定義的action方法也可以互相使用,這裡直接貼官方程式碼了:

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  },
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}
複製程式碼

七、Module

模組這部分正如其名,當所有狀態集中在一個物件中時,會變的相當臃腫,這個時候就需要模組的管理辦法。這裡我還是用程式碼來說明,比如我在store裡面定義了兩個模組:

// 定義的模組A
const moduleA = {
  state: {
    name: 'lee',
    age: 23,
  },
  mutations: {

  },
  getters: {

  },
  actions: {

  }
};

// 定義模組B
const moduleB = {
  state: {
    name: 'wang',
    age: 22
  },
  mutations: {

  },
  getters: {

  },
  actions: {

  }
}
複製程式碼

然後再Vuex裡面宣告模組:

export default new Vuex.Store({
  modules: {
    ma: moduleA,
    mb: moduleB
  },
  state: {
    ........... // 其他狀態
  }
});
複製程式碼

這樣一來,如果我們想要在元件裡面訪問其他模組的狀態,可以這樣,比如這裡我想呼叫B模組裡的狀態:

computed: {
  msg() {
    return this.$store.mb; // 這裡返回的是:{name: 'wang', age: 22}
  }
}
複製程式碼

關於模組內部的區域性狀態,這裡跟普通的store用法沒有多大的區別,主要區別以下外部傳進來的狀態,比如對於模組內部的 action,區域性狀態通過 context.state 暴露出來,根節點狀態則為 context.rootState,這裡擷取官方程式碼:

const moduleA = {
  // ...
  actions: {
    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
    }
  }
}

複製程式碼

那麼對於getters、mutations、actions裡面的方法我們像基本的store那樣呼叫就可以了,不存在作用域限制,還是貼程式碼栗子吧,下面是我在store.js裡面定義的模組B:

const moduleB = {
  state: {
    name: 'wang',
    age: 22,
    desc: 'nope'
  },
  mutations: {
    modifyDesc(state, payload) {
      state.desc = payload.newMsg;
    }
  },
  getters: {

  },
  actions: {

  }
}
複製程式碼

在元件裡面,我定義了以下內容:

<template>
  <div>
    <h2>7、module使用示例</h2>
    <div>
      <p>名字:{{ name }}</p>
      <p>描述:{{ desc }}</p>
      <button @click="handleClick">修改描述</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      name: this.$store.state.mb.name,
      // desc: this.$store.state.mb.desc 注意這個如果涉及到要在store裡面會被改變的狀態,一定要寫在
      // computed屬性裡面,不然不能及時反饋到檢視上
    }
  },
  computed: {
    desc() {
      return this.$store.state.mb.desc;
    }
  },
  methods: {
    handleClick() {
      this.$store.commit('modifyDesc', {newMsg: 'lao wang is beautiful!'});
    }
  },
}
</script>
複製程式碼

這樣,就可以呼叫mutation裡面的方法了,getters和actions同理

2019/09/09 補充餘下內容

名稱空間模組

預設情況下,mutations、actions、getters這些都是註冊在全域性上面的,你可以直接呼叫,如果希望你的模組具有更高的封裝度和複用性,你可以通過新增 namespaced: true 的方式使其成為帶名稱空間的模組。當模組被註冊後,它的所有 getter、action 及 mutation 都會自動根據模組註冊的路徑調整命名。 首先我新建一個js檔案用來宣告模組C:

/* 
* 這個檔案用來宣告模組C
*/

export const moduleC = {
  namespaced: true,
  state: {
    name: 'moduleC',
    desc: '這是模組C,用來測試名稱空間的!',
    list: [1, 2, 3, 4]
  },
  getters: {
    filterList(state) {
      return state.list.filter((item, index, arrSelf) => {
        return item % 2 !== 0;
      });
    }
  },
  mutations: {
    modifyName(state, payload) {
      state.name = payload.newName;
    }
  },
  actions: {
    
  }
}
複製程式碼

然後在store.js裡面引入:

import { moduleC } from './module_c.js';

export default new Vuex.Store({
  modules: {
    mc: moduleC
  },
});
複製程式碼

要想這個模組成為帶有名稱空間的模組,在上面宣告屬性namespaced: true就可以了,那麼裡面的mutations、getters和actions裡面的方法的呼叫就要多走一層路徑,比如我在元件裡面去呼叫mutations裡面的方法(getters和actions同理):

methods: {
  modify() {
    // this.$store.commit('mc/modifyName', {
    //   newName: '名稱空間模組C'
    // })
    this.$store.commit({
      type: 'mc/modifyName',
      newName: '名稱空間模組C'
    })
  }
}
複製程式碼

當然模組裡面再巢狀模組也可以,路徑要不要多走一層主要看你的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']
          }
        }
      }
    }
  }
})
複製程式碼

在帶名稱空間的模組內訪問全域性內容

如果想要在模組內部的getters、mutations和actions裡面訪問到全域性的內容,這兒Vuex已經封裝好了,你只需要多傳幾個引數即可。官方演示來一波,簡單明瞭:

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) { ... }
    }
  }
}
複製程式碼

在模組裡面使用輔助函式mapState、mapGetters、mapMutations和mapActions

由於存在名稱空間,在元件裡面採用上面的寫法會出現問題,這裡要想使用輔助函式來對映模組裡面的東西需要指定空間名稱來告訴輔助函式應該去哪兒找這些。 這兒我以上面我的C模組為例,首先對於mapSatate函式可以這樣玩,我在全域性的modules裡面宣告瞭mc,那我的空間名稱就是mc:

computed: {
  ...mapState('mc', ['name', 'desc']) // 這裡模組裡面要使用輔助函式的話要多傳一個引數才行
}
複製程式碼

然後在模版裡面寫name,desc即可,或者可以這樣:

computed: {
  ...mapState('mc', {
    name(state) {
      return state.name;
    },
    desc(state) {
      return state.desc;
    }
  })
},
複製程式碼

對於actions、mutations和getters方式類似,主要是要指定空間名稱,比如對於宣告的mutations:

methods: {
  ...mapMutations('mc', ['modifyName'])
}
複製程式碼

如果你確實不想在每個輔助函式裡寫空間名稱,Vuex也提供了其它辦法,使用createNamespacedHelpers建立基於某個名稱空間輔助函式,它返回一個物件,物件裡有新的繫結在給定名稱空間值上的元件繫結輔助函式:

import { createNamespacedHelpers } from 'vuex';

const { mapState, mapMutations } = createNamespacedHelpers('mc');
複製程式碼

這樣你在寫輔助函式的時候就不需要單獨指定空間名稱了。 其它類似,恕我就不再贅述了!

八、結語

本篇相當於基礎入門篇,其他內容大家有興趣進官網瀏覽即可(#滑稽保命)。相關的程式碼我已經傳到github上了,感興趣就下載來看看吧! 演示程式碼

相關文章