6、Vue 全局狀態管理

一、Vuex是什麼

1、簡介

組件中包含視圖(模板template)、雙向綁定的數據(data)、以及一些方法(methods),這3個都寫在同一個組件(component)裏面, 一般視圖(View)觸發方法動作(Actions),動作影響數據狀態(State), 數據狀態的改變又反應到視圖(View)上來,這樣在一個組件內就形成了一個閉環。即當前組件的視圖使用當前組件的數據,當前組件的動作(方法)只修改當前組件的數據,總體來說只能自己跟自己玩,不能多個組件相互玩。
在這裏插入圖片描述

我們有這樣兩個需求:

  • 多個視圖依賴於同一狀態。
  • 來自不同視圖的行爲需要變更同一狀態。

對於問題一,傳參的方法對於多層嵌套的組件將會非常繁瑣,並且對於兄弟組件間的狀態傳遞無能爲力。

對於問題二,我們經常會採用父子組件直接引用或者通過事件來變更和同步狀態的多份拷貝。以上的這些模式非常脆弱,通常會導致無法維護的代碼。

爲了解決以上問題,我們把組件的共享狀態(data)抽取出來,以一個全局單例模式管理, 這樣所有組件在任意時候都可以訪問全局狀態,當在任意組件中修改全局狀態,所有引用全局狀態的視圖也會隨之改變(響應式)。這就是Vuex的功能。

簡單來說Vuex在一個工程中只能有一個全局實例對象(Store),也就是該實例對象是整個應用級別的, 一個對象就包含了全部的應用層級狀態。 store被稱爲倉庫,用於盛放全局狀態的容器。任何組件都能訪問全局對象的數據(State),任何組件都可以修改全局對象的數據。這就是我們平常說的設計模式中的“單例模式”。

2、全局單例 Store 僞代碼
class Vuex.Store {
	public Object state;
	
	
	// 同步操作,接收參數 (state)
	public Object mutations;
	// 異步操作, 接收參數 (context)
	public Object actions;
	
	// 每個函數可以接收兩個參數 (state, getters)
	// 相當於是state的計算函數
	Public Object getters;
	
	// 模塊(module)
	public Object modules;
	
	// 命名空間(類似於包package的概念)
	boolean namespaced;
	
	// 插件
	public Object[] plugins;
	
	// 嚴格模式
	public boolean strict;
	
	/*
	* 提交mutation
	* 載荷(Payload):就是參數
	*/
	public void commit(String mutation, Object payload) {
	
	}
	
	/*
	* 分發action
	* 載荷(Payload):就是參數
	*/
	public void dispatch(String mutation, Object payload) {
	
	}
}

二、Vuex HelloWorld

1、Vuex安裝
npm install vuex --save

顯式使用Vuex插件,一般寫在src/main.js中,或者寫在其它js中然後再在main.js中引入

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

Vue.use(Vuex)
2、多組件共享全局狀態示例

分別在Foo.vue和Bar.vue中改變全局屬性count值,然後在App.vue中顯示count修改後的值。

(1)定義全局單例對象 src/store/store.js

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0
  },

  mutations: {
    increment (state, payload) {
      state.count += payload.step
    },
    decrement: state => state.count--,
  }
})

定義一個全局實例對象(Vuex.Store):

  • 該對象的狀態(state)就是全局屬性, 類似於組件的data,每個組件都可以訪問和修改屬性。

  • 可變的(mutations)類似於組件中的methods, mutations中的每個方法稱作爲 mutation handler,用來修改state中的值,方法的參數可以有兩個(state, payload) state表示全局的單例對象,payload(載荷)也就是參數,調用時可以傳遞參數,該參數是可選的。

使用Mutation時需遵守的一些規則:

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

  • 當需要在對象上添加新屬性時,你應該使用 Vue.set(obj, ‘newProp’, 123), 或者以新對象替換老對象

  • Mutation 必須是同步函數

