前端MVC Vue2學習總結(九)——Vuex狀態管理插件


一、概要

1.1、Vuex定義與注意事項

Vuex是為vue.js框架更好的管理狀態而設計一個插件。Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。Vuex 也集成到 Vue 的官方調試工具 devtools extension,提供了諸如零配置的 time-travel 調試、狀態快照導入導出等高級調試功能。

使用Vue開發中需要將應用拆分成多個組件,但是組件與組件之間數據共享成了一個問題,父子組件實現起來相對簡單,有很多兄弟組件和跨多級組件,實現起來過程繁瑣,在多人協同開發上,不利於統一管理,Vuex可以解決這些問題。

1.1.1、狀態管理模式

沒有使用Vuex時,讓我們看一個簡單的 Vue 計數應用:

new Vue({
  // state
  data () {
    return {
      count: 0
    }
  },
  // view
  template:'<div>{{ count }}</div>',
  // actions
  methods: {
    increment () {
      this.count++
    }
  }
})

這個狀態自管理應用包含以下幾個部分:

  • state,驅動應用的數據源;
  • view,以聲明方式將 state 映射到視圖;
  • actions,響應在 view 上的用戶輸入導致的狀態變化。

以下是一個表示“單向數據流”理念的極簡示意:

但是,當我們的應用遇到多個組件共享狀態時,單向數據流的簡潔性很容易被破壞:

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

對於問題一,傳參的方法對於多層嵌套的組件將會非常繁瑣,並且對於兄弟組件間的狀態傳遞無能為力。對於問題二,我們經常會采用父子組件直接引用或者通過事件來變更和同步狀態的多份拷貝。以上的這些模式非常脆弱,通常會導致無法維護的代碼。

因此使用Vuex,我們為什么不把組件的共享狀態抽取出來,以一個全局單例模式管理呢?在這種模式下,我們的組件樹構成了一個巨大的“視圖”,不管在樹的哪個位置,任何組件都能獲取狀態或者觸發行為!

另外,通過定義和隔離狀態管理中的各種概念並強制遵守一定的規則,我們的代碼將會變得更結構化且易維護。

這就是 Vuex 背后的基本思想,借鑒了  FluxRedux、和  The Elm Architecture。與其他模式不同的是,Vuex 是專門為 Vue.js 設計的狀態管理庫,以利用 Vue.js 的細粒度數據響應機制來進行高效的狀態更新。

 

1.1.2、使用 Vuex

雖然 Vuex 可以幫助我們管理共享狀態,但也附帶了更多的概念和框架。這需要對短期和長期效益進行權衡。

如果您不打算開發大型單頁應用,使用 Vuex 可能是繁瑣冗余的。確實是如此——如果您的應用夠簡單,您最好不要使用 Vuex。一個簡單的  store 模式就足夠您所需了。但是,如果您需要構建一個中大型單頁應用,您很可能會考慮如何更好地在組件外部管理狀態,Vuex 將會成為自然而然的選擇。引用 Redux 的作者 Dan Abramov 的話說就是:"Flux 架構就像眼鏡:您自會知道什么時候需要它。"

1.1.3、注意事項

Vuex會有一定的門檻和復雜性,它的主要使用場景是大型單頁面應用,如果你的項目不是很復雜,用一個bus也可以實現數據的共享(在前面講組件的內容中已經講到過bus作為總線進行通信的示例),但是它在數據管理,維護,還只是一個簡單的組件,而Vuex可以更優雅高效地完成狀態管理,所以,是否使用Vuex取決於你的團隊和技術儲備。

使用bus作為總線通信的示例:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Vue2 Demo</title>
</head>
<body>
<div id="app01">
    <my-comp1></my-comp1>
    <my-comp2></my-comp2>
