玩爛 Vuex

Vuex 概念篇

Vuex 是什麼?

Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。html

什麼是「狀態管理模式」?

從軟件設計的角度,就是以一種統一的約定和準則,對全局共享狀態數據進行管理和操做的設計理念。你必須按照這種設計理念和架構來對你項目裏共享狀態數據進行CRUD。因此所謂的「狀態管理模式」就是一種軟件設計的一種架構模式(思想)。vue

爲何須要這種「狀態管理模式」應用到項目中呢?

現現在,流行組件化、模塊化開發,多人開發各自組件的時候,不難保證各個組件都是惟一性的,多個組件共享狀態確定是存在的,共享狀態又是誰均可以進行操做和修改的,這樣就會致使全部對共享狀態的操做都是不可預料的,後期出現問題,進行 debug 也是困難重重,每每咱們是儘可能去避免全局變量。git

但大量的業務場景下,不一樣的模塊(組件)之間確實須要共享數據,也須要對其進行修改操做。也就引起軟件設計中的矛盾:模塊(組件)之間須要共享數據數據可能被任意修改致使不可預料的結果github

爲了解決其矛盾,軟件設計上就提出了一種設計和架構思想,將全局狀態進行統一的管理,而且須要獲取、修改等操做必須按我設計的套路來。就比如馬路上必須遵照的交通規則,右行斑馬線就是隻能右轉一個道理,統一了對全局狀態管理的惟一入口,使代碼結構清晰、更利於維護。vue-router

Vuex 是借鑑了 Flux 、Redux 和 The Elm Architecture 架構模式、設計思想的產物。vuex

Vuex運行機制

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

不打算開發大型單頁應用,使用 Vuex 多是繁瑣冗餘的。應用夠簡單,最好不要使用 Vuex。一個簡單的 global event bus (父子組件通訊,父組件管理所需的數據狀態)就足夠您所需了。構建一箇中大型單頁應用,您極可能會考慮如何更好地在組件外部管理狀態,Vuex 將會成爲天然而然的選擇。npm

我的看法,何時用?管你小中大型應用,我就想用就用唄,一個長期構建的小型應用項目,誰能知道項目需求之後會是什麼樣子,畢竟在這浮躁的時代,需求就跟川劇變臉同樣快,對不對?畢竟學習了 Vuex 不立馬用到項目實戰中,你永遠不可能揭開 Vuex 的面紗。項目中使用多了,天然而然就會知道何時該用上狀態管理,何時不須要。老話說的好熟能生巧,你認爲呢? (括弧 -- 先了解好Vuex 一些基本概念,而後在本身的項目中使用事後,再用到你公司項目上,你別這麼虎一上來就給用上去了~)json

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 中的狀態的惟一方法是提交 mutationmutation 是一個對象, 含有一個字符串的 事件類型 (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 輔助函數

使用方式跟 mapStatemapGetters 基本相同。

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.statecontext.getters 來獲取 stategetters

觸發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, rootStaterootGetter會做爲第三和第四參數傳入 getter,也會經過 context 對象的屬性傳入 action。

須要在全局命名空間內分發 action 或提交 mutation,將 { root: true } 做爲第三參數傳給 dispatchcommit 便可。

modules: {
  foo: {
    namespaced: true,

    getters: {
      // 在這個模塊的 getter 中,`getters` 被局部化了
      // 全局的 state 和 getters 能夠做爲第3、四個參數進行傳入,從而訪問全局 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-routervuex 結合在一塊兒,實現應用的路由狀態管理。

你也可使用 store.unregisterModule(moduleName)來動態卸載模塊。注意,你不能使用此方法卸載靜態模塊(即建立 store 時聲明的模塊)。

待更新~

相關文章
相關標籤/搜索