天天看點

玩爛 Vuex

Vuex 概念篇

Vuex 是什麼?

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

什麼是“狀态管理模式”?

從軟體設計的角度,就是以一種統一的約定和準則,對全局共享狀态資料進行管理和操作的設計理念。你必須按照這種設計理念和架構來對你項目裡共享狀态資料進行CRUD。是以所謂的“狀态管理模式”就是一種軟體設計的一種架構模式(思想)。

為什麼需要這種“狀态管理模式”應用到項目中呢?

現如今,流行元件化、子產品化開發,多人開發各自元件的時候,不難保證各個元件都是唯一性的,多個元件共享狀态肯定是存在的,共享狀态又是誰都可以進行操作和修改的,這樣就會導緻所有對共享狀态的操作都是不可預料的,後期出現問題,進行 debug 也是困難重重,往往我們是盡量去避免全局變量。

但大量的業務場景下,不同的子產品(元件)之間确實需要共享資料,也需要對其進行修改操作。也就引發軟體設計中的沖突:子產品(元件)之間需要共享資料 和 資料可能被任意修改導緻不可預料的結果。

為了解決其沖突,軟體設計上就提出了一種設計和架構思想,将全局狀态進行統一的管理,并且需要擷取、修改等操作必須按我設計的套路來。就好比馬路上必須遵守的交通規則,右行斑馬線就是隻能右轉一個道理,統一了對全局狀态管理的唯一入口,使代碼結構清晰、更利于維護。

Vuex 是借鑒了 Flux 、Redux 和 The Elm Architecture 架構模式、設計思想的産物。
玩爛 Vuex

什麼情況下我應該使用 Vuex?

不打算開發大型單頁應用,使用 Vuex 可能是繁瑣備援的。應用夠簡單,最好不要使用 Vuex。一個簡單的

global event bus

(父子元件通信,父元件管理所需的資料狀态)就足夠您所需了。建構一個中大型單頁應用,您很可能會考慮如何更好地在元件外部管理狀态,Vuex 将會成為自然而然的選擇。

個人見解,什麼時候用?管你小中大型應用,我就想用就用呗,一個長期建構的小型應用項目,誰能知道項目需求以後會是什麼樣子,畢竟在這浮躁的時代,需求就跟川劇變臉一樣快,對不對?畢竟學習了 Vuex 不立馬用到項目實戰中,你永遠不可能揭開 Vuex 的面紗。項目中使用多了,自然而然就會知道什麼時候該用上狀态管理,什麼時候不需要。老話說的好熟能生巧,你認為呢?

(括弧 -- 先了解好Vuex 一些基本概念,然後在自己的項目中使用過後,再用到你公司項目上,你别這麼虎一上來就給用上去了~)

Vuex 基本使用篇

安裝

npm i vuex -S           

項目全局中任何地方使用 Vuex, 需要将 Vuex 注冊到 Vue 執行個體中:

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

Vue.use(Vuex)           

Vuex 上車前的一個

上車規則:

  1. 每一個 Vuex 應用的核心就是 store(倉庫),一個項目中必須隻有一個 store 執行個體。包含着你的應用中大部分的狀态 (state)。
  2. 不能直接改變 store 中的狀态。改變 store 中的狀态的唯一途徑就是顯式地送出 (commit) mutation。

Vuex 和單純的全局對象有以下兩點不同:

(1) Vuex 的狀态存儲是響應式的。Vue 元件從 store 中讀取狀态的時候,若 store 中的狀态發生變化,那麼相應的元件也會相應地得到高效更新。

(2)不能直接改變 store 中的狀态。(重要的事情多來一遍)

上:

<div id="app">
  <p>{{ count }}</p>
  <p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </p>
</div>
           
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex) // Vuex 注冊到 Vue 中

// 建立一個 store

const store = new Vuex.Store({
  // 初始化 state   
  state: {
    count: 0
  },
 // 改變狀态唯一聲明處
  mutations: {
  	increment: state => state.count++,
    decrement: state => state.count--
  }
})

new Vue({
  el: '#app',
  // 從根元件将 store 的執行個體注入到所有的子元件  
  store,  
  computed: {
    count () {
        // Vuex 的狀态存儲是響應式的,從 store 執行個體中讀取狀态最簡單的方法就是在計算屬性中傳回某個狀态,
        // 每當狀态 count 發生變化,都會重新求取計算
	    return this.$store.state.count
    }
  },
  methods: {
    increment () {
      this.$store.commit('increment')
    },
    decrement () {
    	this.$store.commit('decrement')
    }
  }
})
           

