Vuex是什麼?
Vuex 是一個專為 Vue.js應用程式開發的狀態管理模式。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。Vuex 也整合到Vue 的官方除錯工具 devtools extension,提供了諸如零配置的 time-travel除錯、狀態快照匯入匯出等高階除錯功能。
反正我是懵懵的,沒太看懂他有什麼用。 (~ ̄▽ ̄)~
什麼是“狀態管理模式”?
讓我們從一個簡單的 Vue 計數應用開始:
new Vue({
// state
data () {
return {
count: 0
}
},
// view
template: `<div>{{ count }}</div>`,
// actions
methods: {
increment () {
this.count++
} } })
這個狀態自管理應用包含以下幾個部分:
state,驅動應用的資料來源; view,以宣告方式將state對映到檢視; actions,響應在view上的使用者輸入導致的狀態變化。
我的理解是 state狀態就是資料來源,通常用data,view是檢視層不用說,通常用template,action就是方法進行一些資料操作,通常用methods。
當我們的應用遇到多個元件共享狀態時,單向資料流的簡潔性很容易被破壞:
-多個檢視依賴於同一狀態。
-來自不同檢視的行為需要變更同一狀態。
應該是 很多檢視層檔案都是同一個資料來源,不同檢視的操作需要改為同一個資料來源。
對於問題一,傳參的方法對於多層巢狀的元件將會非常繁瑣,並且對於兄弟元件間的狀態傳遞無能為力。對於問題二,我們經常會採用父子元件直接引用或者通過事件來變更和同步狀態的多份拷貝。以上的這些模式非常脆弱,通常會導致無法維護的程式碼。
因此,我們為什麼不把元件的共享狀態抽取出來,以一個全域性單例模式管理呢?在這種模式下,我們的元件樹構成了一個巨大的“檢視”,不管在樹的哪個位置,任何元件都能獲取狀態或者出發行為!
另外,通過定義和隔離狀態管理中的各種概念並強制遵守一定的規則,我們的程式碼將會變得更結構化且易維護。
這就是 Vuex 背後的基本思想,借鑑了 Flux、Redux、和 The Elm Architecture。與其他模式不同的是,Vuex
是專門為 Vue.js 設計的狀態管理庫,以利用 Vue.js 的細粒度資料響應機制來進行高效的狀態更新。
也就是遇到上述問題怎麼辦呢,Vuex將資料來源打包了,然後要操作的時候,都從這個資料來源來操作,不會有那種繼承父元件,多層元件巢狀導致不利於維護的情況。
什麼情況下我應該使用 Vuex?
雖然 Vuex 可以幫助我們管理共享狀態,但也附帶了更多的概念和框架。這需要對短期和長期效益進行權衡。
如果您不打算開發大型單頁應用,使用 Vuex 可能是繁瑣冗餘的。確實是如此——如果您的應用夠簡單,您最好不要使用 Vuex。一個簡單的
global event bus
就足夠您所需了。但是,如果您需要構建是一箇中大型單頁應用,您很可能會考慮如何更好地在元件外部管理狀態,Vuex 將會成為自然而然的選擇。引用
Redux 的作者 Dan Abramov 的話說就是:
Flux 架構就像眼鏡:您自會知道什麼時候需要它。
其實我真的覺得這段話說了等於沒說。。。 ㄟ( ▔, ▔ )ㄏ
每一個 Vuex 應用的核心就是 store(倉庫)。”store” 基本上就是一個容器,它包含著你的應用中大部分的狀態(即state)。Vuex 和單純的全域性物件有以下兩點不同:
1.Vuex 的狀態儲存是響應式的。當 Vue 元件從 store 中讀取狀態的時候,若 store中的狀態發生變化,那麼相應的元件也會相應地得到高效更新。
2.你不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地提交mutations。這樣使得我們可以方便地跟蹤每一個狀態的變化,從而讓我們能夠實現一些工具幫助我們更好地瞭解我們的應用。
1大概也就是資料繫結,同步更新資料,2不是很懂,mutations是什麼東西?
最簡單的 Store
提示:我們將在後續的文件示例程式碼中使用 ES2015 語法。如果你還沒能掌握 ES2015,你得抓緊了!
安裝 Vuex 之後,讓我們來建立一個 store。建立過程直截了當——僅需要提供一個初始 state 物件和一些 mutations:
// 如果在模組化構建系統中,請確保在開頭呼叫了 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 中提交 mutations。
這是一個最基本的 Vuex 記數應用示例。
接下來,我們將會更深入地探討一些核心概念。讓我們先從 State 概念開始。
反正每次對資料物件進行操作的時候,都需要進行commit,好處就是他上面說的,但是現在不在專案中,肯定體會不到好處,就像redux作者說的“您自會知道什麼時候需要它。”
state
單一狀態樹
Vuex 使用 單一狀態樹 ——
是的,用一個物件就包含了全部的應用層級狀態。至此它便作為一個『唯一資料來源(SSOT)』而存在。這也意味著,每個應用將僅僅包含一個 store
例項。單一狀態樹讓我們能夠直接地定位任一特定的狀態片段,在除錯的過程中也能輕易地取得整個當前應用狀態的快照。
單狀態樹和模組化並不衝突 —— 在後面的章節裡我們會討論如何將狀態和狀態變更事件分佈到各個子模組中。
每個應用將僅僅包含一個 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
}
}
}
通過一種類似虛擬主機的機制,將store注入到每個子元件,然後子元件就能操作store了,還是有點像繼承。
mapState 輔助函式
當一個元件需要獲取多個狀態時候,將這些狀態都宣告為計算屬性會有些重複和冗餘。為了解決這個問題,我們可以使用 mapState
輔助函式幫助我們生成計算屬性
這個我看的不是很懂,只知道他是輔助計算屬性的。
mapState函式返回的是一個物件。我們如何將它與區域性計算屬性混合使用呢?通常,我們需要使用一個工具函式將多個物件合併為一個,以使我們可以將最終物件傳給 computed 屬性。但是自從有了物件展開運算子(現處於 ECMASCript 提案 stage-3 階段),我們可以極大地簡化寫法。
上面的沒懂,那這個等等再說。
Getters
有時候我們需要從 store 中的 state 中派生出一些狀態,例如對列表進行過濾並計數:
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
如果有多個元件需要用到此屬性,我們要麼複製這個函式,或者抽取到一個共享函式然後在多處匯入它 —— 無論哪種方式都不是很理想。
Vuex 允許我們在 store 中定義『getters』(可以認為是 store 的計算屬性)。Getters 接受 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)
}
}
})
Getters 會暴露為 store.getters 物件:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getters 也可以接受其他 getters 作為第二個引數:
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1
我們可以很容易地在任何元件中使用它:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
getters可以認為是 store 的計算屬性,也就是多次呼叫,所以將其封裝成getters,方便呼叫。
mapGetters 輔助函式
mapGetters 輔助函式僅僅是將 store 中的 getters 對映到區域性計算屬性:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用物件展開運算子將 getters 混入 computed 物件中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
如果你想將一個 getter 屬性另取一個名字,使用物件形式:
mapGetters({
// 對映 this.doneCount 為 store.getters.doneTodosCount
doneCount: 'doneTodosCount'
})
和上面的state一樣,沒太明白。不著急。
Mutations
更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation。Vuex 中的 mutations 非常類似於事件:每個 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')
我到現在還是沒有明白 mutations是幹什麼的,具體有什麼用。只知道要操作state就是要commit mutations。
提交載荷(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
})
不知道載荷用的多不多,感覺還挺多的,荷載就是 commit額外的引數。
物件風格的提交方式
提交 mutation 的另一種方式是直接使用包含 type 屬性的物件:
store.commit({
type: 'increment',
amount: 10
})
當使用物件風格的提交方式,整個物件都作為載荷傳給 mutation 函式,因此 handler 保持不變:
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
感覺這種風格更適用於vue,更像vue的寫法
Mutations 需遵守 Vue 的響應規則
既然 Vuex 的 store 中的狀態是響應式的,那麼當我們變更狀態時,監視狀態的 Vue 元件也會自動更新。這也意味著 Vuex 中的 mutation 也需要與使用 Vue 一樣遵守一些注意事項:
最好提前在你的 store 中初始化好所有所需屬性。
當需要在物件上新增新屬性時,你應該使用 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 不知道什麼時候回撥函式實際上被呼叫 —— 實質上任何在回撥函式中進行的的狀態的改變都是不可追蹤的。
這個如果不是同步函式的話,就顯示不了回撥函式的狀態日誌了,可以這樣理解嗎?
在元件中提交 Mutations
你可以在元件中使用 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({
add: 'increment' // 對映 this.add() 為 this.$store.commit('increment')
})
}
}
這個倒是能看懂
下一步:Actions
在 mutation 中混合非同步呼叫會導致你的程式很難除錯。例如,當你能呼叫了兩個包含非同步回撥的 mutation 來改變狀態,你怎麼知道什麼時候回撥和哪個先回撥呢?這就是為什麼我們要區分這兩個概念。在 Vuex 中,mutation 都是同步事務:
store.commit('increment')
// 任何由 "increment" 導致的狀態變更都應該在此刻完成。
為了處理非同步操作,讓我們來看一看 Actions。