</div>
<script src="../../js/vue/vue.js"></script>
<script>
    //事件總線
    var bus = new Vue();

    Vue.component("my-comp1", {
        template: "<button @click='incrN'>{{n}}</button>",
        data() {
            return {n: 0}
        },
        methods: {
            incrN: function () {
                this.n++;
                //發布事件
                bus.$emit("inc",this.n);
            }
        }
    });


    Vue.component("my-comp2", {
        template: "<button @click='incrN'>{{n}}</button>",
        data() {
            return {n: 999}
        },
        methods: {
            incrN: function () {
                this.n--;
            }
        },
        //勾子,掛載完成時執行事件
        mounted:function () {
            var _this=this;
            //監聽事件,訂閱事件
            bus.$on("inc",function (val) {
                _this.n+=val;
            })
        }
    });

    var vm = new Vue({
        el: "#app01",
        data: {}
    });
</script>
</body>
</html>
View Code

1.2、概念

每一個 Vuex 應用的核心就是store(倉庫),store基本上就是一個容器,它包含着你的應用中大部分的狀態 (state)。 Vuex和單純的全局對象有以下兩點不同:

1.Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那么相應的組件也會相應地得到高效更新;

2.你不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation。這樣使得我們可以方便地跟蹤每一個狀態的變化,從而讓我們能夠實現一些工具幫助我們更好地了解我們的應用。

  • store:表示對Vuex對象的全局引用。組件通過Store來訪問Vuex對象中的State。
  • state:保存數據的狀態、對象的狀態,即其所擁有的數據。
  • getter:相當於Store的計算屬性。因為就像計算屬性一樣,Getter的返回值會根據它的依賴被緩存起來,且只有當它的依賴值發生了改變才會被重新計算。下面會說到具體的使用場景。
  • mutations:定義了對state中數據的修改操作,更改store中的狀態的唯一方法是提交mutation。mutation類似於事件:每個mutation都有一個字符串的事 件類型(type),和一個回調函數(handler).利用store.commit('方法名')來調用這個函數。
  • mutations-type:可以認為是store中的計算屬性,mapGetters是輔助函數,僅僅將store中的getter映射到局部計算屬性。
  • action :mutation中定義的操作只能執行同步操作,Vuex中的異步操作在Action中進行,Action最終通過調用Mutation的操作來更新數據;類似於mutation,不同在於action提交的是mutation,而不是直接變更狀態,action可以包含任意 異步操作。action用store.dispatch方法觸發函數。mapActions是輔助函數,將組件的 methods 映射為store.dispatch。
  • module:Store和State之間的一層,便於大型項目管理,Store包含多個Module,Module包含State、Mutation和Action。

1.3、資源

github: https://github.com/vuejs/vuex

中文幫助: https://vuex.vuejs.org/zh/(本文大量引用)

英文幫助: https://vuex.vuejs.org/

視頻教程: https://www.bilibili.com/video/av17503637/

二、安裝

2.1、直接下載或CDN 引用

引用地址: https://unpkg.com/vuex

Unpkg.com 提供了基於 NPM 的 CDN 鏈接。以上的鏈接會一直指向 NPM 上發布的最新版本。您也可以通過 https://unpkg.com/vuex@3.0.1/dist/vuex.js這樣的方式指定特定的版本。

CDN引用:

<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vuex"></script>

也可以使用下載后本地引用,vuex的github地址是: https://github.com/vuejs/vuex

2.2、NPM包管理器

npm i vuex --save

2.3、Yarn

yarn add vuex

在一個模塊化的打包系統中,您必須顯式地通過 Vue.use() 來安裝 Vuex:

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

Vue.use(Vuex)

//當使用全局 script 標簽引用 Vuex 時,不需要以上安裝過程。

2.4、Promise

Vuex 依賴 Promise。如果你支持的瀏覽器並沒有實現 Promise (比如 IE),那么你可以使用一個polyfill的庫,例如 es6-promise。

你可以通過 CDN 將其引入:

<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.js"></script>

然后 window.Promise 會自動可用。

如果你喜歡使用諸如 npm 或 Yarn 等包管理器,可以按照下列方式執行安裝:

npm install es6-promise --save # npm
yarn add es6-promise # Yarn

