Vuex入門


title: Vuex入門
toc: true
date: 2018-09-23 22:30:52
categories:html

  • Web

tags:vue

  • Vue
  • Vuex

vuex文檔學習筆記。vuex

安裝

比較經常使用的兩種:shell

直接下載或CDN引用

https://unpkg.com/vuex下載後利用script標籤在vue後引入:npm

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

api

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex@3.0.1/dist/vuex.js"></script>

使用npm

在項目目錄下運行:數組

npm install vuex --save

在模塊化的打包系統中利用這種方法時,必須顯式地利用Vue.use()來安裝Vuex(而script標籤引入後是自動安裝的):promise

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

Vue.use(Vuex)

介紹

功能:把組件的共享狀態抽取出來,用一個全局單例模式管理。緩存

核心:store(倉庫),它包含了應用中的大部分state(狀態,驅動應用的數據源)。bash

這種全局單例模式管理和單純的全局變量的區別:

  • Vuex 的狀態存儲是響應式的。若 store 中的狀態發生變化,那麼相應的組件也會相應地獲得高效更新。
  • 不能直接改變store中的state。改變 store 中的state的惟一途徑就是顯式地commit (提交) mutation(變化)。這樣咱們能夠方便地跟蹤每個狀態的變化。

一個栗子:

// 若是在模塊化構建系統中,請確保在開頭調用了 Vue.use(Vuex)

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

// 觸發狀態變動
store.commit('increment')

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

再次強調,使用提交 mutation ,而不是直接改變 store.state.count

是由於咱們想要更明確地追蹤到狀態的變化。

固然,使用 Vuex 並不意味着須要將全部的狀態放入 Vuex。

雖然將全部的狀態放到 Vuex 會使狀態變化更顯式和易調試,但也會使代碼變得冗長和不直觀。

若是有些狀態嚴格屬於單個組件,最好仍是做爲組件的局部狀態。

你應該根據你的應用開發須要進行權衡和肯定。

核心概念

State

每一個應用只包含一個 store 實例,它包含了全部須要vuex管理的狀態。

利用計算屬性讀取state

從 store 實例中讀取狀態最簡單的方法就是在計算屬性中返回某個狀態:

// 建立一個 Counter 組件
const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return store.state.count
    }
  }
}

但這種模式致使組件依賴全局狀態單例。

註冊 store 選項

爲了解決上述模式致使的組件依賴全局狀態單例的問題,

咱們能夠經過在根實例中註冊 store 選項——

這樣 store 實例會注入到根組件下的全部子組件中,

且子組件能經過 this.$store 訪問到:(需調用 Vue.use(Vuex)

// 根組件
const app = new Vue({
  el: '#app',
  // 把 store 對象提供給 「store」 選項,這能夠把 store 的實例注入全部的子組件
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
})
// 子組件
const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      // 經過 this.$store 訪問store
      return this.$store.state.count
    }
  }
}

mapState輔助函數

當一個組件須要獲取多個狀態的時候,將這些狀態都聲明爲計算屬性會有些重複和冗餘。

爲了解決這個問題,咱們可使用 mapState 輔助函數幫助咱們生成計算屬性,讓你少按幾回鍵:

// 在單獨構建的版本中輔助函數爲 Vuex.mapState
import { mapState } from 'vuex'

export default {
  // ...
  computed: mapState({
    // 箭頭函數可以使代碼更簡練
    // 將 `this.count` 映射爲 `this.$store.state.count
    count: state => state.count,

    // 傳字符串參數 'count' 等同於 `state => state.count`
    countAlias: 'count',

    // 爲了可以使用 `this` 獲取局部狀態,必須使用常規函數
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}

當映射的計算屬性的名稱與 state 的子節點名稱相同時,咱們也能夠給 mapState 傳一個字符串數組。

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

對象展開操做符

ES6引入的新語法,由名字就能夠看出來這個操做符的含義:把對象展開,

來個栗子更容易理解:

var a = {'a':1, 'b':2, 'c':3};
{...a,'d':4} // {a: 1, b: 2, c: 3, d: 4}

var b = [1,2,3];
[...b,4] // [1, 2, 3, 4]

有了這個操做符,咱們就能夠把mapState函數和局部計算屬性混合使用了:

computed: {
  // 局部計算屬性
  localComputed () { /* ... */ },
  // 使用對象展開運算符將此對象展開混入到外部對象中
  ...mapState({
    // ...
  })
}

Getter

Vuex 容許咱們在 store 中定義「getter」(能夠認爲是 store 的計算屬性)。

就像計算屬性同樣,getter 的返回值會根據它的依賴被緩存起來,且只有當它的依賴值發生了改變纔會被從新計算。

Getter 會暴露爲 store.getters 對象,你能夠以屬性的形式訪問這些值;getter 在經過屬性訪問時是做爲 Vue 的響應式系統的一部分緩存其中的。

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

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]

Getter 也能夠接受其餘 getter 做爲第二個參數:

getters: {
  // ...
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
  }
}

