前言
在react+redux項(xiàng)目里,關(guān)于reducer中處理state的方式,在redux官方文檔中有這樣一段描述 (鏈接):
不要修改 state暮的。 使用 Object.assign() 創(chuàng)建了一個(gè)副本。不能這樣使用 Object.assign(state, {visibilityFilter: action.filter }),因?yàn)樗鼤?huì)改變第一個(gè)參數(shù)的值桃犬。你必須把第一個(gè)參數(shù)設(shè)置為空對(duì)象恋拍。你也可以開啟對(duì)ES7提案對(duì)象展開運(yùn)算符的支持, 從而使用 { ...state, ...newState }達(dá)到相同的目的。
對(duì)此,我們可能會(huì)產(chǎn)生以下一些疑問:
- 為什么要?jiǎng)?chuàng)建副本state?
- 怎樣創(chuàng)建副本state才是合理的?
- 外部插件直接更新state是否合理?
為什么要?jiǎng)?chuàng)建副本state
在redux-devtools中,我們可以查看到redux下所有通過reducer更新state的記錄,每一個(gè)記錄都對(duì)應(yīng)著內(nèi)存中某一個(gè)具體的state,讓用戶可以追溯到每一次歷史操作產(chǎn)生與執(zhí)行時(shí),當(dāng)時(shí)的具體狀態(tài),這也是使用redux管理狀態(tài)的重要優(yōu)勢(shì)之一.
若不創(chuàng)建副本,redux的所有操作都將指向內(nèi)存中的同一個(gè)state,我們將無從獲取每一次操作前后,state的具體狀態(tài)與改變,若沒有副本,redux-devtools列表里所有的state都將被最后一次操作的結(jié)果所取代.我們將無法追溯state變更的歷史記錄.
創(chuàng)建副本也是為了保證向下傳入的this.props與nextProps能得到正確的值,以便我們能夠利用前后props的改變情況以決定如何render組件
怎樣創(chuàng)建副本state才是合理的?
既然創(chuàng)建副本是為了保留更改歷史,那么,原則上原state所有被改動(dòng)過的屬性都應(yīng)該被創(chuàng)建副本,
我們可以看一下官方示例(鏈接):
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
default:
return state
}
}
示例中的state結(jié)構(gòu)較為簡(jiǎn)單,而實(shí)際項(xiàng)目中的業(yè)務(wù)需求可能遠(yuǎn)比示例中更為復(fù)雜.
若visibilityFilter是下面這樣的結(jié)構(gòu):
visibilityFilter:{
a:{
c:1
},
b:{
d:2
}
}
而我們需要改動(dòng)的是visibilityFilter.b.d,就會(huì)產(chǎn)生一些問題,方案可以是以下幾種:
方案1
將todoApp這個(gè)reducer拆分為更細(xì)化的reducer,以保證visibilityFilter屬性中嵌套對(duì)象b的屬性d能得到正確更新
方案2
采用官方實(shí)例中Object.assign方法,但需要將visibilityFilter中未更新的對(duì)象用原state的中的對(duì)象進(jìn)行手動(dòng)賦值
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: {
state.visibilityFilter.a,
b:{
d:action.filter
}
}
})
default:
return state
}
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return {
...state,
visibilityFilter:{
...state.visibilityFilter,
b:{
...state.visibilityFilter.b,
d:action.filter
}
}
}
default:
return state
}
}
方案3
將state進(jìn)行深度對(duì)象克隆后,再進(jìn)行更新,可以用原生js去實(shí)現(xiàn),但這里直接采用lodash的cloneDeep方法
import cloneDeep from 'lodash/cloneDeep'
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
const newState = cloneDeep(state)
newState.visibilityFilter.b.d = action.filter
return newState
default:
return state
}
}
方案4
采用官方提供的Immutability Helper工具中update()方法進(jìn)行數(shù)據(jù)更新(鏈接)
import update from 'react/lib/update'
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return update(state, {
visibilityFilter:{
d:{$set: action.filter}
}
})
default:
return state
}
}
方案小結(jié)
方案1在結(jié)構(gòu)更復(fù)雜時(shí),將產(chǎn)生更多更為細(xì)化的reducer,而其中可能有很多reducer其實(shí)并沒有必要再進(jìn)行深層次的細(xì)化拆分.
方案2中,我們需要將原對(duì)象中所有沒有變更的對(duì)象手動(dòng)賦值給副本對(duì)象,并確保副本對(duì)象的結(jié)構(gòu)完整性與原對(duì)象相同.相比方案1,方案2的優(yōu)勢(shì)則在于更少的代碼量.
方案3是上述方案中最為簡(jiǎn)便且不易出錯(cuò)的方案,但深度復(fù)制,將會(huì)為整個(gè)被復(fù)制的對(duì)象創(chuàng)建一個(gè)完整的副本,與方案1,2中只創(chuàng)建變更部分的副本相比,性能上將消耗更多內(nèi)存,在執(zhí)行效率上也會(huì)明顯低于前面的方案.
方案4不存在方案3的性能問題,并且相比方案2而言,創(chuàng)建副本的方式更為簡(jiǎn)單,所以本文更為推薦采用此方案創(chuàng)建副本
錯(cuò)誤示例!
由于官方示例采用Object.assign方法創(chuàng)建副本,所以有時(shí)候我們?yōu)榱藭鴮懞?jiǎn)便,可能會(huì)出現(xiàn)這樣的副本創(chuàng)建方式
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
const newState = Object.assign({}, state)
newState.visibilityFilter.b.d = action.filter
return newState
default:
return state
}
}
或者
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
state.visibilityFilter.b.d = action.filter
return Object.assign({}, state)
default:
return state
}
}
此處我們對(duì)Object.assign方法進(jìn)行一個(gè)小的測(cè)試
const x = {
a1: {a2: 1},
b1: {
b2: {b4: 2},
b3: {b5: 3}
},
c1:4
}
const y = Object.assign({}, x)
y.b1.b3.b5 = 8
y.c1 = 9
console.log(x); //=> {a1:{a2:1}, b1:{b2:{b4:2}, b3:{b5:8}}, c1:4}
console.log(y); //=> {a1:{a2:1}, b1:{b2:{b4:2}, b3:{b5:8}}, c1:9}
console.log(x == y); //=> false
console.log(x.a1 == y.a1); //=> true
console.log(x.b1 == y.b1); //=> true
console.log(x.b1.b2 == y.b1.b2); //=> true
console.log(x.b1.b3 == y.b1.b3); //=> true
console.log(x.b1.b3.b5 == y.b1.b3.b5); //=> true
由此可見Object.assign操作后,x,y的區(qū)別是其自身的引用地址和屬性c1所引用的數(shù)值不同,而屬性a1,b1所引用的對(duì)象及其內(nèi)部子對(duì)象在內(nèi)存中是同一個(gè)引用地址,這就意味著,改變y.b1.b3.b5的值實(shí)際上同時(shí)也改變了x.b1.b3.b5的值,
這會(huì)導(dǎo)致redux中state歷史混亂以及之后components所調(diào)用的this.props與nextProps無法得到正確的值
外部插件直接更新state是否合理?
筆者目前接觸較多的是redux-form插件,所以,此處暫且以redux-form更新state的方式進(jìn)行一些探討.
redux-form
當(dāng)組件采用redux-form進(jìn)行監(jiān)聽后,其內(nèi)部form表單里的對(duì)象都將被放入redux的state中進(jìn)行管理,并由redux-form自身發(fā)起action進(jìn)行更新刪除等操作.
而問題在于,redux-form會(huì)為每一次的表單更新都發(fā)起一次action,也就意味著我們?cè)谝粋€(gè)input框里輸入一句簡(jiǎn)單的"hello world",將會(huì)有11個(gè)state副本產(chǎn)生
首先,就創(chuàng)建副本而言,其本身是一種性能消耗,而redux創(chuàng)建副本的目的是為了追溯歷史操作與更改,所以我們應(yīng)該考慮,類似redux-form這樣短時(shí)間高頻率的更改state的方式,產(chǎn)生大量細(xì)碎的輸入歷史,我們是否應(yīng)該避免這樣的更新方式?
其次,若外部插件直接更新state,由于其處理方式大多封裝在其內(nèi)部,若插件自身對(duì)創(chuàng)建state副本的方式?jīng)]有深入的考慮,其高頻率的更新state,可能會(huì)對(duì)整個(gè)項(xiàng)目的運(yùn)行效率產(chǎn)生較為嚴(yán)重的影響.
小結(jié)
就redux-form使用體驗(yàn)而言,在一些輸入場(chǎng)景中,其會(huì)導(dǎo)致輸入操作產(chǎn)生明顯的頓挫感,可以猜測(cè)這是由于其工作方式導(dǎo)致的性能問題造成的結(jié)果.
所以,外部插件直接更新state可能會(huì)使一些業(yè)務(wù)狀態(tài)更方便于管理,但其對(duì)整個(gè)項(xiàng)目的性能影響情況,可能需要我們?nèi)ド髦卦u(píng)估.