天天看點

Vue狀态管理一、Vuex 介紹二、安裝三、開始四、核心概念五、Vuex應用案例

一、Vuex 介紹

Vuex 是什麼?

​ 試想一下,如果在一個項目開發中頻繁的使用元件傳參的方式來同步

data

中的值,一旦項目變得很龐大,管理和維護這些值将是相當棘手的工作。為此,

Vue

為這些被多個元件頻繁使用的值提供了一個統一管理的工具——

VueX

。在具有

VueX

的Vue項目中,我們隻需要把這些值定義在VueX中,即可在整個Vue項目的元件中使用。

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

​ Vuex 也內建到 Vue 的官方調試工具 devtools extension,提供了諸如零配置的 time-travel 調試、狀态快照導入導出等進階調試功能。

​ 簡單來說: 對 vue 應用中多個元件的共享狀态進行集中式的管理(讀/寫)

​ 1) github 站點: https://github.com/vuejs/vuex

​ 2) 線上文檔: https://vuex.vuejs.org/zh/

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

讓我們從一個簡單的 Vue 計數應用開始:

<template>
  <div>
    {{ count }}
    <button @click="increment">+</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    increment() {
      this.count++;
    },
  },
};
</script>

<style>
</style>
           

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

  • state,驅動應用的資料源;
  • view,以聲明方式将 state 映射到視圖;
  • actions,響應在 view 上的使用者輸入導緻的狀态變化(包含 n 個更新狀态的方法)。

以下是一個表示“單向資料流”理念的簡單示意:

Vue狀态管理一、Vuex 介紹二、安裝三、開始四、核心概念五、Vuex應用案例

多元件共享狀态的問題

但是,當我們的應用遇到多個元件共享狀态時,單向資料流的簡潔性很容易被破壞:

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

對于問題一,傳參的方法對于多層嵌套的元件将會非常繁瑣,并且對于兄弟元件間的狀态傳遞無能為力。

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

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

通過定義和隔離狀态管理中的各種概念并通過強制規則維持視圖和狀态間的獨立性,我們的代碼将會變得更結構化且易維護。

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

如果你想互動式地學習 Vuex,可以看這個 Scrimba 上的 Vuex 課程,它将錄屏和代碼試驗場混合在了一起,你可以随時暫停并嘗試。

App.vue

<template>
  <div>
    {{ count }}
    <button @click="increment">+</button>
    <hr />
    <Child :count="count" :increment="increment"></Child>
  </div>
</template>

<script>
import Child from "./components/Child";
export default {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    increment() {
      this.count++;
    },
  },
  components: { Child },
};
</script>

<style>
</style>
           

Child.vue

<template>
  <div>
    {{ count }}
    <button @click="increment">+</button>
  </div>
</template>

<script>
export default {
  props: ["count", "increment"],
};
</script>

<style >
</style>
           

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

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

如果您不打算開發大型單頁應用,使用 Vuex 可能是繁瑣備援的。确實是如此——如果您的應用夠簡單,您最好不要使用 Vuex。一個簡單的 store 模式就足夠您所需了。但是,如果您需要建構一個中大型單頁應用,您很可能會考慮如何更好地在元件外部管理狀态,Vuex 将會成為自然而然的選擇。

二、安裝

直接下載下傳 / CDN 引用

https://unpkg.com/vuex

Unpkg.com 提供了基于 NPM 的 CDN 連結。以上的連結會一直指向 NPM 上釋出的最新版本。您也可以通過

https://unpkg.com/[email protected]

這樣的方式指定特定的版本。

在 Vue 之後引入

vuex

會進行自動安裝:

<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>
           

NPM

npm install vuex --save

# 或
yarn add vuex
           

在一個子產品化的打包系統中,您必須顯式地通過

Vue.use()

來安裝 Vuex:

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

Vue.use(Vuex)
           

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

Promise

Vuex 依賴 Promise。如果你支援的浏覽器并沒有實作 Promise (比如 IE),那麼你可以使用一個 polyfill 的庫,例如 es6-promise。

你可以通過 CDN 将其引入:

然後

window.Promise

會自動可用。

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

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

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

