一.出發點
在相對獨立的元件中,action -> state -> view 的單向資料流能得到保證。而真實業務場景經常需要狀態傳遞及共享,一般方法是:
-
狀態傳遞:父子元件通訊透過
props完成(正向傳屬性值,反向傳方法),對於兄弟元件間通訊,則需要透過事件或者把狀態提升到父級(把兄弟通訊問題轉換成父子通訊)來完成 -
狀態共享:要麼放在一個元件裡,其他元件想辦法拿到狀態參照,要麼提出來作為單例,供各元件共享
深層次的 props 傳遞比較難受,兄弟元件間的交錯的事件通訊會帶來維護上的問題,提升狀態到父級會讓父級膨脹,管理過多細節狀態。把共享狀態放在一個元件裡,其他元件取狀態參照比較費勁,提出來作為單例稍好一些,但元件樹外存在零散的共享狀態,也可能會帶來維護上的問題
把狀態層單獨提出來,能有效解決狀態傳遞和共享的問題,再用 action 給狀態變更添上語意,不僅緩解了維護上的問題,還帶來了除錯方面的好處
二.基本原則
-
應用級的狀態由
store集中管理 -
修改狀態的唯一方式是
commit同步的mutation -
非同步邏輯放在
action裡
認同便於管理的單一狀態樹、規範修改狀態的方式,此外更貼近業務,從設計上考慮非同步場景
三.結構
不像 Redux 一樣奇怪(reducer 乍看好像和 Flux 沒什麼關係),Vuex 更像是中規中矩的 Flux 實作:
component 视图层 dispatch action
---
action 事件层 commit mutation
异步 统一管理异步请求
---
mutation 响应层 mutate state
同步 逻辑上原子级的状态修改
---
state 数据模型层 update model
通过 数据绑定 映射到视图更新
其中,mutation, action 都是全域共享的,所以也解決了元件通訊的問題(不需要手動傳遞狀態,只需要告訴 store 發生了什麼,store 知道該做什麼),避免提升/傳遞狀態,並帶來了語意上的好處
全域共享就存在命名衝突的問題,所以 Vuex 還提供了命名空間選項
對比 Flux
产生action 传递action update state
view交互 -----------> dispatcher -----------> stores --------------> views
可以發現最大的區別是 Vuex 把 action 細分成了 action 和 mutation,分別應對非同步場景和同步場景,由 store 自身充當 dispatcher(負責註冊/分發 action/(mutation))
也就是說,把 action, mutation 看作一層(Flux 裡的 action)的話,二者結構完全一致,所以說 Vuex 更像是中規中矩的 Flux 實作
store
作為 state 的容器,另外充當 dispatcher
用 store 來管理 state,從作用上看相當於 global.share = {},但 Vuex 裡的 store.state 有一些別的特點:
-
state是響應式資料 -
不允許直接修改
store持有的state,必須顯式的commit mutation
與元件的 data 類似,store.state 也是響應式的,與元件的計算屬性關聯起來,state 更新精確傳遞到 view 層
而不允許直接修改 store.state 也是道德約束,雖然在開啟 strict 選項後會報錯,而實際上修改是可以生效的,這裡不做強約束(防寫)可能是出於市場考慮
另外:
-
單一狀態樹,與 Redux 相同,提供額外模組化機制來管理(拆分/組織
state) -
同樣,不要求把所有
state全都塞進 Vuex,建議把相對獨立的維護在元件級
getter
作用上相當於 store 的計算屬性
用來包裝 state,把原始 state 包裝(對 store.state 做簡單計算,比如 filter, count, find 等等)成視圖展示需要的形式
沒有 getter 的話,這部分弱邏輯要麼放在 computed 裡,要麼放在範本裡,提供 getter 把 state 相關的所有東西都抽離出去
mutation
負責更新 state,mutation 都是同步操作,commit mutation 下一行 state 就更新完了
預先註冊在 store 中,每次 commit 時查 mutation 表,執行對應的 state 更新函式
注意,要求 mutation 必須是同步的,否則除錯工具拿不到正確的狀態快照(如果非同步修改狀態的話),會破壞狀態追蹤
action
用來應對非同步場景,作為 mutation 的補充
Vuex 相當於把 Flux 裡的 action 按同步非同步分為 mutation 和 action
action 不像 mutation 一樣直接修改 state,而是透過 commit mutation 來間接修改,也就是說只有 mutation 對應原子級的狀態更新操作
action 裡可以有非同步操作,設計上故意把非同步作為 action 和同步的 mutation 分開
非同步流程控制
非同步流程控制可以透過讓 action 返回 promise 來解決,比傳入回呼函式優雅一些
Vuex v2.x(目前 2017/7/1 最新 v2.3.0)的 store.dispatch 預設返回 promise,非 promise 的 action 返回值會經 Promise.resolve() 包裝成 promise
dispatch(type: string, payload?: any, options?: Object) | dispatch(action: Object, options?: Object)
Dispatch an action. options can have root: true that allows to dispatch root actions in namespaced modules. Returns a Promise that resolves all triggered action handlers.
(摘自 API Reference)
但對於非同步操作沒有意義(Promise.resolve(undefined)),需要控制非同步流程的話,還是應該手動返回 promise,並把需要的資訊從內層 promise 傳遞出來
module
模組化機制,用來拆分組織 store
提供 namespaced 選項,註冊時把模組路徑作為前綴。很精緻的設計,透過向模組注入 local.dispatch/commit/getters/state 來抹平命名空間的影響,模組內不用帶命名空間,模組外(業務或者其他模組)需要帶命名空間。這樣命名空間就變成了一個開關選項,對 store 部分沒有任何影響
四.工具
同樣,Vuex 也需要處理 state -> view 的部分(作用類似於 react-redux,把狀態管理層接入視圖層)
支援精確資料繫結的 Vue 不用像 React 那麼麻煩(往虛擬 DOM 樹上插一些 container,把 store.state 的變化傳播下去),只需要把 store.state 和元件狀態連接起來就行,像軟連結一樣,元件與 store 共享 state 物件,state 的變化透過響應式特性傳遞給元件,視圖得到更新
mapState
把 store.state 和元件的 computed 連接起來
注意:mapState 能夠強制禁止在元件裡直接修改 computed 影響實際狀態(透過 mapState 生成的計算屬性是唯讀的)
{
configurable: true,
enumerable: true,
get: function computedGetter(),
set: function noop()
}
mapGetters
把 store.getter 和元件的 computed 連接起來
與 mapState 類似,也有防寫
mapMutations
把 mutation 和元件的 methods 連接起來
簡化元件 commit mutation 的過程(需要在頂層注入 store)
mapActions
把 action 和元件的 methods 連接起來
簡化 dispatch action 的過程(同樣需要注入 store)
五.疑問
1.怎樣避免相同元件共享狀態?
比如 list 裡有 3 個相同元件,怎樣避免共享 state 帶來的狀態一致問題?
模組複用與狀態共享的衝突。像處理 data 一樣,用函式 state 返回新狀態物件,而不用物件 state。這樣 3 個元件對應的 state(store.state 上的一小塊)都是獨立的,而且不需要額外的狀態管理
注意,函式 state 的特性在 Vuex v2.3.0+ 可用,低版本需要考慮別的方式,比如:
-
從
state提升一級(維護一個陣列,管理state list) -
考慮把無法共享的局部狀態放到元件級,把可共享的資料及操作放到
store裡
第一種方式會讓 store 迅速膨脹,而且 action/mutation 等等都需要 index,元件需要把 index 傳回給 store,太麻煩不可取
第二種方式是終極解決辦法,劃分 state 的技巧在哪裡都適用,不要單純為了 Vuex 化而使用 Vuex。把所有狀態都從元件抽離出來放在 store 裡也不是不可以,但 store 持有的狀態過於細致的話,對開發維護來說都是巨大的麻煩:
-
開發時元件裡的任何一個細微變化,都要走
dispatch/commit -
維護時會面對一個非常複雜的
store,上千個mutation type
而這麻煩完全是自找的。那麼考慮狀態該如何劃分:
-
互動相關的 UI 狀態,放在元件級。比如展開/收起、
loading顯示/隱藏、tab/表格分頁等等 -
無法共享的資料狀態,放在元件級。比如表單輸入資料
-
可共享的資料狀態,放在狀態層。比如可快取的服務資料
store 的角色應該是 server + database,作為前端資料層存在,而不是單純地把應用狀態從元件樹抽離出來作為狀態樹,沒有太大意義
2.computed 屬性和 vuex 的 store.state 怎麼關聯起來的?
執行時相依性收集機制
// 组件
computed: {
user() {
return this.$store.state.user;
}
}
// store
mutations: {
[types.SET_USER] (state, user) {
state.user = user;
}
}
計算各 computed 屬性,執行 user() 過程中存取了 store.state.user,觸發 state 的 getter,把 user() 函式相依 store.state.user 這個資訊記錄下來
之後 commit mutation 修改了 store.state.user 的話,觸發 state 的 setter,對 user 屬性對應的所有相依項(其中有 user() 函式)重新求值
接著觸發 computed 的 setter,執行 computed.user 對應的所有相依項(其中有視圖更新函式),視圖更新完成
P.S.相依性收集機制的具體實作見 vue/src/core/observer/dep.js
3.store 傳遞機制
與 react-redux 的 Provider 類似,也提供了一次注入全域可用的方式(Vue.use(Vuex) 並在 new 頂層元件時傳入 store)
Vuex 作為外掛程式,透過修改 Vue.prototype,把 $store 掛上去,讓所有 vm 共享
4.input 等雙向繫結場景與 store.state 不能直接修改的衝突
透過計算屬性的 getter/setter 來處理:
-
getter裡讀store.state -
setter裡commit mutation寫store.state
暫無評論,快來發表你的看法吧