state

一個對象管理應用的所有狀态,唯一資料源(SSOT, Single source of truth),必須前期初始化就定義好,不然後面在來修改設定

state

,程式就不會捕獲到

mutations(突變的意思)

是以資料也就不會又任何的更新,元件上也無法展現出來。

擷取 store 管理的狀态, 因為在 Vue 執行個體化的時候将 Vuex store 對象 注入了進來 ,是以任何地方都可以通過

this.$store

擷取到 store,

this.$store.state

來擷取狀态對象,

this.$store.commit

來觸發之前定義好的 mutations 中的方法

this.$store.state('count') // => 0

this.$store.commit('increment') // => 1           

通過送出

mutation

的方式,而非直接改變

store.state.count

, 使用

commit

方式可以讓 Vuex 明确地追蹤到狀态的變化,利于後期維護和調試。

通過了解

state

(狀态,資料)和

mutations

(修改資料唯一聲明的地方,類似 SQL 語句)知道了 Vuex 最重要的核心兩部分,然後通過掌握 gttter、action、module 來讓 Vuex 更加的工程化、合理化來适應更大型的項目的狀态管理。

mapState 輔助函數

mapState

可以幹什麼呢?字面意思狀态映射,通過

mapState

可以更加快捷友善幫我們生成計算屬性,拿上面的例子進行示範:

computed: {
    count () {
	    return this.$store.state.count
    }
 }

// 使用 mapState

import { mapState } from 'vuex' // 需要先導入

computed: mapState([
    // 箭頭函數方式
	count: state => state.count ,
    
    // or 傳字元串參數方式, 'count' 等同于 state => state.count
    countAlias: 'count'
    
    // 擷取狀态後,你還需要和目前元件别的屬性值時,就必須使用正常函數的寫法了, 隻有這樣才能擷取到目前元件的 this
    countPlusLocalState (state) {
    	return state.count + this.localCount
    } 
])
           

目前計算屬性名稱和狀态名稱相同時,可以傳遞一個字元串數組:

computed: mapState([
    // 映射 `this.count` 為 `this.$store.state.count`
	'count'
])           

以上使用

mapState

輔助函數後,整個

computed

計算屬性都成了

state

狀态管理聚集地了, 元件裡并不是所有的計算屬性都需要被狀态管理化,還是有很多計算屬性是不需要狀态管理的資料的,那如何将它與局部計算屬性混合使用呢?

因為

mapState

函數傳回的是一個對象。是以我們使用對象擴充運算符就可以把局部計算屬性和

mapState

函數傳回的對象融合了。

computed: {
	...mapState({
		count: state => state.count    
    }),
    
   localComputed () {
   	   /* ... */ 
   }     
}           

️ 注意:

對象擴充運算符,現處于 ECMASCript 提案 stage-4 階段(将被添加到下一年度釋出),是以項目中要使用需要安裝

babel-plugin-transform-object-rest-spread 插件 或 安裝 presets 環境為 stage 為 1的 env 版本 babel-preset-stage-1 和修改 babelrc 配置檔案

.babelrc

{
    "presets": [
        "env",
        "stage-1" // 添加此項
    ],
    "plugins": [
        "transform-vue-jsx",
        "syntax-dynamic-import"
    ]
}
           

核心概念

上面以講述過 state 了,這裡就不過多的說明。

Getter

Getter

就是

Store

狀态管理層中的計算屬性,擷取源

State

後,希望在對其進行一些包裝,再傳回給元件中使用。也是就将直接擷取到

State

後在

computed

裡進行再次的過濾、包裝邏輯統統提取出放到

Getter

裡進行,提高了代碼的複用性、可讀性。

computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}           

提取到 

Getter

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  //  `Getter` 預設第一個參數為 `state`:  
  getters: {
    doneTodos: state => {
      return state.todos.filter( todo => todo.done )
    }
  }
})


//  元件中擷取,通過屬性通路

computed: {
	doneTodosCount () {
    	return this.$store.getters.doneTodos.length
    } 
}           
️ 注意: getter 在通過屬性通路時是作為 Vue 的響應式系統的一部分進行緩存。