(2)在src/main.js中導入store.js並作爲Vue的選項

import Vue from 'vue'
import App from './App'
import router from './router'

import store from './store/store'

Vue.config.productionTip = false


/* eslint-disable no-new */
new Vue({
  el: '#app',
  store,
  router,
  components: { App },
  template: '<App/>'
})

將store作爲Vue的選項,這樣Vue會將store“注入”到每一個子組件中,也就是說每個組件都可以通過this.$store來訪問全局單例對象store。
(3)Foo.vue

<template>
  <div>
    Foo.vue <button @click="increment">+</button>
  </div>
</template>

<script>
  export default {
    name: 'Foo',
    methods: {
      increment () {
        this.$store.commit('increment', {
          step: 10
        })
      }
    }
  }
</script>

調用store中的mutations方法只能通過提交的方式this.$store.commit('方法名', 負載參數)這一種形式來調用,而不能使用this.$store.方法名 這種普通的的對象.方法()方式直接調用。如果感覺這種方式麻煩,Vuex提供了一種將Mutations映射(map)爲methods的方式, 然後在使用時直接調用method就會自動幫你commit。

mapMutations() 函數,它接收一個參數,參數類型可以是數組也可以是對象:

  • 數組類型:當使用方法時方法名稱和Mutation的名稱一樣時使用數組類型。
  • 對象類型:當使用方法時方法名稱不想和Mutation的名稱一樣,可以對method起一個新的名稱
<template>
  <div>
    Foo.vue <button @click="add({step: 10})">+</button>
  </div>
</template>

<script>
  import { mapMutations } from 'vuex'

  export default {
    name: 'Foo',
    methods: {
      // 將 `this.increment()` 映射爲 `this.$store.commit('increment')`
      // 將 `this.incrementBy({step: 10})` 映射爲 `this.$store.commit('incrementBy', {step: 10})`
      ...mapMutations(['increment', 'incrementBy']),
      
      // 將Mutation(increment)映射爲method(add)
      ...mapMutations({
        add: 'increment'
      })
    }
  }
</script>

注意:mapMutations只是將Mutations簡單的映射爲methods, 其中method的方法體只包含this.$store.commit(‘mutation’, payload)這一樣代碼,如果method還要處理其它業務邏輯的話,那麼只能使用提交commit方式,而不能使用映射方式mapMutations。

Bar.vue

<template>
    <div>
      Bar.vue <button @click="decrement">-</button>
    </div>
</template>

<script>
  export default {
    name: 'Bar',
    methods: {
      decrement () {
        this.$store.commit('decrement')
      }
    }
  }
</script>

App.vue

<template>
  <div id="app">
    App.vue {{count}}
    <router-view name="foo"></router-view>
    <router-view name="bar"></router-view>
  </div>
</template>

<script>
  export default {
    name: 'App',
    computed: {
      count() {
        return this.$store.state.count
      }
    }
  }
</script>

可以通過{{ this.$store.state.count }}來獲取count的值,也可以將this.$store.state.count這行代碼包裝到計算屬性count中,這樣獲取值就方便點{{ count }}。
src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'

import Foo from '../components/Foo'
import Bar from '../components/Bar'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      components: {
        foo: Foo,
        bar: Bar
      }
    }
  ]
})

爲了在一個組件中使用多個兄弟組件,使用命名視圖,將這些兄弟組件作爲父組件的孩子組件。

(4)actions

Action 類似於 mutation,Action用於分發(dispatch)mutation,而不直接修改狀態。 Action 可以包含任意異步操作(如發送http請求)

action方法接收兩個參數(context, payload),context爲Context對象,context對象store實例具有相同方法和屬性,所以context可以解構成var {state, dispatch, commit, getters} = context