或者更進一步,將下列代碼添加到你使用 Vuex 之前的一個地方:

import 'es6-promise/auto'

2.5、自己構建

如果需要使用 dev 分支下的最新版本,您可以直接從 GitHub 上克隆代碼並自己構建。

git clone https://github.com/vuejs/vuex.git node_modules/vuex
cd node_modules/vuex
npm install
npm run build

三、應用

3.1、頁面使用Vuex快速起步

創建一個 store,創建過程直截了當——僅需要提供一個初始 state 對象和一些 mutation:

// 如果在模塊化構建系統中,請確保在開頭調用了 Vue.use(Vuex)
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

現在,你可以通過 store.state 來獲取狀態對象,以及通過 store.commit 方法觸發狀態變更:

store.commit('increment')

console.log(store.state.count) // -> 1

再次強調,我們通過提交 mutation 的方式,而非直接改變 store.state.count,是因為我們想要更明確地追蹤到狀態的變化。這個簡單的約定能夠讓你的意圖更加明顯,這樣你在閱讀代碼的時候能更容易地解讀應用內部的狀態改變。此外,這樣也讓我們有機會去實現一些能記錄每次狀態改變,保存狀態快照的調試工具。有了它,我們甚至可以實現如時間穿梭般的調試體驗。

由於 store 中的狀態是響應式的,在組件中調用 store 中的狀態簡單到僅需要在計算屬性中返回即可。觸發變化也僅僅是在組件的 methods 中提交 mutation。

完整示例代碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    <p>{{ count }}</p>
    <p>
        <button @click="increment">+</button>
        <button @click="decrement">-</button>
    </p>
    <div>
        <comp1></comp1>
        <comp1></comp1>
    </div>
</div>
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vuex"></script>
<script>

    //定義倉庫對象
    const store = new Vuex.Store({
        state: {
            count: 0
        },
        mutations: {
            increment: state => state.count++,
            decrement: state => state.count--
        }
    })

    //定義組件
    Vue.component("comp1",{
        template:"<h2>{{count}}</h2>",
        computed: {
            count () {
                return store.state.count
            }
        }
    });

    new Vue({
        el: '#app',
        computed: {
            count () {
                return store.state.count
            }
        },
        methods: {
            increment () {
                store.commit('increment')
            },
            decrement () {
                store.commit('decrement')
            }
        }
    })
</script>
</body>
</html>

運行結果:

 

在線示例:這是一個 最基本的 Vuex 記數應用示例

3.2、Vue-cli中使用Vuex快速起步

安裝vuex:

插件引用:

//導入插件
import Vuex from 'vuex'

//使用插件
Vue.use( Vuex );

//定義倉庫對象
const store = new Vuex.Store({
    //屬性
})

//定義vue實例並關聯存儲倉庫
new Vue({
    el: '#app',
    store,
    render: h => h(App)
});

定義狀態對象main.js:

import Vue from 'vue'
import Vuex from 'vuex';
import App from './App'
import router from './router/hello'

Vue.config.productionTip = false

Vue.use(Vuex);

//定義倉庫對象
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment: state => state.count++,
    decrement: state => state.count--
  }
})

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

直接使用狀態對象中的數據App.vue:

<template>
  <div>
    <img src="./assets/logo.png">
    <h2>
      {{$store.state.count}}
    </h2>
    <Counter/>
    <header>
      <!-- router-link 定義點擊后導航到哪個路徑下 -->
      <router-link to="/" exact>index</router-link>
      <router-link to="/foo">Go to Foo</router-link>
      <router-link to="/bar">Go to Bar</router-link>
    </header>
    <!-- 對應的組件內容渲染到router-view中 -->
    <router-view></router-view>
  </div>
</template>
<script>
  import Counter from './components/Counter';

  export default {
    components:{Counter}
  }
</script>
<style scoped>
  a {
    color: #777;
  }

  a:hover {
    color: orangered;
  }
</style>
View Code

在組件中使用狀態對象:

//Counter.vue
<template>
  <div id="app">
    <h2>{{ count }}</h2>
    <p>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
    </p>
  </div>
