原文:https://hackernoon.com/an-artificial-example-where-mobx-really-shines-and-redux-is-not-really-suited-for-it-1a58313c0c70
譯者:SunnyEver0
首先,我要申明一下旺入,這個(gè)標(biāo)題并不是引戰(zhàn)的兑凿,也不是說MobX是比Redux更好的狀態(tài)管理庫。
Redux和MobX我都在使用眨业,這兩個(gè)我都很喜歡急膀,如果有需要,我會再次使用它們龄捡。
這個(gè)文章下的例子卓嫂,我們可以看到如果想要MobX和Redux都在同一個(gè)呈現(xiàn)水平的話,我們使用MobX可以愉悅輕松地開發(fā)但使用Redux時(shí)我們卻步步維艱聘殖。如果我不清楚晨雳,這是一個(gè)不公平的比較。
因?yàn)檫@篇文章也并不是將這兩個(gè)進(jìn)行綜合的比較也不沒有完全考慮在一個(gè)真實(shí)的境況下奸腺。所以請不要將這個(gè)作為一個(gè)基準(zhǔn)說:”讓我們使用它吧餐禁,它更加快速⊥徽眨”
我不相信這個(gè)世界上存在最好的工具帮非。我相信任何工具都有其用武之地的地方,也有其不適合的地方。
好末盔,一切是時(shí)候了筑舅。我做這個(gè)實(shí)驗(yàn)是為可更好地理解這兩個(gè)工具,當(dāng)我們需要使用這個(gè)工具的時(shí)候陨舱,我能夠選擇正確的工具做正確的事翠拣。
好,我們現(xiàn)在繼續(xù)游盲。
使用版本
為什么要列出版本误墓?因?yàn)樵贘avaScript這個(gè)世界里面,隨著不斷的優(yōu)化益缎,我相信這篇文章會很快過時(shí)谜慌。
-
react@15.4.1
,react-dom@15.4.1
-
redux@3.6.0
,react-redux@5.0.0-rc.1
-
mobx@2.6.4
,mobx-react@4.0.3
課題:pixel paint
我通過React構(gòu)建了一個(gè)"pixel paint"的應(yīng)用,它通過canvas
渲染了一個(gè)可繪制像素的128*128格子莺奔。
你可以通過鼠標(biāo)漂浮在上面繪制任意的像素畦娄。
我們將會并排渲染兩個(gè)canvas畫板,而且這兩個(gè)畫板共享一樣的圖片弊仪。否則熙卡,我們可以自己通過component自身state進(jìn)行狀態(tài)管理,而不需要任何狀態(tài)容器庫進(jìn)行管理励饵。
每一個(gè)像素點(diǎn)通過<div>
進(jìn)行展示驳癌,所以一共有1281282=32768個(gè) DOM節(jié)點(diǎn)去渲染和更新。
這個(gè)實(shí)驗(yàn)在進(jìn)行的過程中非常慢役听。
注意:所以的測試均是通過發(fā)布包進(jìn)行颓鲜。
MobX 版
這是容器store(我避免使用了裝飾器語法,因?yàn)樵趯戇@篇文章的時(shí)候典予,該語法還為待定提案)甜滨。
const store = observable({
pixels: asMap({ }),
isActive (i, j) {
return !!store.pixels.get(i + ',' + j)
},
toggle: action(function toggle (i, j) {
store.pixels.set(i + ',' + j, !store.isActive(i, j))
})
})
渲染每一個(gè)像素點(diǎn)的canvas
function MobXCanvas () {
const items = [ ]
for (let i = 0; i < 128; i++) {
for (let j = 0; j < 128; j++) {
items.push(<PixelContainer i={i} j={j} key={i + ',' + j} />)
}
}
return <div>{items}</div>
}
管理每一個(gè)像素state的狀態(tài)容器
const PixelContainer = observer(function PixelContainer ({ i, j }) {
return <Pixel
i={I}
j={j}
active={store.isActive(i, j)}
onToggle={() => store.toggle(i, j)}
/>
})
這里是演示結(jié)果。在屏幕的右上角瘤袖,有mobx-react-devtools可以使用衣摩。
以下為演示結(jié)果:
通過統(tǒng)計(jì)的餅狀圖,我們可以看到scripting只占到一小部分百分比捂敌。大多數(shù)時(shí)間是花在了rendering和painting上面艾扮。
所以表明MobX做得很棒!
Redux 版(初次)
這是reducer:
const store = createStore((state = Immutable.Map(), action) => {
if (action.type === 'TOGGLE') {
const key = action.i + ',' + action.j
return state.set(key, !state.get(key))
}
return state
})
selector:
const selectActive = (state, i, j) => state.get(i + ',' + j)
action creator:
const toggle = (i, j) => ({ type: 'TOGGLE', i, j })
Redux store已經(jīng)準(zhǔn)備就緒占婉。
給每一個(gè) pixel提供的canvas store
:
function ReduxCanvas () {
const items = [ ]
for (let i = 0; i < 128; i++) {
for (let j = 0; j < 128; j++) {
items.push(<PixelContainer i={i} j={j} key={i + ',' + j} />)
}
}
return <Provider store={store}>
<div>
{items}
</div>
</Provider>
}
每一個(gè)pixel connect
store
:
const PixelContainer = connect(
(state, ownProps) => ({
active: selectActive(state, ownProps.i, ownProps.j)
}),
(dispatch, ownProps) => ({
onToggle: () => dispatch(toggle(ownProps.i, ownProps.j))
})
)(Pixel)
你可以使用 redux-devtools-extension
來檢查store state泡嘴,actions
和一直耗時(shí)追蹤∧婕茫可以看出這種方式下性能大大低于了MobX版酌予。讓我們一起看下它的圖形結(jié)果:
大量的時(shí)間損耗在了執(zhí)行JS上磺箕。接近50%的比例.這是不好的,為什么會執(zhí)行這么長時(shí)間呢抛虫?t
我們一起來剖析一下:
這個(gè)展示了Redux的訂閱模式是怎么工作的滞磺。
在以上的例子中,每一個(gè)pixel都connect
了store
莱褒,也意味著它對store
的數(shù)據(jù)的數(shù)據(jù)進(jìn)行了subscribe
。只要store
發(fā)生了變化涎劈,每一個(gè)訂閱者都會進(jìn)行檢查是否需要進(jìn)行更新渲染广凸。
這個(gè)也意味著,我們只改變一個(gè)pixel蛛枚,則Redux會通知所有的32768個(gè)觀察者
這個(gè)同Angular 1
的臟檢測類似谅海。通過這個(gè)也建議在使用Redux時(shí):不要在屏幕上渲染太多視圖。
通過Redux蹦浦,你只能對整個(gè)store的狀態(tài)進(jìn)行訂閱扭吁,因?yàn)樗膕ubtree是一個(gè)普通而陳舊的JavaScript對象,我們不能夠訂閱它盲镶。
但是使用MobX侥袜,每一塊的state
都是有自己內(nèi)部進(jìn)行觀察。在我們的MobX
版本中溉贿,每一個(gè)pixel都訂閱了自己本身的state subtree
枫吧。這也是它效率高的原因。
第二次嘗試:單一subscriber
所以宇色,太多的觀察者是一個(gè)問題九杂。這一次,我將保證這里只有一個(gè)subscriber宣蠕。
這里例隆,我們創(chuàng)建了一個(gè)Canvas組件,它可以訂閱整個(gè)store并渲染所有的pixels抢蚀。
function ReduxCanvas () {
return <Provider store={store}><Canvas /></Provider>
}
const Canvas = connect(
(state) => ({ state }),
(dispatch) => ({ onToggle: (i, j) => dispatch(toggle(i, j)) })
)(function Canvas ({ state, onToggle }) {
const items = [ ]
for (let i = 0; i < 128; i++) {
for (let j = 0; j < 128; j++) {
items.push(<PixelContainer
i={I}
j={j}
active={selectActive(state, i, j)}
onToggle={onToggle}
key={i + ',' + j}
/>)
}
}
return <div>{items}</div>
})
PixelContainer
組件將其從Canvas
組件中獲取的props傳遞給Pixel
class PixelContainer extends React.PureComponent {
constructor (props) {
super(props)
this.handleToggle = this.handleToggle.bind(this)
}
handleToggle () {
this.props.onToggle(this.props.i, this.props.j)
}
render () {
return <Pixel
i={this.props.i}
j={this.props.j}
active={this.props.active}
onToggle={this.handleToggle}
/>
}
}
從這個(gè)版本可以看出我們上次的嘗試是有多么的糟糕镀层。
讓我們看看發(fā)生了什么。
問題好像出在了我們的繪圖
Canvas
上皿曲。它是唯一一個(gè)訂閱store的觀察者鹿响,并對16384個(gè)pixels的狀態(tài)進(jìn)行管理。每次store進(jìn)行dispatch action谷饿,它需要傳遞正確的props給16384個(gè)
pixels
進(jìn)行渲染惶我。這個(gè)意味著在每個(gè)canvas中,React會對
React.createElement
會對16384次調(diào)用博投,并嘗試去調(diào)和16384個(gè)子組件绸贡。這不是一件好事。我們可以做得更好。
第三次嘗試:平衡的狀態(tài)樹
Redux的一個(gè)主要優(yōu)勢在于它不可變的state tree(它開啟可一些酷的功能听怕,比如無痛熱加載和time-traveling)
它證明了我們構(gòu)建數(shù)據(jù)的方式和我們的觀點(diǎn)并不是一成不變的捧挺。
一個(gè)不可變的狀態(tài)樹儲存于一個(gè)平衡的樹中
,這是最好的尿瞭。我在這篇文章中討論了這個(gè)觀點(diǎn):
immutable-js-persistent-data-structures-and-structural-sharing
譯者注:這是文章的主題是 Why use Immutable.js instead of normal Javascript object? 有興趣的朋友可以了解一下闽烙。
所以,我們在這里也這樣做吧!
我們可以把我們的canvas分為一下四個(gè)象限:
當(dāng)我們需要去改變一個(gè)pixel
我們只需更新這個(gè)區(qū)塊声搁,不用去考慮其他區(qū)塊黑竞。
與重新渲染所有的16384 pixels,我們只需重新渲染 64×64=4096個(gè) pixels疏旨。我們提升了75%的效率很魂。
但是4096仍然是一個(gè)較大的數(shù)量。所以我們可以繼續(xù)遞歸地分割的我們的渲染區(qū)塊直到最后的1×1 pixel檐涝。
為了通過這種當(dāng)時(shí)來更新組件遏匆,我們需要通過同樣的方式來構(gòu)造我們的state,當(dāng)state改變時(shí)谁榜,我們可以直接用===
來判斷區(qū)塊的state是否已經(jīng)改變
下面是初始化state的代碼(recursively)
const generateInitialState = (size) => (size === 1
? false
: Immutable.List([
generateInitialState(size / 2),
generateInitialState(size / 2),
generateInitialState(size / 2),
generateInitialState(size / 2)
])
)
現(xiàn)在我們的state是一個(gè)遞歸嵌套的樹幅聘,每個(gè)pixel也不是由這樣的坐標(biāo)----(58, 52)進(jìn)行標(biāo)識,而是使用這樣的路徑---- (1, 3, 3, 2, 0, 2, 1) 去標(biāo)識窃植。
但要在屏幕上展示喊暖,我們需要通過這個(gè)path能找到它的坐標(biāo)。
function keyPathToCoordinate (keyPath) {
let i = 0
let j = 0
for (const quadrant of keyPath) {
i <<= 1
j <<= 1
switch (quadrant) {
case 0: j |= 1; break
case 2: i |= 1; break
case 3: i |= 1; j |= 1; break
default:
}
}
return [ i, j ]
}
// 譯者注:如果這種二進(jìn)制構(gòu)造不好理解撕瞧,可以用下面這種迭代還原法
function keyPathToCoordinate(keyPath) {
let i = 0, j = 0;
for (let index = 0; index < keyPath.length; index++) {
let path = keyPath[index];
let power = keyPath.length - index - 1;
if (path === 0) {
j += Math.pow(2, power);
} else if (path === 2) {
i += Math.pow(2, power);
} else if (path === 3) {
i += Math.pow(2, power);
j += Math.pow(2, power);
}
}
return [i ,j];
}
我們同樣需要能夠通過坐標(biāo)獲得path:
function coordinateToKeyPath (i, j) {
const keyPath = [ ]
for (let threshold = 64; threshold > 0; threshold >>= 1) {
keyPath.push(i < threshold
? j < threshold ? 1 : 0
: j < threshold ? 2 : 3
)
i %= threshold
j %= threshold
}
return keyPath
}
現(xiàn)在我們需要更新我們的reducer變成這樣:
const store = createStore(
function reducer (state = generateInitialState(128), action) {
if (action.type === 'TOGGLE') {
const keyPath = coordinateToKeyPath(action.i, action.j)
return state.updateIn(keyPath, (active) => !active)
// |
// This is why I use Immutable.js:
// So that I can use this method.
}
return state
}
)
然后我們構(gòu)造一個(gè)組件來遍歷這個(gè)樹并將所有的內(nèi)容放置在這里陵叽。GridContainer
與store相連接并對最外層的Grid
進(jìn)行渲染。
function ReduxCanvas () {
return <Provider store={store}><GridContainer /></Provider>
}
const GridContainer = connect(
(state, ownProps) => ({ state }),
(dispatch) => ({ onToggle: (i, j) => dispatch(toggle(i, j)) })
)(function GridContainer ({ state, onToggle }) {
return <Grid keyPath={[ ]} state={state} onToggle={onToggle} />
})
然后每個(gè)Grid
遞歸渲染一個(gè)較小的版本丛版,直到它到達(dá)一個(gè)葉子(白色/黑色1x1像素畫布)
class Grid extends React.PureComponent {
constructor (props) {
super(props)
this.handleToggle = this.handleToggle.bind(this)
}
shouldComponentUpdate (nextProps) {
// Required since we construct a new `keyPath` every render
// but we know that each grid instance will be rendered with
// a constant `keyPath`. Otherwise we need to memoize the
// `keyPath` for each children we render to remove this
// "escape hatch."
return this.props.state !== nextProps.state
}
handleToggle () {
const [ i, j ] = keyPathToCoordinate(this.props.keyPath)
this.props.onToggle(i, j)
}
render () {
const { keyPath, state } = this.props
if (typeof state === 'boolean') {
const [ i, j ] = keyPathToCoordinate(keyPath)
return <Pixel
i={I}
j={j}
active={state}
onToggle={this.handleToggle}
/>
} else {
return <div>
<Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 0 ]} state={state.get(0)} />
<Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 1 ]} state={state.get(1)} />
<Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 2 ]} state={state.get(2)} />
<Grid onToggle={this.props.onToggle} keyPath={[ ...keyPath, 3 ]} state={state.get(3)} />
</div>
}
}
}
哇哦巩掺,我們終于恢復(fù)了速度!它同之前的MobX版本一樣快页畦,而且擁有熱加載和時(shí)間追蹤功能胖替。
我們的DOM樹看起來也更像是樹了:
對比所有之前的方法:
終極優(yōu)化方案
因?yàn)樗鼘?shí)用性不高,我沒有進(jìn)行編碼豫缨。
怎么做呢:針對每一個(gè)pixel創(chuàng)建一個(gè)Redux Store独令。我沒有進(jìn)行測試因?yàn)槲掖_信這種方法是Redux最快的方式。
但你使用這種方式時(shí)好芭,Redux的很多優(yōu)點(diǎn)都會被丟棄燃箭。舉個(gè)例子,Redux DevTools 可能閃退舍败。而且時(shí)間追蹤對于么一個(gè)pixel不是那么有用招狸,不是嗎敬拓?
更好的方式?
以上即為所有我所考慮到的地方裙戏。
如果你有更好的優(yōu)雅解決方案乘凸,還請聯(lián)系我。
Updates:
- Dan Abramov 提交了他的版本累榜,它的性能比V1的要好但低于V3的性能而且比較簡單易懂营勤。
譯者注:Dan Abramov為Redux的作者
結(jié)論
這是一個(gè)有趣的實(shí)驗(yàn)。
我們大多數(shù)在優(yōu)化命令式算法方面擁有扎實(shí)的知識壹罚,但對于在不可變數(shù)據(jù)上的應(yīng)用層面葛作,如果我們不懂性能影響,優(yōu)化它會變成一個(gè)挑戰(zhàn)渔嚷。
一旦我們優(yōu)化了Redux版本,我們可以看到性能優(yōu)化后導(dǎo)致了代碼的可讀性降低稠曼。上面寫的代碼真的有點(diǎn)糟糕形病!
就像Dan Abramov說的一樣,Redux提供了一種折中的方案(MobX也是)霞幅。所以你會在不失去熱加載和時(shí)間追蹤功能的前提下漠吻,用代碼的清晰度和可讀性去交換性能嗎?
在我的midi-instrument項(xiàng)目下司恳,因?yàn)樗鼤贛obileSafari下運(yùn)行途乃,所以性能是很重要的,特別是當(dāng)一個(gè)instrument包含了幾百個(gè)buttons的時(shí)候扔傅。
我同樣也希望能夠在使用不可變數(shù)據(jù)時(shí)快速創(chuàng)建新的原型耍共,而不用去擔(dān)心性能影響。
我也發(fā)現(xiàn)hot-reloading 和 time-traveling在這個(gè)項(xiàng)目中并不是那么適用猎塞。大多數(shù)state只持續(xù)了短暫的幾秒鐘试读,我的項(xiàng)目也足夠的小我可以自己手動(dòng)刷新界面。
所以我最后很開心地使用了MobX荠耽。
在我正在做的這個(gè)旋律游戲--Bemuse中钩骇,我能感覺到使用不可變數(shù)據(jù)能夠幫助我寫一些簡單易用的代碼。
我并不擔(dān)心不確定的state突然變更铝量,因?yàn)檫@里并不會有倘屹。
這里不會有大量的數(shù)據(jù)需要渲染,所以我可能不需要像上面的例子一樣去優(yōu)化它慢叨。
使用Redux DevTools纽匙,讓所有state更新都集中顯示中一個(gè)固定地方,也讓我受益匪淺拍谐,這里哄辣,Redux盡情發(fā)揮了它的價(jià)值请梢。
所以我很開心地在這個(gè)項(xiàng)目中使用了Redux。
一個(gè)不公平的性能比較
這個(gè)比較一開始就是不公平的力穗,當(dāng)我使用函數(shù)式方法(Redux)同命令式方法(MobX)進(jìn)行比較時(shí)毅弧。
在1996年,Chris Okasaki在它140頁的論文中得出了結(jié)論--“純函數(shù)數(shù)據(jù)結(jié)構(gòu)”当窗,如下:
無論編譯器怎么進(jìn)步够坐,只要命令式的算法優(yōu)于函數(shù)式的算法,函數(shù)式程序絕不會比它的同行--命令式更快崖面。
In that thesis (now available as a book), he tried to make data structures in functional programming as efficient its imperative counterpart.
在那篇論文中(現(xiàn)在已經(jīng)發(fā)版出書)元咙,他嘗試通過構(gòu)造數(shù)據(jù)結(jié)構(gòu)讓函數(shù)式編程比命令式編程更有效。
本文提供了大量的函數(shù)式數(shù)據(jù)結(jié)構(gòu)巫员,它可以漸近地同命令式編程一樣有效庶香。
我不會停止函數(shù)式編程因?yàn)樗粫衩钍剿惴敲纯臁?br> 這完全取決于利弊的權(quán)衡。這是為什么我不會說"讓我們使用Redux/MobX做所有事情吧简识!"這也是為什么當(dāng)人們問我“2017年了案站,我應(yīng)該使用MobX還是Redux该肴?”卻沒有給出特定的場景,我也不會給出確定答案的原因。這也是我寫這篇文章的原因富岳。
謝謝閱讀进副!