這篇 Vuex 學習筆記,會大量引用Vuex 官方文章,算是自己學習重點筆記。
心法
狀態管理模式
1 | const Counter = { |
以上包含以下幾個部分:
狀態,驅動應用的數據源;
視圖,以聲明方式將狀態映射到視圖;
操作,響應在視圖上的用戶輸入導致的狀態變化。
vuex
非套件做到的狀態管理
安裝
1 | npm install vuex@next --save |
最簡單的 Store
Vuex 和單純的全局對象有以下兩點不同:
Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那麼相應的組件也會相應地得到高效更新。
你不能直接改變 store 中的狀態。改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation。這樣使得我們可以方便地跟蹤每一個狀態的變化,從而讓我們能夠實現一些工具幫助我們更好地瞭解我們的應用。
簡單來講,處理 store 狀態需要提交(commit) mutation 。(所以外部操作 vuex store 可以修改?)
:::success
mutation 翻譯為變化;浮沉盛衰;變質
參考:mutation - Yahoo奇摩字典 搜尋結果
:::
一個初始 state 對象和一些 mutation:
這邊還滿有趣的,vuex4 和 vuex3 這邊寫法就不一樣了
Main.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import { createApp } from 'vue'
import { createStore } from 'vuex'
import App from './App.vue'
// 創建一個新的 store 實例
const store = createStore({
state () {
return {
count: 0
}
},
mutations: {
increment (state) {
state.count++
}
}
})
// 將 store 實例作為插件安裝
// app.use(store)
createApp(App).use(store).mount('#app')
//測試
store.commit('increment')
console.log(store.state.count) // -> 1
// 證明也是能直接改 state
store.state.count=100
console.log(store.state.count)
之前看到舊版本使用 $this.store
,但看新版本好像很少這樣看到。
為了修復 issue #994,Vuex 4 刪除了 this.$store 在 Vue 組件中的全局類型聲明。當使用 TypeScript 時,必須聲明自己的模塊補充(module augmentation)。
可能是因為要讓 TypeScript 支援原因,可參考:從 3.x 遷移到 4.0 | Vuex。
組件呼叫改 State 狀態1
2
3
4
5
6methods: {
increment() {
this.$store.commit('increment')
console.log(this.$store.state.count)
}
}
GIT: https://github.com/malagege/vuex-test/commit/a05d6cc9bd64e11de6be51a57a1f1e547b637a46
再次強調,我們通過提交 mutation 的方式,而非直接改變 store.state.count,是因為我們想要更明確地追蹤到狀態的變化。這個簡單的約定能夠讓你的意圖更加明顯,這樣你在閱讀代碼的時候能更容易地解讀應用內部的狀態改變。此外,這樣也讓我們有機會去實現一些能記錄每次狀態改變,保存狀態快照的調試工具。有了它,我們甚至可以實現如時間穿梭般的調試體驗。
由於 store 中的狀態是響應式的,在組件中調用 store 中的狀態簡單到僅需要在計算屬性中返回即可。觸發變化也僅僅是在組件的 methods 中提交 mutation。
這邊修改store.state 需要透過 Vue Component 的 method 去做提交(commit) mutation 。
這邊修改store.state 需要透過 Vue Component 的 method 去做提交(commit) mutation 。
這邊修改store.state 需要透過 Vue Component 的 method 去做提交(commit) mutation 。
因為看起來很重要,所以講三遍XD
State
Vuex 使用單一狀態樹——是的,用一個對象就包含了全部的應用層級狀態。至此它便作為一個「唯一數據源 (SSOT )」而存在。
在 Vue 組件中獲得 Vuex 狀態,一般都是用 computed 狀態取得出來。计算属性和侦听器 — Vue.js
1 | // 創建一個 Counter 組件 |
Vuex 通過 Vue 的插件系統將 store 實例從根組件中「注入」到所有的子組件裡。且子組件能通過 this.$store 訪問到。讓我們更新下 Counter 的實現:
1
2
3
4
5
6
7
8 const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
mapState 輔助函數
小記:通常用在 computed 上面
當一個組件需要獲取多個狀態的時候,將這些狀態都聲明為計算屬性會有些重複和冗餘。為瞭解決這個問題,我們可以使用 mapState 輔助函數幫助我們生成計算屬性,讓你少按幾次鍵:
1 | // 在單獨構建的版本中輔助函數為 Vuex.mapState |
(一般)使用
1 | computed: mapState([ |
GIT: https://github.com/malagege/vuex-test/commit/4e9b200cdcca3a043ff9e94c446b2d4900c1e5d0
但一般 computed
不可能全部包給 mapState
。所以我們要可以使用 ES 解構。
1 | computed: { |
GIT: https://github.com/malagege/vuex-test/commit/9c690013d6cc52afc1cd72eb801ac16fc4bf5d1d
小記
1 | console.log(mapState({count:state=>state.count})) |
這邊查看結果,mapState會回傳物件,然後用...
解構回去塞進 computed
。
這邊mapState
有兩個用法。
- 帶 Array
- 帶 物件(Object)
Array,只能帶字串,不能做到客製化。
Object ,可做到多個客製化。
這邊第一次看,會有點混亂,分成兩類看就還好。
回顧 State,簡單來說就是 vuex 的 state (狀態)要在 Compment 取得時候用 computed
去做 mapState 動作。
等等,程式是死的,人是活的,可以在 methods 呼叫?
這當然是可以,但為了程式可用性,最好不要用在別的地方。
好了,當然我用在 methods 上面不能使用。應該跟內部程式有關係。
mapState({count:state=>state.count}).count
也能讀到資料。但正常不會正樣用
Getter
有點像 一般 Component 的 computed
。
###
main.js (官方這邊文件有寫錯…)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos(state){
return state.todos.filter(todo => todo.done)
}
}
})
console.log( store.getters.doneTodos[0].id)
console.log('store.getters.doneTodos :' + store.getters.doneTodos )
GIT: https://github.com/malagege/vuex-test/commit/750360bfa2a97e02aa1c84708a33edfe9718366e
通過屬性訪問
1 | getters: { |
這邊比較特別看第二參數是用getters
,裡面可以用 getters 變數
當然也可以用
1 | computed: { |
通過方法訪問(特別)
1 | getters: { |
GIT: https://github.com/malagege/vuex-test/commit/536f50d2aab41bf5448b6f617f124a8913a808aa
簡單說,Function 裡面回傳 Function 。還滿特別用法。
mapGetters 輔助函數
也是用在computed
。
1 | import { mapGetters } from 'vuex' |
1 | ...mapGetters({ |
Mutation
提交 mutations 是改變 Vuex 中 store 的唯一方式。 mutations 非常類似於組件中的事件(event),每個 mutation 都有一個字串的 事件類型 (type) 和一個回調函數 (handler), handler 就是我們實際進行狀態更改的地方,並且他會接受 state 作為第一個參數
[Vue.js] Vuex 學習筆記 (7) - mutations 的核心概念 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天備份圖
這邊我原本看不懂這句mutations 非常類似於組件中的事件(event),每個 mutation 都有一個字串的 事件類型 (type) 和一個回調函數 (handler)
,後來看到上面文字敘述才了解。
簡單來說,事件類型 (type)
我們常用 JS Event 都是用 trigger
去觸發事件,Vuex 是用 store.commit
,一個回調函數 (handler)
是指 store 裡的 mutations 的 function ,type命名在function 上面。這樣看真的很像 Event。
可參照上面敘述看下面程式:1
2
3
4
5
6
7
8
9
10
11const store = createStore({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
呼叫 mutation 要用
1 | store.commit('increment') |
GIT: https://github.com/malagege/vuex-test/commit/00bd0b93a7ffb3d41a413eabc519cb15a3a2df2f
什麼是 payload
payload意思即為承載量,在開發中則是指出在一堆資料中我們所關心的部分!
google到一篇很好的文章對payload為何這樣叫有很好的解釋,文中指出這個名詞是借用運輸工具上的觀念而來的,例如:卡車、油罐車、貨輪等所謂的載具,然後通常一個載具的總重量一定大於載具的承載量,例如油罐車的總重量包含了他所運載的油量、司機的重量、油罐車行駛所需的油量,但我們所關心僅是油罐車所承載的油量而已。
簡單來說,當作一個data的主體,例如像健保卡一樣,裡面存放資料。
開發中,常見的參數payload是什麼? - Noel Saga
提交載荷(payload)
你可以向 store.commit 傳入額外的參數,即 mutation 的載荷(payload):
1 | // ... |
1 | store.commit('increment', 10) |
GIT: https://github.com/malagege/vuex-test/commit/e04820c2cd0245e0faee9364ecf0aa927c9019bc
在大多數情況下,載荷應該是一個物件,這樣可以包含多個字段並且記錄的 mutation 會更易讀:
1 | // ... |
1 | store.commit('increment', { |
GIT: https://github.com/malagege/vuex-test/commit/d033910dafe268599615e67d374ecb33709cc5dc
commit 使用物件傳值
1 | store.commit({ |
1 | mutations: { |
GIT: https://github.com/malagege/vuex-test/commit/caa624c19d4741924f3abe5c9f6d0f5b93cb34f3
使用常量替代 Mutation 事件類型
使用常量替代 mutation 事件類型在各種 Flux 實現中是很常見的模式。這樣可以使 linter 之類的工具發揮作用,同時把這些常量放在單獨的文件中可以讓你的代碼合作者對整個 app 包含的 mutation 一目瞭然:
1 | // mutation-types.js |
1 | // store.js |
用不用常量取決於你——在需要多人協作的大型項目中,這會很有幫助。但如果你不喜歡,你完全可以不這樣做。
GIT: https://github.com/malagege/vuex-test/commit/00841e0ede5e9473df641eff3646f69dade93256
Mutation 必須是同步函數
一條重要的原則就是要記住 mutation 必須是同步函數。為什麼?請參考下面的例子:
1 | mutations: { |
現在想像,我們正在 debug 一個 app 並且觀察 devtool 中的 mutation 日誌。每一條 mutation 被記錄,devtools 都需要捕捉到前一狀態和後一狀態的快照。然而,在上面的例子中 mutation 中的異步函數中的回調讓這不可能完成:因為當 mutation 觸發的時候,回調函數還沒有被調用,devtools 不知道什麼時候回調函數實際上被調用——實質上任何在回調函數中進行的狀態的改變都是不可追蹤的。
這邊參考範例調整:手把手教你使用Vuex,猴子都能看懂的教程
1 | // 創建一個新的 store 實例 |
非同步測試
1 | mutations: { |
結果:
GIT: https://github.com/malagege/vuex-test/commit/739138bbef8a7ed162456fafbf7a9d5808b4f1d2
同步測試
1 | mutations: { |
結果:
這邊結果來看,官方Mutition
為什麼會關連DevTools
原因所在。
mapMutation
1 | import { mapMutations } from 'vuex' |
用法跟之前差不多。
心得整理
其實當初看的時候一直在想為什麼要分 Mutation
和 Action
,其實不照這樣做,程式也是可以使用的,但是這樣做的話,程式可能不容易看,維護上也不方便。Mutation
觸發是透過store.commit
去操作,在使用上跟 Web 觸發事件(Event)很像。
奇怪?我的程式沒有很複雜動作,一定要經過 Action 嗎?目前我看,應該也可以Component method
,畢竟官方都給了 mapMutation
不用嗎?XD
Action
Action 類似於 mutation,不同在於:
- Action 提交的是 mutation,而不是直接變更狀態(state)。
- Action 可以包含任意異步操作。
1 | const store = createStore({ |
Action 函數接受一個與 store 實例具有相同方法和屬性的 context 對象,因此你可以調用 context.commit 提交一個 mutation,或者通過 context.state 和 context.getters 來獲取 state 和 getters。當我們在之後介紹到 Modules 時,你就知道 context 對象為什麼不是 store 實例本身了。
實踐中,我們會經常用到 ES2015 的參數解構來簡化代碼(特別是我們需要調用 commit 很多次的時候):
lukehoban/es6features: Overview of ECMAScript 6 features
1 | actions: { |
分發 Action
Action 通過 store.dispatch 方法觸發
1 | store.dispatch('increment') |
GIT: https://github.com/malagege/vuex-test/commit/637dfc3482613d86027ee32af369ede218793438
乍一眼看上去感覺多此一舉,我們直接分發 mutation 豈不更方便?實際上並非如此,還記得 mutation 必須同步執行這個限制麼?Action 就不受約束!我們可以在 action 內部執行異步操作:
1 | actions: { |
Actions 支持同樣的載荷(payload)方式和物件方式進行分發:
1 | // 以載荷形式分發 |
GIT: https://github.com/malagege/vuex-test/commit/c0de63b661c0addc1edc6e24da3cb920cd242ab3
來看一個更加實際的購物車示例,涉及到調用異步 API 和分發多重 mutation:
1 | actions: { |
在組件中分發 Action(mapActions)
你在組件中使用
this.$store.dispatch('xxx')
分發 action,或者使用 mapActions 輔助函數將組件的 methods 映射為 store.dispatch 調用(需要先在根節點注入 store):
1 | import { mapActions } from 'vuex' |
組合 Action(重點)
Action 通常是異步的,那麼如何知道 action 什麼時候結束呢?更重要的是,我們如何才能組合多個 action,以處理更加複雜的異步流程?
首先,你需要明白 store.dispatch 可以處理被觸發的 action 的處理函數返回的 Promise,並且 store.dispatch 仍舊返回 Promise:
1 | actions: { |
現在你可以:
1 | store.dispatch('actionA').then(() => { |
在另外一個 action 中也可以:
1 | actions: { |
最後,如果我們利用 async / await,我們可以如下組合 action:
1 | // 假設 getData() 和 getOtherData() 返回的是 Promise |
GIT: https://github.com/malagege/vuex-test/commit/4d639191a71bd674fffc7593a097f44673dcdf63
Mutation / Action 差異
\ | Mutation | Action |
---|---|---|
呼叫 | commit | dispatch |
呼叫Function 第一個參數 | State | Context |
限制 | 不可用非同步 | 可用非同步 |
乍看之下,Mutation 和 Action 真的很像,但是 Mutation 主要修改 State ,所以 Function 第一個參數是 State
。Action 可以做很多事情(取API資料、取 State 資料…),我原本以為取 State 都要在所有 Component 去做,但 Action 應該也能做到??但感覺用外面帶進去會比較好?(符合DI原則),之後有確定答案再補充…
mapMutations,mapActions 會自動映射 payload
實作使用mapMutations, mapActions,發現想帶入 payload ,但mapMutations / mapActions 都可以映射參數。官方文件就有說明。一不小心就漏掉,我真是後知後覺
1 | ...mapActions([ |
mapState,mapGetters,mapMutations,mapActions
這邊我發現 mapXXX,只有 State 是單數,其他都是複數。我覺得我之後可能會忘記,特別說一下。簡單來說,State 只有一個,Getters,Mutations,Actions有很多 function 組成。這樣記應該會比較好。
該加強 Promise / Async
看本篇 Action 範例,有許多 Promse / Async 用法,讓我覺得還可以這樣用!!可能有空要加強一下,希望自己使用不會用到爆炸,哈哈。
Module
看到很多教學沒有交到 Module,因為我覺得 Vuex 文章不多,想說把他看完。除非遇到不會的,例如 v-solt 不會,就先跳過。
為瞭解決以上問題,Vuex 允許我們將 store 分割成模塊(module)。每個模塊擁有自己的 state、mutation、action、getter、甚至是嵌套子模塊——從上至下進行同樣方式的分割:
1 | const moduleA = { |
模塊的局部狀態(使用rootState)
對於模塊內部的 mutation 和 getter,接收的第一個參數是模塊的局部狀態對象。
一般常用第一參數
1 | const moduleA = { |
Action
1 | const moduleA = { |
Getter
1 | const moduleA = { |
命名空間(預設全域)
Getter , Action, Mutation 都是全局宣告的。仔細想想也對,像 JavaScript 也是全域宣告 Event。
Vuex 也有做 namesapce
, 參考官方範例也不難。
觀察幾點記錄一下:
- 這邊我自己看
namespace: true
,只宣告moduole
某屬性上面,不會加在 跟 store 上面,因為在跟 store 沒有甚麼意義。 - module 裡面載入別的 module 會繼承父模塊的命名空間。參考下面官方範例: myPage
- 內層 module 可以重新命名空間。參考下面範例: posts
1 | const store = createStore({ |
啟用了命名空間的 getter 和 action 會收到局部化的 getter,dispatch 和 commit。換言之,你在使用模塊內容(module assets)時不需要在同一模塊內額外添加空間名前綴。更改 namespaced 屬性後不需要修改模塊內的代碼。
下面範例會看到,再進行比較。
在帶命名空間的模塊內訪問全域內容(Global Assets)
如果你希望使用全局 state 和 getter,rootState 和 rootGetters 會作為第三和第四參數傳入 getter,也會通過 context 對象的屬性傳入 action。
若需要在全局命名空間內分發 action 或提交 mutation,將 { root: true } 作為第三參數傳給 dispatch 或 commit 即可。
參考官方範例就會了解。
1 | modules: { |
這邊還有一點比較特別,若需要在全局命名空間內分發 action 或提交 mutation,將 { root: true } 作為第三參數傳給 dispatch 或 commit 即可。
1 | dispatch('someOtherAction') // -> 'foo/someOtherAction' |
在帶命名空間的模塊註冊全局 action
若需要在帶命名空間的模塊註冊全局 action,你可添加 root: true,並將這個 action 的定義放在函數 handler 中。例如:
1 | { |
老實說不太了解這邊能印用在哪邊,但是可以透過子組件宣告全域Action 事件。
帶命名空間的綁定函數
當使用 mapState、mapGetters、mapActions 和 mapMutations 這些函數來綁定帶命名空間的模塊時,寫起來可能比較繁瑣:
1 | computed: { |
}
對於這種情況,你可以將模塊的空間名稱字符串作為第一個參數傳遞給上述函數,這樣所有綁定都會自動將該模塊作為上下文。於是上面的例子可以簡化為:
1 | computed: { |
而且,你可以通過使用 createNamespacedHelpers 創建基於某個命名空間輔助函數。它返回一個對象,對象裡有新的綁定在給定命名空間值上的組件綁定輔助函數:
1 | import { createNamespacedHelpers } from 'vuex' |
後面就先跳過。Module | Vuex
項目結構
做前面其實我大概知道 store 可以拆出來一個 js,但我為了方便測試還是先放 main.js。
官方還很好貼個範例: vuex/examples/classic/shopping-cart at 4.0 · vuejs/vuex
1 | ├── index.html |
組合式API
1 | import { useStore } from 'vuex' |
這邊用法就跟之前差不多,所以就不貼了。可參考:組合式API | Vuex
官方範例:vuex/examples/composition at 4.0 · vuejs/vuex
嚴格模式
1 | const store = createStore({ |
開發環境與發佈環境
1 | const store = createStore({ |
表單處理
1 | <input v-model="message"> |
1 | computed: { |