自己建構

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

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

三、開始

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

  1. Vuex 的狀态存儲是響應式的。當 Vue 元件從 store 中讀取狀态的時候,若 store 中的狀态發生變化,那麼相應的元件也會相應地得到高效更新。
  2. 你不能直接改變 store 中的狀态。改變 store 中的狀态的唯一途徑就是顯式地送出 (commit) mutation。這樣使得我們可以友善地跟蹤每一個狀态的變化,進而讓我們能夠實作一些工具幫助我們更好地了解我們的應用。

簡單的 Store 模式 (不推薦)

安裝 Vuex 之後,讓我們來建立一個 store。建立過程直截了當——僅需要提供一個初始 state 對象和一些 mutation:

初始化store下index.js中的内容

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

Vue.use(Vuex)

const store = new Vuex.Store({
  // 可以把這個看成是data,專門用來存儲資料
  // 如果在元件中想通路這個資料 $store.state.xxx
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

export default store
           

現在,你可以通過

store.state

來擷取狀态對象,以及通過

store.commit

方法觸發狀态變更:

store.commit('increment')

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

為了在 Vue 元件中通路

this.$store

property,你需要為 Vue 執行個體提供建立好的 store。Vuex 提供了一個從根元件向所有子元件,以

store

選項的方式“注入”該 store 的機制:

new Vue({
  el: '#app',
  store: store,
})
           

現在我們可以從元件的方法送出一個變更:

methods: {
  increment() {
    this.$store.commit('increment')
      // 也可以在元件中直接使用$store.state.count
    console.log(this.$store.state.count)
  }
}
           

**再次強調,我們通過送出 mutation 的方式,而非直接改變 store.state.count,是因為我們想要更明确地追蹤到狀态的變化。**這個簡單的約定能夠讓你的意圖更加明顯,這樣你在閱讀代碼的時候能更容易地解讀應用内部的狀态改變。此外,這樣也讓我們有機會去實作一些能記錄每次狀态改變,儲存狀态快照的調試工具。

由于 store 中的狀态是響應式的,在元件中調用 store 中的狀态簡單到僅需要在計算屬性中傳回即可。觸發變化也僅僅是在元件的 methods 中送出 mutation。

不推薦直接操作state中的資料,因為萬一導緻了資料紊亂,不能快速定位到錯誤的原因,因為每個元件都可能有操作資料的方法。
           

簡單計數器

store/index.js

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

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 100
  },
  mutations: {
    increment(state) {
      state.count++
    }
  }
})

export default store
           

App.vue

<template>
  <div>
    {{ $store.state.count }}
    <button @click="increment">+</button>
    <hr />
    <Child></Child>
  </div>
</template>

<script>
import Child from "./components/Child";
export default {
  methods: {
    increment() {
      // this.$store.state.count++;
        this.$store.commit("increment");
    },
  },
  components: {
    Child,
  },
};
</script>

<style>
</style>
           

Child.vue

<template>
  <div>
    {{ $store.state.count }}
    <button @click="increment">+</button>
  </div>
</template>

<script>
export default {
  methods: {
    increment() {
      this.$store.commit("increment");
    },
  },
};
</script>

<style >
</style>
           

main.js

/*
入口JS
 */
import Vue from 'vue'
import App from './App.vue'
import store from './store/index'

/* eslint-disable no-new */
new Vue({
  el: '#app',
  components: {
    App
  }, // 映射元件标簽
  template: '<App/>', // 指定需要渲染到頁面的模闆
  store
})

           

四、核心概念

State

Vuex 使用單一狀态樹——是的,用一個對象就包含了全部的應用層級狀态。至此它便作為一個“唯一資料源 ”而存在。這也意味着,每個應用将僅僅包含一個 store 執行個體。單一狀态樹讓我們能夠直接地定位任一特定的狀态片段,在調試的過程中也能輕易地取得整個目前應用狀态的快照。

單狀态樹和子產品化并不沖突——在後面的章節裡我們會讨論如何将狀态和狀态變更事件分布到各個子子產品中。

  1. vuex 管理的狀态對象,可以把這個看成是data,專門用來存儲資料
  2. 它應該是唯一的
  3. 如果在元件中想通路這個資料 $store.state.xxx