store.getters.doneTodosCount // -> 1

你也能夠經過讓 getter 返回一個函數,來實現給 getter 傳參。在你對 store 裏的數組進行查詢時很是有用。

getters: {
  // ...
  // getter 在經過方法訪問時,每次都會去進行調用,而不會緩存結果。
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}

store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

咱們能夠很容易地在任何組件中使用getter:

computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

咱們也可使用mapGetters 輔助函數將 store 中的 getter 映射到局部計算屬性:

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
  // 使用對象展開運算符將 getter 混入 computed 對象中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // 把 `this.doneCount` 映射爲 `this.$store.getters.doneTodosCount`
      doneCount: 'doneTodosCount',
      // ...
    ])
  }
}

Mutation

更改 Vuex 的 store 中的狀態的惟一方法是提交 mutation。

Vuex 中的 mutation 很是相似於事件:每一個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)

這個回調函數就是咱們實際進行狀態更改的地方,而且它會接受 state 做爲第一個參數:

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    // 這裏的事件類型爲 'increment' 
    increment (state) {
      // 變動狀態
      state.count++
    }
  }
})

但咱們不能直接調用一個 mutation 回調函數,就像前面說的,咱們只能提交 mutation。

就像是事件註冊:「當觸發一個類型爲 increment 的 mutation 時,調用此函數。」

要喚醒一個 mutation handler,你須要以相應的 type 調用 store.commit 方法(即提交mutation):

store.commit('increment')

提交載荷Payload

咱們還能夠向 store.commit 傳入額外的參數,即 mutation 的 載荷(payload)

// ...
mutations: {
  increment (state, n) {
    state.count += n
  }
}
store.commit('increment', 10)

在大多數狀況下,載荷應該是一個對象,這樣能夠包含多個字段,而且記錄的 mutation 會更易讀:

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

store.commit('increment', {
  amount: 10
})

咱們可使用對象風格的提交方式,將一個直接包含 type 屬性的對象做爲載荷傳給 mutations :

store.commit({
  type: 'increment',
  amount: 10
})

而且handler 無需改變:

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

Mutation 需遵照 Vue 的響應規則

由於 Vuex 的 store 中的狀態是響應式的,那麼當咱們使用Mutation變動狀態時,監視狀態的 Vue 組件也會自動更新。

所以使用 Vuex 中的 mutation 也須要像使用Vue 同樣遵照一些注意事項:

  1. 提早在 store 中初始化好全部所需屬性。

  2. 當須要在對象上添加新屬性時,你應該

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

    • 以新對象替換老對象。例如利用ES6的對象展開運算符:

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

Mutation 必須是同步函數

mutation 必須是同步函數。爲何?請參考下面的例子:

mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

假設咱們正在 debug 一個 app 而且觀察 devtool 中的 mutation 日誌:

每一條 mutation 被記錄,devtools 都須要捕捉到前一狀態和後一狀態的快照。

然而,在上面的例子中 mutation 中的異步函數中的回調讓這不可能完成:

當 mutation 觸發的時候,回調函數尚未被調用,devtools 不知道何時回調函數實際上被調用——

實質上任何在回調函數中進行的狀態的改變都是不可追蹤的。

在組件中提交 Mutation

咱們能夠在組件中使用 this.$store.commit('xxx') 提交 mutation,