</template>

<script>
  export default {
    name: "Counter",
    computed: {
      count() {
        return this.$store.state.count
      }
    },
    methods: {
      increment() {
        this.$store.commit('increment')
      },
      decrement() {
        this.$store.commit('decrement')
      }
    }
  }
</script>

<style scoped>
  h2 {
    color: darkblue;
  }
</style>

引用組件:

//bar.vue

<template>
  <div>
    <h2>Bar</h2>
    <p>{{msg}}</p>
    <Counter/>
  </div>
</template>
<script>
  import Counter from './Counter';

  export default {
    data() {
      return {
        msg: "我是Bar組件"
      }
    },
    components: {Counter}
  }
</script>
<style scoped>
  h2 {
    color: springgreen;
  }
</style>

App.Vue內容:

<template>
  <div>
    <img src="./assets/logo.png">
    <h2>
      {{$store.state.count}}
    </h2>
    <Counter/>
    <header>
      <!-- router-link 定義點擊后導航到哪個路徑下 -->
      <router-link to="/" exact>index</router-link>
      <router-link to="/foo">Go to Foo</router-link>
      <router-link to="/bar">Go to Bar</router-link>
    </header>
    <!-- 對應的組件內容渲染到router-view中 -->
    <router-view></router-view>
  </div>
</template>
<script>
  import Counter from './components/Counter';

  export default {
    components:{Counter}
  }
</script>
<style scoped>
  a {
    color: #777;
  }

  a:hover {
    color: orangered;
  }
</style>

運行結果:

切換到一個單頁Bar

3.3、沒有使用vuex的汽車列表(示例)

為了方便后面的內容講解這里重新做一個簡單的vuex汽車列表示例,這個示例分別有兩個組件CarListOne.vue和CarListTwo.vue, 在App.vue的datat中保存着共有的汽車列表, 代碼和初始化的效果如下圖所示:

 

App.Vue:
<template>
  <div id="app">
    <h2>汽車商城</h2>
    <hr/>
    <car-list-one v-bind:cars="cars"></car-list-one>
    <car-list-two v-bind:cars="cars"></car-list-two>
  </div>
</template>

<script>
  import CarListOne from './components/CarListOne.vue'
  import CarListTwo from './components/CarListTwo.vue'

  export default {
    name: 'app',
    components: {
      'car-list-one': CarListOne,
      'car-list-two': CarListTwo
    },
    data() {
      return {
        cars: [
          {name: '奇瑞', price: 18.3},
          {name: '吉利', price: 19.6},
          {name: '長安', price: 17.5},
          {name: '紅旗', price: 21.9}
        ]
      }
    }
  }
</script>

<style>
  h2 {
    color: orangered;
  }
</style>

CarListOne.vue

<template>
  <div id="car-list-one">
    <h2>Car List One</h2>
    <ul>
      <li v-for="car in cars">
        <span>{{ car.name }}</span>
        <span>¥{{ car.price }}萬元</span>
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    props: ['cars'],
    data() {
      return {}
    }
  }
</script>

<style scoped>
  h2 {
    color: dodgerblue;
  }
</style>

CarListTwo.vue

<template>
  <div id="car-list-two">
    <h2>Car List Two</h2>
    <ul>
      <li v-for="car in cars">
        <button>{{ car.name }}</button>
        <button>¥{{ car.price }}萬元</button>
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    props: ['cars'],
    data() {
      return {}
    }
  }
</script>

<style scoped>
  h2 {
    color: limegreen;
  }
</style>

3.4、State

state就是Vuex中的公共的狀態, 我是將state看作是所有組件的data, 用於保存所有組件的公共數據。

此時我們就可以把App.vue中的兩個組件共同使用的data抽離出來, 放到state中,代碼如下:

//main.js
import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex'

Vue.use( Vuex )

const store = new Vuex.Store({
  state:{ 
    cars: [
      {name: '奇瑞', price: 20},
      {name: '吉利', price: 40},
      {name: '長安', price: 60},
      {name: '比亞迪', price: 80}
    ]
  }
})