src/store/store.js

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0
  },

  mutations: {
    increment (state, payload) {
      state.count += payload.step
    },
    decrement: state => state.count--,
  },

  actions: {
    incrementAsync (context, payload) {
      console.log(context.state)
      console.log(context.getters)

      // 延遲1秒執行
      setTimeout(() => {
        context.commit('increment', payload)
      }, 1000)
    }
  }
})

Foo.vue

<template>
  <div>
    Foo.vue <button @click="increment">+</button>
  </div>
</template>

<script>

  export default {
    name: 'Foo',
    methods: {
      increment () {
        this.$store.dispatch('incrementAsync', { step: 10 })
      }
    }
  }
</script>

如果感覺封裝個方法調用store的dispatch()方法麻煩的話可以使用mapActions()輔助函數,它的用法和mapMutations一樣。mapActions()的作用就是簡單的將this.$store.dispatch('Action名稱', payload)這一行代碼封裝到方法中。同樣該函數接收一個參數,該參數的類型可以是數組類型也可以是對象類型。

<template>
  <div>
    Foo.vue <button @click="add({ step: 10 })">+</button>
  </div>
</template>

<script>
  import { mapActions } from 'vuex'
  export default {
    name: 'Foo',
    methods: {
      // 參數爲數組類型:method的名稱和action名稱一樣
      ...mapActions(['incrementAsync']),
      // 參數爲對象類型:method的名稱和action名稱不一致
      ...mapActions({
        add: 'incrementAsync'
      })
    }
  }
</script>

Action中操作一般都是異步的,通常都需要在異步操作完成後做一些其它邏輯,如何知道異步處理完成了呢?可以在action中將異步處理的邏輯封裝到Promise對象中,當異步邏輯處理完成就會調用Promise對象的then()方法,這樣我們將異步完成後的邏輯寫在then()方法中即可。

注意:dispatch(‘action’, payload)函數返回一個Promise對象,如果action中顯式返回Promise, 那麼dispatch()函數返回的就是action中的promise對象,如果action中沒有顯式的返回Promise對象,系統會將action中的邏輯封裝到一個新的Promise對象中,然後返回一個新的promise對象,所以dispatch()之後可以調用then()

src/store/store.js

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0
  },

  mutations: {
    increment (state, payload) {
      state.count += payload.step
    },
    decrement: state => state.count--,
  },

  actions: {
    incrementAsync (context, payload) {
      console.log(context.state)
      console.log(context.getters)

      return new Promise((resolve, reject) => {
        // 延遲1秒執行
        setTimeout(() => {
          // 提交mutation
          context.commit('increment', payload)
          // 成功,繼續執行
          resolve('異步執行結束')
        }, 1000)
      })
    }
  }
})

Foo.vue

<template>
  <div>
    Foo.vue <button @click="increment">+</button>
  </div>
</template>

<script>
  export default {
    name: 'Foo',
    methods: {
      increment () {
        // dispatch()函數的返回值爲action的返回值, action返回Promise
        // 所以dispatch()之後可以調用then()
        this.$store.dispatch('incrementAsync', { step: 10 }).then((resp) => {
          console.log(resp)
        })
      }
    }
  }
</script>

action之間相互調用通過dispatch來調用

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

Vue.use(Vuex)

export default new Vuex.Store({
  // ...
  actions: {
    fooAsync () {
      console.log('fooAsync')
    },
    barAsync ({ dispatch }) {
      // 使用dispatch調用其它action
      dispatch('fooAsync').then(() => {
        console.log('barAsync')
      });
    }
  }
})

三、組件之間傳遞參 示例

src/store/store.js

預先定義出屬性名 pageParams

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    pageParams: {}
  }
})
App.vue
<template>
  <div id="app">
    <button @click="goPage">頁面傳參</button>

    <router-view></router-view>
  </div>
</template>

<script>
  export default {
    name: 'App',
    methods: {
      goPage () {
        this.$store.state.pageParams = {foo: 'foo', bar: 'bar'}
        this.$router.push('/foo')
      }
    }
  }