或者使用 mapMutations 輔助函數將組件中的 methods 映射爲 store.commit 調用(須要在根節點注入 store):

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

使用常量替代 Mutation 事件類型

使用常量替代 mutation 事件類型在各類 Flux 實現中是很常見的模式。這樣可使 linter 之類的工具發揮做用,同時把這些常量放在單獨的文件中可讓你的代碼合做者對整個 app 包含的 mutation 一目瞭然:

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = new Vuex.Store({
  state: { ... },
  mutations: {
    // 咱們可使用 ES2015 風格的計算屬性命名功能來使用一個常量做爲函數名
    [SOME_MUTATION] (state) {
      // mutate state
    }
  }
})

用不用常量取決於實際狀況——在須要多人協做的大型項目中,這會頗有幫助。你果真若是不想用,也徹底能夠不用。

Action

Action 相似於 mutation,不一樣在於:

  • Action 是提交 mutation,而不是直接變動狀態。
  • Action 能夠包含任意異步操做。
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      // 變動狀態
      state.count++
    }
  },
  actions: {
    // 接受一個與 store 實例具備相同方法和屬性的 context 對象
    increment (context) {
      // 提交mutation
      context.commit('increment')
    }
  }
})

實踐中,咱們可使用 ES2015 的 參數解構 來簡化代碼(特別是咱們須要調用 commit 不少次的時候):

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

進行異步操做

由於action是提交mutation而不是直接變動狀態,所以咱們就能夠在action內部執行異步操做了:

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

分發Action

Action 經過 store.dispatch 方法觸發:

store.dispatch('increment')

Actions 支持Mutation一樣的載荷方式和對象方式進行分發:

// 以載荷形式分發
store.dispatch('incrementAsync', {
  amount: 10
})

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

也能夠在組件中使用 this.$store.dispatch('xxx') 分發 action,或者使用 mapActions 輔助函數將組件的 methods 映射爲 store.dispatch 調用(須要先在根節點注入 store):

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

store.dispatch 能夠處理被觸發的 action 的處理函數返回的 Promise,

而後返回這個 Promise:

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

如今咱們就能夠:

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

在另一個 action 中也能夠:

actions: {
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}
使用async / await

若是咱們能夠利用 async / await,咱們還能夠以下組合 action:

// 假設 gotData() 和 gotOtherData() 返回的是 Promise

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

一個 store.dispatch 在不一樣模塊中能夠觸發多個 action 函數。在這種狀況下,只有當全部觸發函數完成後,返回的 Promise 纔會執行。

Module

因爲使用單一狀態樹,應用的全部狀態會集中到一個比較大的對象。當應用變得很是複雜時,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 的狀態

模塊的局部狀態

對於模塊內部的 mutation 和 getter,接收的第一個參數是模塊的局部狀態對象

const moduleA = {
  state: { count: 0 },
  mutations: {
    increment (state) {
      // 這裏的 `state` 對象是模塊的局部狀態
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

一樣,對於模塊內部的 action,局部狀態經過 context.state 暴露出來,根節點狀態則爲 context.rootState

const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

對於模塊內部的 getter,根節點狀態也會做爲第三個參數暴露出來:

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

項目結構

Vuex 並不限制咱們的代碼結構。可是,它規定了一些須要遵照的規則:

  1. 應用層級的狀態應該集中到單個 store 對象中。
  2. 提交 mutation 是更改狀態的惟一方法,而且這個過程是同步的。
  3. 異步邏輯都應該封裝到 action 裏面。

若是 store 文件太大,只需將 action、mutation 和 getter 分割到單獨的文件。

對於大型應用,咱們會但願把 Vuex 相關代碼分割到模塊中。下面是項目結構示例:

├── index.html
├── main.js
├── api
│   └── ... # 抽取出API請求
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── index.js          # 咱們組裝模塊並導出 store 的地方
    ├── actions.js        # 根級別的 action
    ├── mutations.js      # 根級別的 mutation
    └── modules
        ├── cart.js       # 購物車模塊
        └── products.js   # 產品模塊

有時候看不進去文檔,一邊總結一邊看就能看進去了 :)

相關文章
相關標籤/搜索