new Vue({
  el: '#app',
  store,
  render: h => h(App)
})
此時,CarListOne.vue和CarListTwo.vue也需要做相應的更改
//CarListOne.vue
export default {
    data () {
        return {
            cars : this.$store.state.cars //獲取store中state的數據
        }
    }
}
//CarListTwo.vue
export default {
    data () {
        return {
            cars: this.$store.state.cars //獲取store中state的數據
        }
    }
}

此時的頁面如下圖所示, 可以看到, 將公共數據抽離出來后, 頁面沒有發生變化。

3.5、Getters

我將getters屬性理解為所有組件的computed屬性, 也就是計算屬性。vuex的官方文檔也是說到可以將getter理解為store的計算屬性,getters的返回值會根據它的依賴被緩存起來,且只有當它的依賴值發生了改變才會被重新計算。

此時,我們可以在main.js中添加一個getters屬性, 其中的saleCars對象將state中的價格減少一半(除以2)

//main.js
const store = new Vuex.Store({
  state:{
    cars: [
      {name: '奇瑞', price: 20},
      {name: '吉利', price: 40},
      {name: '長安', price: 60},
      {name: '比亞迪', price: 80}
    ]
  },
  getters:{ //添加getters
    saleCars: (state) => {
      let saleCars = state.cars.map( car => {
        return {
          name: car.name,
          price: car.price / 2
        }
      })
      return saleCars;
    }
  } 
})
將carListOne.vue中的cars的值更換為this.$store.getters.saleCars
export default {
    data () {
        return {
            cars : this.$store.getters.saleCars 
        }
    }
}

現在的頁面中,Car List One中的每項汽車的價格都減少了一半

 

getters 和 vue 中的 computed 類似 , 都是用來計算 state 然后生成新的數據 ( 狀態 ) 的。

還是前面的例子 , 假如我們需要一個與狀態 show 剛好相反的狀態 , 使用 vue 中的 computed 可以這樣算出來 :

computed(){
    not_show(){
        return !this.$store.state.dialog.show;
    }
}
那么 , 如果很多很多個組件中都需要用到這個與 show 剛好相反的狀態 , 那么我們需要寫很多很多個 not_show , 使用 getters 就可以解決這種問題 :

export default {
    state:{//state
        show:false
    },
    getters:{
        not_show(state){//這里的state對應着上面這個state
            return !state.show;
        }
    },
    mutations:{
        switch_dialog(state){//這里的state對應着上面這個state
            state.show = state.show?false:true;
            //你還可以在這里執行其他的操作改變state
        }
    },
    actions:{
        switch_dialog(context){//這里的context和我們使用的$store擁有相同的對象和方法
            context.commit('switch_dialog');
            //你還可以在這里觸發其他的mutations方法
        },
    }
}
我們在組件中使用 $store.state.dialog.show 來獲得狀態 show , 類似的 , 我們可以使用 $store.getters.not_show 來獲得狀態 not_show 。

注意 : $store.getters.not_show 的值是不能直接修改的 , 需要對應的 state 發生變化才能修改。

 

 

mapState、mapGetters、mapActions
很多時候 , $store.state.dialog.show 、$store.dispatch('switch_dialog') 這種寫法又長又臭 , 很不方便 , 我們沒使用 vuex 的時候 , 獲取一個狀態只需要 this.show , 執行一個方法只需要 this.switch_dialog 就行了 , 使用 vuex 使寫法變復雜了 ?

使用 mapState、mapGetters、mapActions 就不會這么復雜了。

以 mapState 為例 :

<template>
  <el-dialog :visible.sync="show"></el-dialog>
</template>

<script>
import {mapState} from 'vuex';
export default {
  computed:{

    //這里的三點叫做 : 擴展運算符
    ...mapState({
      show:state=>state.dialog.show
    }),
  }
}
</script>
相當於 :

<template>
  <el-dialog :visible.sync="show"></el-dialog>