</script>

修改對象的某個屬性除了使用等號=直接賦值外,也可以使用Vue的全局方法set來賦值。Vue.set(對象, ‘屬性名’, 屬性值)

import Vue from 'vue'
Vue.set(this.$store.state, 'pageParams', {foo: 'foo', bar: 'bar'})
src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'

import Foo from '../components/Foo'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/foo',
      component: Foo
    }
  ]
})
Foo.vue
<template>
  <div>
    {{ this.$store.state.pageParams }}
  </div>
</template>

<script>
  export default {
    name: 'Foo'
  }
</script>
mapState 輔助函數

當組件中要訪問多個store中的狀態就需要寫多個計算屬性,比較麻煩,可以使用mapState輔助函數簡化寫法, mapState函數的作用就是用於生成計算屬性。mapState函數接收一個對象參數

  • 數組參數: 如果直接取store下的state值而不需要任何計算,可以直接傳數組參數,值爲store中的state中的屬性名
  • 對象參數:如果需要對全局屬性進行額外的計算,可以使用一個函數進行計算
    mapState({ 計算方法 })
<script>
 import { mapState } from 'vuex'
 export default {
   name: 'Foo',
   computed: mapState({
     pageParams1 (state) {
       return state.pageParams
     },
     // 傳字符串參數 'pageParams' 等同於 `state => state.pageParams`
     pageParams2: 'pageParams'
   })
 }
</script>
mapState([‘屬性名’])
<script>
 import { mapState } from 'vuex'
 export default {
   name: 'Foo',
   computed: mapState(['pageParams'])
 }
</script>

對象展開運算符 …mapState
<script>
  import { mapState } from 'vuex'
  export default {
    name: 'Foo',
    data () {
      return {
        msg: 'Hello Vue'
      }
    },
    computed: {
      message () {
        return this.msg + "!"
      },
      // 使用對象展開運算符將此對象混入到外部對象中,即computed對象中
      // 相當於將mapState(['pageParams'])裏的計算函數都拿到computed裏,作爲computed的函數
      // 當計算函數中除了mapState外還有別的計算方法時使用
      ...mapState(['pageParams'])
    }
  }
</script>

四、Getter

Getter就是對store對象的狀態state進行計算的方法,getter方法接收兩個參數(state, getters), 可以通過this.$store.getters.getter方法名來調用getter方法。

src/store/store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: 'Task A', isDone: true },
      { id: 2, text: 'Task B', isDone: false }
    ]
  },
  getters: {
    doneTodos (state, getters) {
      return state.todos.filter(item => item.isDone)
    },

    // 調用其他getter方法
    doneTodosCount (state, getters) {
      return getters.doneTodos.length
    },

    // getter方法也可以返回一個函數
    getTodoById (state) {
      var myfun = function (id) {
        return state.todos.find(todo => todo.id === id)
      }
      return myfun;
    }
  }
})

App.vue

<template>
  <div id="app">
    {{ this.$store.getters.doneTodos }} <br>
    {{ this.$store.getters.doneTodosCount }} <br>
    {{ this.$store.getters.getTodoById(2) }} <br>

    {{ doneTodoList }}
  </div>
</template>

<script>
  import { mapGetters } from 'vuex'
  export default {
    name: 'App',
    computed: {
      // 使用對象展開運算符將 getter 混入 computed 對象中
      ...mapGetters({
        doneTodoList: 'doneTodos'
      })
    }
  }
</script>

mapGetters 輔助函數僅僅是將 store 中的 getter 映射到局部計算屬性, mapGetters() 函數接收一個參數,參數類型可以是對象類型也可以是數組類型:

  • 如果需要將getter方法映射成計算屬性起一個別名時使用對象參數
  • 如果不需要將getter方法映射成計算屬性起一個別名時使用數組參數,數組裏的值就是getter的名字,如 [‘doneTodos’, ‘doneTodosCount’]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章