Getter 還接受其他 getter 作為第二個參數

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 )
    },
    // 第二個參數為 getters  
    doneTodosLength: (state, getters) => {
    	return getters.doneTodos.length
    }  
  }
})
           

還可以通過給 Getter 傳遞參數擷取特定的資料

getters: {
	// ...
    
    getTodoById: state => id => {
    	return state.todos.find( todo => todo.id === id )
    }
}           

元件内調用方式

this.$store.getters.getTodoById(2) // => { id: 2, text: '...', done: false }           
️ 注意:getter 在通過方法通路時,每次都會去進行調用,而不會緩存結果。

mapGetters 輔助函數

和前面 

mapState

 輔助函數作用和使用上基本相同。

import { mapGetters } from 'vuex'

// getter 名稱和 計算屬性名稱相同的情況下,可以傳遞字元串數組

export default {
  // ...
  computed: {
  	...mapGetters([
    	'doneTodos'
    ])
  }
}

// 傳遞對象的方式

export default {
  // ...
  computed: {
  	...mapGetters({
    	doneTodos: 'doneTodos',
        getTodoById: 'getTodoById' // 此處傳遞回來的是一個函數,是以在使用的時候 => {{ getTodoById(2) }}
    })
  }
}

           

Mutation

不能直接修改狀态,需要通過 Vuex store 中聲明的 Mutations 裡的方法修改狀态。 更改 Vuex 的 store 中的狀态的唯一方法是送出

mutation

mutation

是一個對象, 含有一個字元串的 事件類型 (type) 和 一個 回調函數 (handler)。

回調函數就是我們實際進行狀态更改的地方,預設接受 state 作為第一個參數。

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    // increment 事件類型(type)名稱,increment() 回調函數
    // increment: function (state) {}  原本寫法
    increment (state) {
      // 變更狀态
      state.count++
    }
  }
})
           

mutation handler

 不能被直接調用,需要通過 

store.commit()

 來通知我需要觸發一個 

mutation handler

this.$store.commit('increment')           

mutation

 接收參數必須隻能兩個,超出的都無法擷取;第二個參數推薦傳遞的是一個對象,來接收更多的資訊。

this.$store.commit('increment', 10) 

// or

this.$store.commit('increment', { num: 10 })            

對象風格的送出方式

this.$store.commit({
	type: 'increment',
    num: 10
})           

Mutation 需要遵守 Vue 的響應規則

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

  1. 最好提前在你的 store 中初始化好所有所需屬性(state 中的屬性)。
  2. 當需要在對象上添加新屬性時,你應該傳回的是一個新對象

    * 使用 Vue.set(obj, 'newProp', 123),或者

    * 以新對象替換老對象。例如:

    Object.assgin({}, state.obj, newProps)

    、對象擴充運算符

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

mapMutation 輔助函數

使用方式跟

mapState

mapGetters

基本相同。

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

調用方式就跟

methods

其他普通方法一樣,通過

this.<methodName>

來調用。

mutation 必須是同步函數,修改 state 必須是同步的、同步的、同步的。

如果是異步的,當觸發 mutation 的時候,内部的回調函數還沒有被調用,根本不知道實際執行在何處,很難追蹤起問題。(實質上任何在回調函數中進行的狀态的改變都是不可追蹤的。

Vuex 也提供了異步操作的解決方案, 需要将異步操作提取出來放入到 Action 裡進行操作。而 Mutation 隻負責同步事務。

Action

在之前也講述了,Action 是用來處理異步操作的。這裡在詳細說明一下 Action 的基本使用。

Action 類似于 mutation, 不同在于:

  • Action 送出的是 mutation,而不是直接變更狀态。(不直接修改狀态,修改狀态還是需要通過 mutation)
  • Action 可以包含任意異步操作。
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
     // 實踐中,經常用到參數解構來簡化代碼, increment ({commit}) { commit('') }
    increment (context) {
      context.commit('increment')
    }
  }
})           

Action 函數接受一個與 store 執行個體具有相同方法和屬性的 context 對象(并不是真正的 store 本身),是以可以調用

store.commit

進行送出 mutation, 或者通過

context.state

context.getters

來擷取

state

getters

觸發Action

Action 通過

store.dispatch

方法觸發:

this.$store.dispatch('increment')