</template>

<script>
import {mapState} from 'vuex';
export default {
  computed:{
    show(){
        return this.$store.state.dialog.show;
    }
  }
}
</script>
mapGetters、mapActions 和 mapState 類似 , mapGetters 一般也寫在 computed 中 , mapActions 一般寫在 methods 中。

 

 

3.6、Mutations

我將mutaions理解為store中的methods, mutations對象中保存着更改數據的回調函數,該函數名官方規定叫type, 第一個參數是state, 第二參數是payload, 也就是自定義的參數.

下面,我們在main.js中添加mutations屬性,其中minusPrice這個回調函數用於將汽車的價格減少payload這么多, 代碼如下:

//main.js
const store = new Vuex.Store({
  state:{
    cars: [
      {name: '奇瑞', price: 20},
      {name: '吉利', price: 40},
      {name: '長安', price: 60},
      {name: '比亞迪', price: 80}
    ]
  },
  getters:{
    saleCars: (state) => {
      let saleCars = state.cars.map( car => {
        return {
          name: car.name,
          price: car.price / 2
        }
      })
      return saleCars;
    }
  },
  mutations:{ //添加mutations
    minusPrice (state, payload ) {
      let newPrice = state.cars.forEach( car => {
        car.price -= payload
      })
    }
  }
})
在CarListTwo.vue中添加一個按鈕,為其添加一個點擊事件, 給點擊事件觸發minusPrice方法
//CarListTwo.vue
<template>
    <div id="car-list-two">
        <h2>Car List Two</h2>
        <ul>
            <li v-for="car in cars">
                <span class="name">{{ car.name }}</span>
                <span class="price">${{ car.price }}</span>
            </li>
            <button @click="minusPrice">減少價格</button> //添加按鈕
        </ul>
    </div>
</template>
在CarListTwo.vue中注冊minusPrice方法, 在該方法中commitmutations中的minusPrice這個回調函數
注意:調用mutaions中回調函數, 只能使用store.commit(type, payload)
//CarListTwo.vue
export default {
    data () {
        return {
            cars: this.$store.state.cars
        }
    },
    methods: {
        minusPrice() {
            this.$store.commit('minusPrice', 2); //提交`minusPrice,payload為2
        }
    }
}

添加按鈕, 可以發現, Car List Two中的價格減少了2, 當然你可以自定義payload,以此自定義減少對應的價格.

mutations效果


(Car List One中的價格沒有發生變化, 是因為getters將價格進行了緩存)

 

前面我們提到的對話框例子 , 我們對vuex 的依賴僅僅只有一個 $store.state.dialog.show 一個狀態 , 但是如果我們要進行一個操作 , 需要依賴很多很多個狀態 , 那管理起來又麻煩了 !

mutations 登場 , 問題迎刃而解 :

export default {
    state:{//state
        show:false
    },
    mutations:{
        switch_dialog(state){//這里的state對應着上面這個state
            state.show = state.show?false:true;
            //你還可以在這里執行其他的操作改變state
        }
    }
}
使用 mutations 后 , 原先我們的父組件可以改為 :

<template>
  <div id="app">
    <a href="javascript:;" @click="$store.commit('switch_dialog')">點擊</a>
    <t-dialog></t-dialog>
  </div>
</template>

<script>
import dialog from './components/dialog.vue'
export default {
  components:{
    "t-dialog":dialog
  }
}
</script>
使用 $store.commit('switch_dialog') 來觸發 mutations 中的 switch_dialog 方法。

這里需要注意的是:

mutations 中的方法是不分組件的 , 假如你在 dialog_stroe.js 文件中的定義了
switch_dialog 方法 , 在其他文件中的一個 switch_dialog 方法 , 那么
$store.commit('switch_dialog') 會執行所有的 switch_dialog 方法。
mutations里的操作必須是同步的。
你一定好奇 , 如果在 mutations 里執行異步操作會發生什么事情 , 實際上並不會發生什么奇怪的事情 , 只是官方推薦 , 不要在 mutationss 里執行異步操作而已。

 

 