const state = { 
    xxx: initValue 
}
           

Mutations

如果要操作store中的state值,隻能通過調用mutations提供的方法,才能操作對應的資料,不推薦直接操作state中的資料,因為萬一導緻了資料紊亂,不能快速定位到錯誤的原因,因為每個元件都可能有操作資料的方法。

更改 Vuex 的 store 中的狀态的唯一方法是送出 mutation。Vuex 中的 mutation 非常類似于事件:每個 mutation 都有一個字元串的 事件類型 (type) 和 一個 回調函數 (handler)。這個回調函數就是我們實際進行狀态更改的地方,并且它會接受 state 作為第一個參數:

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    // 如果想要調用mutation中的方法,隻能使用this.$store.commit("方法名")
    increment (state) {
      // 變更狀态
      state.count++
    }
  }
})
           

你不能直接調用一個 mutation handler。這個選項更像是事件注冊:“當觸發一個類型為

increment

的 mutation 時,調用此函數。”要喚醒一個 mutation handler,你需要以相應的 type 調用 store.commit 方法:

  1. 包含多個直接更新 state 的方法(回調函數)的對象
  2. 誰來觸發: action 中的 commit(‘mutation 名稱’)
  3. 隻能包含同步的代碼, 不能寫異步代碼
const mutations = { 
    yyy (state, {data1}) {
        // 更新 state 的某個屬性
    } 
}
           

注意:在mutations中的方法,最多傳遞兩個參數。其中參數1:是state狀态,參數2:通過commit送出過來的參數。如果傳遞多個參數,可以傳遞一個對象。例如

{a:1,b:3}

mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}
           

需要注意的是,在mutations中必須是同步的函數。在 mutation 中混合異步調用會導緻你的程序很難調試。例如,當你調用了兩個包含異步回調的 mutation 來改變狀态,你怎麼知道什麼時候回調和哪個先回調呢?這就是為什麼我們要區分這兩個概念。在 Vuex 中,mutation 都是同步事務:

store.commit('increment')
// 任何由 "increment" 導緻的狀态變更都應該在此刻完成。

           

Actions

Action 類似于 mutation,不同在于:

  • Action 送出的是 mutation,而不是直接變更狀态。
  • Action 可以包含任意異步操作。

讓我們來注冊一個簡單的 action:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
           

Action 函數接受一個與 store 執行個體具有相同方法和屬性的 context 對象,是以你可以調用

context.commit

送出一個 mutation,或者通過

context.state

context.getters

來擷取 state 和 getters。當我們在之後介紹到 Modules 時,你就知道 context 對象為什麼不是 store 執行個體本身了。

實踐中,我們會經常用到 ES2015 的 參數解構 來簡化代碼(特别是我們需要調用

commit

很多次的時候):

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}
           
  1. 包含多個事件回調函數的對象
  2. 通過執行: commit()來觸發 mutation 的調用, 間接更新 state
  3. 誰來觸發: 元件中: $store.dispatch(‘action 名稱’, data1) // ‘zzz’
  4. 可以包含異步代碼(定時器, ajax)
const actions = { 
    zzz ({commit, state}, data1) { 
        commit('yyy', {data1}) 
    } 
}
           

分發 Action

Action 通過 store.dispatch 方法觸發:

乍一眼看上去感覺多此一舉,我們直接分發 mutation 豈不更友善?實際上并非如此,還記得 mutation 必須同步執行這個限制麼?Action 就不受限制!我們可以在 action 内部執行異步操作:

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}
           

調用

export default {
  methods: {
    increment() {
      console.log("dispatch");
      this.$store.dispatch("increment");
    },
  },
};
           

Getters

getters隻負責對外提供資料,不負責修改資料。如果想要修改state中的資料,請去找mutations。

有時候我們需要從 store 中的 state 中派生出一些狀态,例如對清單進行過濾并計數:

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

如果有多個元件需要用到此屬性,我們要麼複制這個函數,或者抽取到一個共享函數然後在多處導入它——無論哪種方式都不是很理想。

