1. 程式人生 > 其它 >vue狀態管理01

vue狀態管理01

開始

【安裝】

npm install vuex --save

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

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

Vue.use(Vuex)

  當使用全域性 script 標籤引用 Vuex 時,不需要以上安裝過程

【概述】

  每一個 Vuex 應用的核心就是 store(倉庫)。“store”基本上就是一個容器,它包含著應用中大部分的狀態 (state)。Vuex 和單純的全域性物件有以下兩點不同:

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

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

【最簡單的store】

  下面來建立一個 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

  下面是一個使用vuex實現的簡單計數器

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment: state => state.count++,
    decrement: state => state.count--,
  }
})
new Vue({
  el: '#app',
  computed: {
    count () {
      return store.state.count
    }
  },
  // view
  template: `
  <div>
    <input type="button" value="-" @click="decrement">
    <span>{{count}}</span>
    <input type="button" value="+" @click="increment">
  </div>
  `,
  // actions
  methods: {
    increment () {
      store.commit('increment')
    },
    decrement () {
      store.commit('decrement')
    },    
  }
})

核心概念

state

【單一狀態樹】

  Vuex 使用單一狀態樹——用一個物件就包含了全部的應用層級狀態。至此它便作為一個“唯一資料來源 (SSOT)”而存在。這也意味著,每個應用將僅僅包含一個 store 例項。單一狀態樹能夠直接地定位任一特定的狀態片段,在除錯的過程中也能輕易地取得整個當前應用狀態的快照

【在VUE元件中獲得VUEX狀態】

  如何在 Vue 元件中展示狀態呢?由於 Vuex 的狀態儲存是響應式的,從 store 例項中讀取狀態最簡單的方法就是在計算屬性中返回某個狀態

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

  每當 store.state.count 變化的時候, 都會重新求取計算屬性,並且觸發更新相關聯的 DOM

  然而,這種模式導致元件依賴全域性狀態單例。在模組化的構建系統中,在每個需要使用 state 的元件中需要頻繁地匯入,並且在測試元件時需要模擬狀態。

  Vuex 通過 store 選項,提供了一種機制將狀態從根元件“注入”到每一個子元件中(需呼叫 Vue.use(Vuex)):

const app = new Vue({
  el: '#app',
  // 把 store 物件提供給 “store” 選項,這可以把 store 的例項注入所有的子元件
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
}

  通過在根例項中註冊 store 選項,該 store 例項會注入到根元件下的所有子元件中,且子元件能通過this.$store訪問到。下面來更新下 Counter 的實現:

const Counter = {
  template: `<div>{{ count }}</div>`,
  computed: {
    count () {
      return this.$store.state.count
    }
  }

【mapState輔助函式】

  當一個元件需要獲取多個狀態時,將這些狀態都宣告為計算屬性會有些重複和冗餘。為了解決這個問題,可以使用mapState輔助函式幫助生成計算屬性

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

export default {
  // ...
  computed: mapState({
    // 箭頭函式可使程式碼更簡練
    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'
])

【物件展開運算子】

  mapState 函式返回的是一個物件。如何將它與區域性計算屬性混合使用呢?通常,需要使用一個工具函式將多個物件合併為一個,將最終物件傳給 computed 屬性。但是自從有了物件展開運算子,可以極大地簡化寫法:

computed: {
  localComputed () { /* ... */ },
  // 使用物件展開運算子將此物件混入到外部物件中
  ...mapState({
    // ...
  })
}

【元件仍然保有區域性狀態】

  使用 Vuex 並不意味著需要將所有的狀態放入 Vuex。雖然將所有的狀態放到 Vuex 會使狀態變化更顯式和易除錯,但也會使程式碼變得冗長和不直觀。如果有些狀態嚴格屬於單個元件,最好還是作為元件的區域性狀態

Getter

  有時候需要從 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)
    }
  }
})

  Getter 會暴露為 store.getters 物件:

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

  Getter 也可以接受其他 getter 作為第二個引數:

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

  可以很容易地在任何元件中使用它:

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

  也可以通過讓 getter 返回一個函式,來實現給 getter 傳參。在對 store 裡的陣列進行查詢時非常有用

getters: {
  // ...
  getTodoById: (state, getters) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

  如果箭頭函式不好理解,翻譯成普通函式如下

var getTodoById = function(state,getters){
  return function(id){
    return state.todos.find(function(todo){
      return todo.id === id
    })
  }
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

【mapGetters輔助函式】

  mapGetters 輔助函式僅僅是將 store 中的 getter 對映到區域性計算屬性:

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
  // 使用物件展開運算子將 getter 混入 computed 物件中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

  如果想將一個 getter 屬性另取一個名字,使用物件形式:

mapGetters({
  // 對映 `this.doneCount` 為 `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 (state) {
      // 變更狀態
      state.count++
    }
  }
})

  不能直接呼叫一個 mutation handler。這個選項更像是事件註冊:“當觸發一個型別為 increment 的 mutation 時,呼叫此函式。”要喚醒一個 mutation handler,需要以相應的 type 呼叫 store.commit 方法:

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

【物件風格的提交方式】

  提交 mutation 的另一種方式是直接使用包含 type 屬性的物件

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

  當使用物件風格的提交方式,整個物件都作為載荷傳給 mutation 函式,因此 handler 保持不變:

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

【遵守響應規則】

  既然 Vuex 的 store 中的狀態是響應式的,那麼當變更狀態時,監視狀態的 Vue 元件也會自動更新。這也意味著 Vuex 中的 mutation 也需要與使用 Vue 一樣遵守一些注意事項:

  1、最好提前在store中初始化好所有所需屬性

  2、當需要在物件上新增新屬性時,應該使用 Vue.set(obj, 'newProp', 123), 或者以新物件替換老物件

  例如,利用物件展開運算子可以這樣寫:

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

【使用常量替代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
    }
  }
})

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

action

  在 mutation 中混合非同步呼叫會導致程式很難除錯。例如,當能呼叫了兩個包含非同步回撥的 mutation 來改變狀態,怎麼知道什麼時候回撥和哪個先回調呢?這就是為什麼要區分這兩個概念。在 Vuex 中,mutation 都是同步事務:

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

  Action類似於mutation,不同之處在於:

  1、Action 提交的是 mutation,而不是直接變更狀態

  2、Action 可以包含任意非同步操作

  下面來註冊一個簡單的action

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