3.7、Actions

actions 類似於 mutations,不同在於:

  • actions提交的是mutations而不是直接變更狀態

  • actions中可以包含異步操作, mutations中絕對不允許出現異步

  • actions中的回調函數的第一個參數是context, 是一個與store實例具有相同屬性和方法的對象

  • 此時,我們在store中添加actions屬性, 其中minusPriceAsync采用setTimeout來模擬異步操作,延遲2s執行 該方法用於異步改變我們剛才在mutaions中定義的minusPrice

//main.js
const store = new Vuex.Store({
  state:{
    cars: [
      {name: '奇瑞', price: 20},
      {name: '吉利', price: 40},
      {name: '長安', price: 60},
      {name: '比亞迪', price: 80}
    ]
  },
  getters:{
    saleCars: (state) => {
      let saleCars = state.cars.map( car => {
        return {
          name: car.name,
          price: car.price / 2
        }
      })
      return saleCars;
    }
  },
  mutations:{
    minusPrice (state, payload ) {
      let newPrice = state.cars.forEach( car => {
        car.price -= payload
      })
    }
  },
  actions:{ //添加actions
    minusPriceAsync( context, payload ) {
      setTimeout( () => {
        context.commit( 'minusPrice', payload ); //context提交
      }, 2000)
    }
  }
})
在CarListTwo.vue中添加一個按鈕,為其添加一個點擊事件, 給點擊事件觸發minusPriceAsync方法
<template>
    <div id="car-list-two">
        <h2>Car List Two</h2>
        <ul>
            <li v-for="car in cars">
                <span class="name">{{ car.name }}</span>
                <span class="price">${{ car.price }}</span>
            </li>
            <button @click="minusPrice">減少價格</button>
            <button @click="minusPriceAsync">異步減少價格</button> //添加按鈕
        </ul>
    </div>
</template>
在CarListTwo.vue中注冊minusPriceAsync方法, 在該方法中dispatchactions中的minusPriceAsync這個回調函數
export default {
    data () {
        return {
            cars: this.$store.state.cars
        }
    },
    methods: {
        minusPrice() {
            this.$store.commit('minusPrice', 2);
        },
        minusPriceAsync() {
            this.$store.dispatch('minusPriceAsync', 5); //分發actions中的minusPriceAsync這個異步函數
        }
    }
}

 

  • 添加按鈕, 可以發現, Car List Two中的價格延遲2s后減少了5


    actions效果
    actions效果
     
     
     
    多個 state 的操作 , 使用 mutations 會來觸發會比較好維護 , 那么需要執行多個 mutations 就需要用 action 了:
    
    export default {
        state:{//state
            show:false
        },
        mutations:{
            switch_dialog(state){//這里的state對應着上面這個state
                state.show = state.show?false:true;
                //你還可以在這里執行其他的操作改變state
            }
        },
        actions:{
            switch_dialog(context){//這里的context和我們使用的$store擁有相同的對象和方法
                context.commit('switch_dialog');
                //你還可以在這里觸發其他的mutations方法
            },
        }
    }
    那么 , 在之前的父組件中 , 我們需要做修改 , 來觸發 action 里的 switch_dialog 方法:
    
    <template>
      <div id="app">
        <a href="javascript:;" @click="$store.dispatch('switch_dialog')">點擊</a>
        <t-dialog></t-dialog>
      </div>
    </template>
    
    <script>
    import dialog from './components/dialog.vue'
    export default {
      components:{
        "t-dialog":dialog
      }
    }
    </script>
    使用 $store.dispatch('switch_dialog') 來觸發 action 中的 switch_dialog 方法。
    
    官方推薦 , 將異步操作放在 action 中。

     

     

 

3.8、Modules

