從源碼解讀 Vuex 的一些缺陷
原創(chuàng)不易,轉(zhuǎn)載前務(wù)必與作者聯(lián)系
眾所周知,Vuex 是 Flux 架構(gòu)的一種實(shí)現(xiàn)。Flux 清晰確立了數(shù)據(jù)管理場景下各種職能單位,其主要準(zhǔn)則有:
中心化狀態(tài)管理
狀態(tài)只能通過專門
突變單元進(jìn)行變更應(yīng)用層通過發(fā)送信號(hào)(一般稱 action),觸發(fā)變更
Vuex 也是緊緊圍繞這些準(zhǔn)則開發(fā)的,通過 store 類提供 Flux 模式的核心功能。在滿足架構(gòu)的基本要求之外,則進(jìn)一步設(shè)計(jì)了許多便利的措施:
通過“模塊化”設(shè)計(jì),隔離數(shù)據(jù)單元
提供 getter 機(jī)制,提高代碼復(fù)用性
使用
Vue.$watch方法,實(shí)現(xiàn)數(shù)據(jù)流零配置,天然整合進(jìn) Vue 環(huán)境
網(wǎng)上已經(jīng)有很多解析的文章,沒必要贅述。本文僅就 中心化、信號(hào)機(jī)制、數(shù)據(jù)流 三個(gè)點(diǎn)的實(shí)現(xiàn)上展開,討論一下 Vuex 實(shí)現(xiàn)上的缺陷。
中心化
在Vuex中,store 整合了所有功能,是對(duì)外提供的主要接口,也是Flux模式下的數(shù)據(jù)管理中心。通過它,Vuex 主要對(duì)外提供了:
信號(hào)相關(guān)的:
dispatch、commit偵聽器接口:
subscribestate 值變更接口(替換state值,不應(yīng)調(diào)用):
replaceStatestate 模型變更接口(建議僅在按需引用場景下使用):
registerModule、unregisterModule熱更新接口(HMR邏輯,不關(guān)注):
hotUpdate
官方實(shí)現(xiàn)的 store 非常復(fù)雜,耦合了許多邏輯。簡便起見,我們刨除各種旁路邏輯,只關(guān)注Flux架構(gòu)的中心化、信號(hào)控制機(jī)制,可以總結(jié)出一份非常簡單的實(shí)現(xiàn):
export default class Store {
constructor(options) {
this._state = options.state;
this._mutations = options.mutations;
}
get state() {
return this._state;
}
commit(type, payload) {
this._mutations[type].apply(this, [this.state].concat([...payload]));
}
}
這是理解 Vuex 的核心,整份代碼只有兩個(gè)邏輯:
通過
_state屬性實(shí)現(xiàn)中心化、自包含數(shù)據(jù)中心層。通過
dispatch方法,回調(diào)觸發(fā)事先注冊(cè)的_mutations方法。
這份代碼有很多問題,舉例來說:
使用簡單對(duì)象作為 state
狀態(tài)的突變僅僅通過修改state對(duì)象屬性值實(shí)現(xiàn)
沒有任何有效的機(jī)制,防止 state 對(duì)象被誤修改
這些設(shè)計(jì)問題,在Vuex中同樣存在,這與Vue.$watch機(jī)制有非常密切的關(guān)系(見下文),個(gè)人認(rèn)為這是極其不嚴(yán)謹(jǐn)?shù)摹?/p>
信號(hào)機(jī)制
Vuex 提供了兩個(gè)與信號(hào)有關(guān)的接口,其源碼可簡略為:
export default class Store {
...
commit (_type, _payload, _options) {
...
const entry = this._mutations[type]
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
this._subscribers.forEach(sub => sub(mutation, this.state))
...
}
dispatch (_type, _payload) {
...
const entry = this._actions[type]
return entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
}
...
}
兩者之間的不同在于:
dispatch觸發(fā)的是action回調(diào);commit觸發(fā)的mutation回調(diào)。dispatch返回 Promise;commit無返回值。
這樣的設(shè)計(jì)意圖,主要還是職責(zé)分離,action 單元用于描述 發(fā)生了什么;mutation用于修改數(shù)據(jù)層狀態(tài)state。Vuex 用相似的接口,將兩者放置在相同的地位上,這一層接口設(shè)計(jì)其實(shí)存在弊?。?/p>
action、mutation 各自需要一套type體系
允許應(yīng)用層繞過action,直接
commitmutationstate 并非
immutable的,而且在 action 中允許修改state
雖然確實(shí)提升了便利性,但對(duì)初學(xué)者而言,可能導(dǎo)致如下反模式:
設(shè)計(jì)了兩套無法正交的type體系
造成“直接提交mutation即可”的假象,破壞了Flux的信號(hào)機(jī)制
在 action 中手誤修改了 state ,而沒有友好的跟蹤機(jī)制(這一點(diǎn)在getter中特別嚴(yán)重)
由于沒有確切有效的機(jī)制防止錯(cuò)誤,在使用Vuex的過程中,需要非常非常警惕;需要嚴(yán)謹(jǐn)正確地使用各種職能單元;或者以規(guī)范填補(bǔ)設(shè)計(jì)上的缺陷。
單向數(shù)據(jù)流
這里的數(shù)據(jù)流是指從 Vuex 的 state 到 Vue 組件的props/computed/data 等狀態(tài)單元的映射,即如何在組件中獲取state。Vuex 官方推薦使用 mapGetter、mapState 接口實(shí)現(xiàn)數(shù)據(jù)綁定。
mapState
該函數(shù)非常簡單,代碼邏輯可梳理為:
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
...
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState() {
...
return typeof val === 'function' ?
val.call(this, state, getters) :
state[val]
}
})
...
return res
})
mapState 直接讀取 state 對(duì)象的屬性。值得注意的一點(diǎn)是,res[key]一般作為函數(shù)掛載在外部對(duì)象,此時(shí)函數(shù)的this指向掛載的 Vue 組件。
mapGetter
該函數(shù)同樣非常簡單,其代碼邏輯為:
export const mapGetters = normalizeNamespace((namespace, getters) => {
const res = {}
normalizeMap(getters).forEach(({ key, val }) => {
res[key] = function mappedGetter() {
...
return this.$store.getters[val]
}
...
})
return res
})
mapGetter 訪問的則是組件掛載是 $store 實(shí)例的 getters 屬性。
從 state 到 getter
Vuex 的 getter屬性 與 Vue 的computed屬性在各方面的特性都非常相似,實(shí)際上,getter 正是基于 computed 實(shí)現(xiàn)的。其核心邏輯有:
function resetStoreVM(store, state, hot) {
...
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
// 遍歷 getter 配置,生成 computed 屬性
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = () => fn(store)
Object.defineProperty(store.getters, key, {
// 獲取 vue 實(shí)例屬性
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// 新建 Vue 實(shí)例,專門用于監(jiān)聽屬性變更
store._vm = new Vue({
data: {
?state: state
},
computed
})
...
}
從代碼可以看出,Vuex 將整個(gè) state 對(duì)象托管到vue實(shí)例的data屬性中,以此換取Vue的整個(gè) watch 機(jī)制。而getter屬性正是通過返回實(shí)例的 computed 屬性實(shí)現(xiàn)的,這種實(shí)現(xiàn)方式,不可謂不精妙。問題則是:
Vuex 與 Vue 深度耦合,致使不能遷移到其他環(huán)境下使用
Vue 的
watch機(jī)制是基于屬性讀寫函數(shù)實(shí)現(xiàn)的,如果直接替換根節(jié)點(diǎn),會(huì)導(dǎo)致各種子屬性回調(diào)失效,即不可能實(shí)現(xiàn)immutable特性
后語
Vuex 給我最大的感覺是:便利,同樣的功能有各種不同語義的邏輯單元處理,職責(zé)分離方面做的非常好,如果嚴(yán)格遵循規(guī)范的話,確實(shí)能非常好的組織代碼;接口也很簡明易懂,對(duì)開發(fā)者非常友好。從用戶數(shù)量、影響力等方面來看,無疑是一個(gè)非常偉大的框架。這里提出來的一些觀點(diǎn)當(dāng)然也是見仁見智的,目的不外乎拋磚引玉而已。
往期推薦