Vuex 允許我們在 store 中定義“getter”(可以認為是 store 的計算屬性)。就像計算屬性一樣,getter 的傳回值會根據它的依賴被緩存起來,且隻有當它的依賴值發生了改變才會被重新計算。

Getter 接受 state 作為其第一個參數:

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)
    }
  }
})
           
  1. 包含多個計算屬性(get)的對象
  2. 誰來讀取: 元件中: $store.getters.xxx
const getters = { 
    mmm (state) { 
        return ...
    } 
}
           

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 的狀态
           
  1. 包含多個 module
  2. 一個 module 是一個 store 的配置對象
  3. 與一個元件(包含有共享資料)對應

Vuex結構圖

官方結構圖:

Vue狀态管理一、Vuex 介紹二、安裝三、開始四、核心概念五、Vuex應用案例

結構圖詳解:

Vue狀态管理一、Vuex 介紹二、安裝三、開始四、核心概念五、Vuex應用案例
Vuex提供了mapState、MapGetters、MapActions、mapMutations等輔助函數給開發在vm中處理store。
           

五、Vuex應用案例

改版Vue元件化實作的評論頁面;所有關于操作comments資料的元件通信都改為Vuex來實作;

目錄結構

src
	|- components
		|- Add.vue
		|- Item.vue
		|- List.vue
	|- store
		|- actions.js
		|- getters.js
		|- index.js
		|- mutations.js
		|- state.js
		|- types.js
	App.vue
	main.js
           

狀态管理實作

index.js

/*
    Vuex核心管理子產品store對象
*/
import Vue from 'vue'
import Vuex from 'vuex'

import state from './state'
import mutations from './mutations'
import actions from './actions'
import getters from './getters'

Vue.use(Vuex)

export default new Vuex.Store({
  state,
  mutations,
  actions,
  getters
})
           

actions.js

/*
    包含多個用于間接更新狀态的方法的對象子產品
*/
import { ADD_COMMENT, DELETE_COMMENT } from './types'

export default {

  addComment ({commit}, comment) {
    // 送出一個comutation請求
    commit(ADD_COMMENT, {comment}) // 傳遞給mutation的是一個包含資料的對象
  },

  deleteComment ({commit}, index) {
    commit(DELETE_COMMENT, {index})
  }
}
           

types.js

/* 
    包含多個 mutation name  常量
*/
export const ADD_COMMENT = 'add_comment'
export const DELETE_COMMENT = 'delete_comment'
           

mutations.js

/* 
    包含多個用于直接更新狀态的方法的對象子產品
*/
import { ADD_COMMENT, DELETE_COMMENT } from './types'
export default {
  [ADD_COMMENT](state, {comment}) {
    state.comments.unshift(comment)
  },
  [DELETE_COMMENT](state, {index}) {
    state.comments.splice(index, 1)
  },
}
           

state.js

/*
    狀态對象子產品
*/
export default {
  comments: [{
      name: "張三",
      content: "Vue 真好用",
    },
    {
      name: "李四",
      content: "Vue 真難啊",
    },
  ]
}
           

getters.js

/*
    包含多個基于state的getter計算屬性方法的對象子產品
*/
export default {
  // 總數量
  totalSize(state) {
    return state.commonts.length
  }
}
           

元件實作

App.vue

<template>
  <div id="app">
    <div>
      <header class="site-header jumbotron">
        <div class="container">
          <div class="row">
            <div class="col-xs-12">
              <h1>請發表對Vue的評論</h1>
            </div>
          </div>
        </div>
      </header>
      <div class="container">
        <Add/>
        <List/>
      </div>
    </div>
  </div>
</template>

<script>
// App元件中引入Add和List
import Add from "./components/Add";
import List from "./components/List";

export default {
  // 映射元件标簽
  components: {
    Add,
    List,
  },
};
</script>

<style>
</style>

           

List.vue

<template>
  <div class="col-md-8">
    <h3 class="reply">評論回複:( {{ totalSize }} )</h3>
    <h2 v-show="comments.length == 0">暫無評論,點選左側添加評論!!!</h2>
    <ul class="list-group">
      <!-- 周遊comments,并且把每個comments的資料傳遞給子元件Item  -->
      <Item
        v-for="(comment, index) in comments"
        :key="index"
        :comment="comment"
        :index="index"
      />
    </ul>
  </div>