由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象。當應用變得非常復雜時,store 對象就有可能變得相當臃腫。為了解決以上問題,Vuex 允許我們將 store 分割成模塊(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 的狀態

 

 

前面為了方便 , 我們把 store 對象寫在了 main.js 里面 , 但實際上為了便於日后的維護 , 我們分開寫更好 , 我們在 src 目錄下 , 新建一個 store 文件夾 , 然后在里面新建一個 index.js :

import Vue from 'vue'
import vuex from 'vuex'
Vue.use(vuex);

export default new vuex.Store({
    state:{
        show:false
    }
})
那么相應的 , 在 main.js 里的代碼應該改成 :

//vuex
import store from './store'

new Vue({
  el: '#app',
  router,
  store,//使用store
  template: '<App/>',
  components: { App }
})
這樣就把 store 分離出去了 , 那么還有一個問題是 : 這里 $store.state.show 無論哪個組件都可以使用 , 那組件多了之后 , 狀態也多了 , 這么多狀態都堆在 store 文件夾下的 index.js 不好維護怎么辦 ?

我們可以使用 vuex 的 modules , 把 store 文件夾下的 index.js 改成 :

import Vue from 'vue'
import vuex from 'vuex'
Vue.use(vuex);

import dialog_store from '../components/dialog_store.js';//引入某個store對象

export default new vuex.Store({
    modules: {
        dialog: dialog_store
    }
})
這里我們引用了一個 dialog_store.js , 在這個 js 文件里我們就可以單獨寫 dialog 組件的狀態了 :

export default {
    state:{
        show:false
    }
}
做出這樣的修改之后 , 我們將之前我們使用的 $store.state.show 統統改為 $store.state.dialog.show 即可。

如果還有其他的組件需要使用 vuex , 就新建一個對應的狀態文件 , 然后將他們加入 store 文件夾下的 index.js 文件中的 modules 中。

modules: {
    dialog: dialog_store,
    other: other,//其他組件
}

 

 

 

改進的計算器

eg:store.js

import Vue from 'vue';
import Vuex from 'vuex'; //引入 vuex
import store from './store' //注冊store

Vue.use(Vuex); //使用 vuex

export default new Vuex.Store({
    state: {
        // 初始化狀態
        count: 0,
        someLists:[]
    },
    mutations: {
        // 處理狀態
        increment(state, payload) {
            state.count += payload.step || 1;
        }
    },
    actions: {
        // 提交改變后的狀態
        increment(context, param) {
            context.state.count += param.step;
            context.commit('increment', context.state.count)//提交改變后的state.count值
        },
        incrementStep({state, commit, rootState}) {
            if (rootState.count < 100) {
                store.dispatch('increment', {//調用increment()方法
                    step: 10
                })
            }
        }
    },
    getters: {
        //處理列表項
        someLists: state =>param=> {
            return state.someLists.filter(() => param.done)
        }
    }
})
使用時,eg:

main.js:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store' //引入狀態管理 store

Vue.config.productionTip = false

new Vue({
  router,
  store,//注冊store(這可以把 store 的實例注入所有的子組件)
  render: h => h(App)
}).$mount('#app')
views/home.vue:

<template>
  <div class="home">
    <!--在前端HTML頁面中使用 count-->
    <HelloWorld :msg="count"/>
    <!--表單處理 雙向綁定 count-->
    <input :value="count" @input="incrementStep">
  </div>
</template>

<script>
    import HelloWorld from '@/components/HelloWorld.vue'
    import {mapActions, mapState,mapGetters} from 'vuex' //注冊 action 和 state

    export default {
        name: 'home',
        computed: {
            //在這里映射 store.state.count,使用方法和 computed 里的其他屬性一樣
            ...mapState([
                'count'
            ]),
            count () {
                return store.state.count
            }
        },
        created() {
            this.incrementStep();
        },
        methods: {
            //在這里引入 action 里的方法,使用方法和 methods 里的其他方法一樣
            ...mapActions([
                'incrementStep'
            ]),
            // 使用對象展開運算符將 getter 混入 computed 對象中
            ...mapGetters([
                'someLists'
                // ...
            ])
        },
        components: {
            HelloWorld
        }
    }
</script>

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM