Vue官網知識點整理

1.每一個 Vuex 應用的核心就是 store(倉庫)。“store”基本上就是一個容器,它包含着你的應用中大部分的狀態 (state)

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

改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation

Vuex 使用單一狀態樹——是的,用一個對象就包含了全部的應用層級狀態。至此它便作爲一個“唯一數據源 (SSOT)”而存在。這也意味着,每個應用將僅僅包含一個 store 實例

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

由於 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)):

最簡單的 Store

提示: 我們將在後續的文檔示例代碼中使用 ES2015 語法。如果你還沒能掌握 ES2015,你得抓緊了

安裝 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

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

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 輔助函數

在 Scrimba 上嘗試這節課

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

// 在單獨構建的版本中輔助函數爲 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 屬性。但是自從有了對象展開運算符(現處於 ECMAScript 提案 stage-4 階段),我們可以極大地簡化寫法:

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

我們可以很容易地在任何組件中使用它:this.$store.getters.doneTodosCount

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

注意,getter 在通過屬性訪問時是作爲 Vue 的響應式系統的一部分緩存其中的。

#通過方法訪問

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

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

注意,getter 在通過方法訪問時,每次都會去進行調用,而不會緩存結果

#mapGetters 輔助函數

mapGetters 輔助函數僅僅是將 store 中的 getter 映射到局部計算屬性:

import { mapGetters } from 'vuex'

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

如果你想將一個 getter 屬性另取一個名字,使用對象形式:

mapGetters({
  // 把 `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 (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
  }
}

#Mutation 需遵守 Vue 的響應規則

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

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

  2. 當需要在對象上添加新屬性時,你應該使用 Vue.set(obj, 'newProp', 123), 或者以新對象替換老對象。例如,利用 stage-3 的對象展開運算符我們可以這樣寫:

  • 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

Action

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

#分發 Action

Action 通過 store.dispatch 方法觸發:

store.dispatch('increment')

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

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

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

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

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

來看一個更加實際的購物車示例,涉及到調用異步 API 和分發多重 mutation

actions: {
  checkout ({ commit, state }, products) {
    // 把當前購物車的物品備份起來
    const savedCartItems = [...state.cart.added]
    // 發出結賬請求,然後樂觀地清空購物車
    commit(types.CHECKOUT_REQUEST)
    // 購物 API 接受一個成功回調和一個失敗回調
    shop.buyProducts(
      products,
      // 成功操作
      () => commit(types.CHECKOUT_SUCCESS),
      // 失敗操作
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

注意我們正在進行一系列的異步操作,並且通過提交 mutation 來記錄 action 產生的副作用(即狀態變更)。

#在組件中分發 Action

你在組件中使用 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,以處理更加複雜的異步流程?

首先,你需要明白 store.dispatch 可以處理被觸發的 action 的處理函數返回的 Promise,並且 store.dispatch 仍舊返回 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,我們可以如下組合 action:

// 假設 getData() 和 getOtherData() 返回的是 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

在 Scrimba 上嘗試這節課

由於使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象。當應用變得非常複雜時,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
    }
  }
}

#命名空間

默認情況下,模塊內部的 action、mutation 和 getter 是註冊在全局命名空間的——這樣使得多個模塊能夠對同一 mutation 或 action 作出響應。

如果希望你的模塊具有更高的封裝度和複用性,你可以通過添加 namespaced: true 的方式使其成爲帶命名空間的模塊。當模塊被註冊後,它的所有 getter、action 及 mutation 都會自動根據模塊註冊的路徑調整命名。例如:

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章