// 以傳遞額外參數分發
store.dispatch('incrementAsync', {
  amount: 10
})

// 以對象形式分發
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})
           

與伺服器資料異步請求基本在 Action 裡進行, 然後通過 Mutation 來同步應用狀态

state

mapAction 輔助函數

和 

mapMutions

 使用方式基本一緻

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

Action 通常是異步的,那麼如何知道 action 什麼時候結束呢?更重要的是,我們如何才能組合多個 action,以處理更加複雜的異步流程?

通過傳回一個 Promise 對象來進行組合多個 Action。

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

然後:

store.dispatch('actionA').then(() => {
  // ...
})           

利用 

async / await

,我們可以如下組合 action:

// 假設 getData() 和 getOtherData() 傳回的是 Promise

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}
           

Module

由于 Vuex 使用單一狀态樹模式,來統一管理應用所有的狀态,導緻所有狀态會集中到一個比較大的對象,随着後續不斷得疊代,這個對象就會越來越龐大,後期的代碼可讀性、可維護性就會不斷加大。

解決以上問題,就需要對這個對象的内部進行拆分和細分化,對狀态進行分門别類,也就産生了子產品(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 的狀态

           

聲明子產品後,state、mutation、action、getter 等使用方式、作用和不在

modules

内聲明方式基本一樣,隻是在細節上進行了一些細微的改變,比如: getter 裡預設接收一個參數 state,子產品裡接收 state 就是本身子產品自己的 state 狀态了,而不是全局的了; 調用擷取上也多一道需要告知那個子產品擷取狀态 等一些細節上的差異。

Module 裡 state、mutation、action、getter 上的一些差異

(1)子產品内部的 mutation 和 getter,接收的第一個參數

state

是子產品的局部狀态對象。

(2)子產品内部的 action,局部狀态通過

context.state

暴露出來,根節點狀态則為

context.rootState

(3)子產品内部的 getter,根節點狀态會作為第三個參數暴露出來

命名空間

預設情況下,子產品内部的 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']
          }
        }
      }
    }
  }
})
           

在帶命名空間的子產品内通路全局内容

帶命名空間的子產品内通路全局 state 、getter 和 action,

rootState

rootGetter

會作為第三和第四參數傳入

getter

,也會通過

context

對象的屬性傳入 action。

需要在全局命名空間内分發

action

或送出

mutation

,将

{ root: true }

作為第三參數傳給

dispatch

commit

即可。

modules: {
  foo: {
    namespaced: true,

    getters: {
      // 在這個子產品的 getter 中,`getters` 被局部化了
      // 全局的 state 和 getters 可以作為第三、四個參數進行傳入,進而通路全局 state 和 getters
      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')
    }
  },
  modules: {
    foo: {
      namespaced: true,

      actions: {
        someAction: {
          root: true,
          handler (namespacedContext, payload) { ... } // -> 'someAction'
        }
      }
    }
  }
}
           
帶命名空間的子產品裡輔助函數如何使用?

将子產品的空間名稱字元串作為第一個參數傳遞給上述函數,這樣所有綁定都會自動将該子產品作為上下文。

computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  ...mapActions('some/nested/module', [
    'foo',
    'bar'
  ])
}
           

還可以通過使用 

createNamespacedHelpers

 建立基于某個命名空間輔助函數。它傳回一個對象,對象裡有新的綁定在給定命名空間值上的元件綁定輔助函數:

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}
           

子產品動态注冊

在 store 建立之後,你可以使用 

store.registerModule

 方法注冊子產品:

// 注冊子產品 `myModule`
store.registerModule('myModule', {
  // ...
})
// 注冊嵌套子產品 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})
           

子產品動态注冊功能使得其他 Vue 插件可以通過在 store 中附加新子產品的方式來使用 Vuex 管理狀态。例如,

vuex-router-sync

插件就是通過動态注冊子產品将

vue-router

vuex

結合在一起,實作應用的路由狀态管理。

你也可以使用

store.unregisterModule(moduleName)

來動态解除安裝子產品。注意,你不能使用此方法解除安裝靜态子產品(即建立 store 時聲明的子產品)。

待更新~

原文釋出時間為:2018年06月23日

原文作者:Junting

本文來源: 

掘金 https://juejin.im/entry/5b3a29f95188256228041f46

如需轉載請聯系原作者

繼續閱讀