</template>

<script>
// 在List元件引入Item
import Item from "./Item";
import { mapGetters } from "vuex";
import { mapState } from "vuex";
export default {
  components: {
    Item,
  },
  // 我們一般要擷取Vuex管理的資料,推薦在元件中使用計算屬性;
  // 當然我們也完全可以使用$store.state.comments
  computed: {
    // 下面的計算屬性可以簡寫為:
    /* 
      ...mapState(['comments'])
    */
    comments() {
      return this.$store.state.comments;
    },
    // 下面的計算屬性可以簡寫為:
    /* 
      ...mapGetters(['totalSize']),
    */
    totalSize() {
      return this.$store.getters.totalSize;
    },
  },
};
</script>

<style>
.reply {
  margin-top: 0px;
}
</style>
           

Add.vue

<template>
  <div class="col-md-4">
    <form class="form-horizontal">
      <div class="form-group">
        <label>使用者名</label>
        <!-- 雙向資料綁定 -->
        <input
          type="text"
          class="form-control"
          placeholder="使用者名"
          v-model="name"
        />
      </div>
      <div class="form-group">
        <label>評論内容</label>
        <!-- 雙向資料綁定 -->
        <textarea
          class="form-control"
          rows="6"
          placeholder="評論内容"
          v-model="content"
        ></textarea>
      </div>
      <div class="form-group">
        <div class="col-sm-offset-2 col-sm-10">
          <!-- 綁定事件 -->
          <button type="button" class="btn btn-default pull-right" @click="add">
            送出
          </button>
        </div>
      </div>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      name: "",
      content: "",
    };
  },
  methods: {
    add() {
      // 1.檢查輸入的合法性
      const { name, content } = this;
      // 姓名校驗
      let name_reg = /^[\u4e00-\u9fa5]{2,4}$/;
      if (!name_reg.test(name)) {
        alert("請輸入2-4位中文姓名");
        return;
      }
      if (content.length == "") {
        alert("評論不能為空");
        return;
      }

      // 2.根據輸入的資料封裝成一個comment對象
      const comment = {
        name,
        content,
      };

      // 3.添加到comments中去,子向父傳遞資料
      this.$store.dispatch("addComment", comment);

      // 4.清空資料
      this.name = "";
      this.content = "";
    },
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
           

Item.vue

<template>
  <li class="list-group-item">
    <div class="handle">
      <a href="javascript:;" target="_blank" rel="external nofollow"  @click="deleteItem">删除</a>
    </div>
    <p class="user">
      <span>{{ comment.name }}</span
      ><span>說:</span>
    </p>
    <p class="centence">{{ comment.content }}</p>
  </li>
</template>

<script>
export default {
  props: ["comment", "index"],
  methods: {
    deleteItem() {
      const { comment } = this;
      let tf = window.confirm(`确定删除${comment.name}的評論嗎?`);
      if (tf) {
        // 觸發actions裡面的deleteComment
        this.$store.dispatch("deleteComment");
      }
    },
  },
};
</script>

<style>
li {
  transition: 0.5s;
  overflow: hidden;
}

.handle {
  width: 40px;
  border: 1px solid #ccc;
  background: #fff;
  position: absolute;
  right: 10px;
  top: 1px;
  text-align: center;
}

.handle a {
  display: block;
  text-decoration: none;
}

.list-group-item .centence {
  padding: 0px 50px;
}

.user {
  font-size: 22px;
}
</style>

           

render函數

類型:(createElement: () => VNode) => VNode

字元串模闆的代替方案,允許你發揮 JavaScript 最大的程式設計能力。該渲染函數接收一個 createElement 方法作為第一個參數用來建立 VNode。

如果元件是一個函數元件,渲染函數還會接收一個額外的 context 參數,為沒有執行個體的函數元件提供上下文資訊。
           
new Vue({
  // 挂載在index.html的#app元素上
  el: '#app',
  store,
  render: h => h(App)
})