Redux是一個數據管理層,被普遍用於管理複雜應用的數據。可是實際使用中,Redux的表現差強人意,能夠說是很差用。而同時,社區也出現了一些數據管理的方案,Mobx就是其中之一。javascript
Predictable state container for JavaScript appsjava
這是Redux給本身的定位,可是這其中存在不少問題。
首先,Redux作了什麼?看Redux的源碼,createStore
只有一個函數,返回4個閉包。dispatch
只作了一件事,調用reducer
而後調用subscribe
的listener
,這其中state
的不可變或者是可變所有由使用者來控制,Redux並不知道state有沒有發生變化,更不知道state具體哪裏發生了變化。因此,若是view層須要知道哪一部分須要更新,只能經過髒檢查。react
再看react-redux
作了什麼,在store.subscribe上掛回調,每次發生subscribe就調用connect
傳進去mapStateToProps
和mapDispatchToProps
,而後髒檢測props
的每一項。固然,咱們能夠利用不可變數據的特色,去減小prop的數量從而減小髒檢測的次數,可是哪有props都來自同一個子樹這麼好的事呢?redux
因此,若是有n個組件connect,每當dispatch一個action的時候,不管作了什麼粒度的更新,都會發生O(n)時間複雜度的髒檢測。數組
// Redux 3.7.2 createStore.js // ... try { isDispatching = true currentState = currentReducer(currentState, action) } finally { isDispatching = false } const listeners = currentListeners = nextListeners for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } // ...
更糟糕的是,每次reducer執行完Redux就直接調用listener了,若是在短期內發生了屢次修改(例如用戶輸入),不可變的開銷,加上redux用字符串匹配action的開銷,髒檢測的開銷,再加上view層的開銷,整個性能表現會很是糟糕,即便在用戶輸入的時候每每只須要更新一個"input"。應用規模越大,性能表現越糟糕。(這裏的應用指單個頁面。這裏的單頁不是SPA的單頁的意思,由於有Router的狀況下,被切走的頁面其全部組件都被unmount了)安全
在應用規模增大的同時,異步請求數量一多,Redux所宣傳的Predictable
也根本就是泡影,更多的時候是配合各類工具淪爲數據可視化工具。數據結構
Mobx能夠說是衆多數據方案中最完善的一個了。Mobx自己獨立,不與任何view層框架互相依賴,所以你能夠隨意選擇合適的view層框架(部分除外,例如Vue,由於它們的原理是同樣的)。閉包
目前Mobx(3.x)和Vue(2.x)採用了相同的響應式原理,借用Vue文檔的一張圖:
爲每一個組件建立一個Watcher,在數據的getter和setter上加鉤子,當組件渲染的時候(例如,調用render方法)會觸發getter,而後把這個組件對應的Watcher添加到getter相關的數據的依賴中(例如,一個Set)。當setter被觸發時,就能知道數據發生了變化,而後同時對應的Watcher去重繪組件。app
這樣,每一個組件所須要的數據時精確可知的,所以當數據發生變化時,能夠精確地知道哪些組件須要被重繪,數據變化時重繪的過程是O(1)的時間複雜度。框架
須要注意的是,在Mobx中,須要把數據聲明爲observable。
import React from 'react'; import ReactDOM from 'react-dom'; import { observable, action } from 'mobx'; import { Provider, observer, inject } from 'mobx-react'; class CounterModel { @observable count = 0 @action increase = () => { this.count += 1; } } const counter = new CounterModel(); @inject('counter') @observer class App extends React.Component { render() { const { count, increase } = this.props.counter; return ( <div> <span>{count}</span> <button onClick={increase}>increase</button> </div> ) } } ReactDOM.render( <Provider counter={counter}> <App /> </Provider> );
在這篇文章中,做者使用了一個一個128*128的繪圖板來講明問題。
因爲Mobx利用getter
和setter
(將來可能會出現一個平行的基於Proxy
的版本)去收集組件實例的數據依賴關係,所以每單當一個點發生更新的時候,Mobx
知道哪些組件須要被更新,決定哪一個組件更新的過程的時間複雜度是O(1)的,而Redux
經過髒檢查每個connect
的組件去獲得哪些組件須要更新,有n個組件connect
這個過程的時間複雜度就是O(n),最終反映到Perf工具上就是JavaScript的執行耗時。
雖然在通過一系列優化後,Redux的版本能夠得到不輸Mobx版本的性能,當時Mobx不用任何優化就能夠獲得不錯的性能。而Redux最完美的優化是爲每個點創建單獨的store,這與Mobx等一衆精肯定位數據依賴的方案在思想上是相同的。
Mobx並不完美。Mobx不要求數據在一顆樹上,所以對Mobx進行數據但是化或者是記錄每次的數據變化變得不太容易。在Mobx的基礎上,Mobx State Tree誕生了。同Redux同樣,Mobx State Tree要求數據在一顆樹上,這樣對數據進行可視化和追蹤就變得很是容易,對開發來講是福音。同時Mobx State Tree很是容易獲得準確的TypeScript類型定義,這一點Redux不容易作到。同時還提供了運行時的類型安全檢查。
import React from 'react'; import ReactDOM from 'react-dom'; import { types } from 'mobx-state-tree'; import { Provider, observer, inject } from 'mobx-react'; const CountModel = types.model('CountModel', { count: types.number }).actions(self => ({ increase() { self.count += 1; } })); const store = CountModel.create({ count: 0 }); @inject(({ store }) => ({ count: store.count, increase: store.increase })) class App extends React.Component { render() { const { count, increase } = this.props; return ( <div> <span>{count}</span> <button onClick={increase}>increase</button> </div> ) } } ReactDOM.render( <Provider store={store}> <App /> </Provider> );
Mobx State Tree還提供了snapshot
的功能,所以雖然MST自己的數據可變,依然能打到不可變的數據的效果。官方提供了利用snaptshot
直接結合Redux的開發工具使用,方便開發;同時官方還提供了把MST的數據做爲一個Redux的store來使用;固然,利用snapshot也能夠MST嵌在Redux的store中做爲數據(相似在Redux中很流行的Immutable.js的做用)。
// 鏈接Redux的開發工具 // ... connectReduxDevtools(require("remotedev"), store); // ... // 直接做爲一個Redux store使用 // ... import { Provider, connect } from 'react-redux'; const store = asReduxStore(store); @connect(// ...) function SomeComponent() { return <span>Some Component</span> } ReactDOM.render( <Provider store={store}> <App /> <Provider />, document.getElementById('foo') ); // ...
而且,在MST中,可變數據和不可變的數據(snapshot)能夠互相轉化,你能夠隨時把snapshot應用到數據上。
applySnapshot(counter, { count: 12345 });
除此以外,官方還提供了異步action的支持。因爲JavaScript的限制,異步操做難以被追蹤,即時使用了async函數,其執行過程當中也是不能被追蹤的,就會出現雖然在async的函數內操做了數據,這個async函數也被標記爲action,可是會被誤判是在action外修改了數據。以往異步action只能經過多個action組合使用來完成,而Vue則是經過把action和mutation分開來實現。在Mobx State Tree利用了Generator,使異步操做能夠在一個action函數內完成而且能夠被追蹤。
// ... SomeModel.actions(self => ({ someAsyncAction: process(function* () { const a = 1; const b = yield foo(a); // foo必須返回一個Promise self.bar = b; }) })); // ...
Mobx利用getter
和setter
來收集組件的數據依賴關係,從而在數據發生變化的時候精確知道哪些組件須要重繪,在界面的規模變大的時候,每每會有不少細粒度更新,雖然響應式設計會有額外的開銷,在界面規模大的時候,這種開銷是遠比對每個組件作髒檢查小的,所以在這種狀況下Mobx會很容易獲得比Redux更好的性能。而在數據所有發生改變時,基於髒檢查的實現會比Mobx這類響應式有更好的性能,但這類狀況不多。同時,有些benchmark並非最佳實踐,其結果也不能反映真實的狀況。
可是,因爲React
自己提供了利用不可變數據結構來減小無用渲染的機制(例如PureComponent,函數式組件),同時,React的一些生態和Immutable綁定了(例如Draft.js),所以在配合可變的觀察者模式的數據結構時並非那麼舒服。因此,在遇到性能問題以前,建議仍是使用Redux和Immutable.js搭配React。
The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.
因爲JavaScript的限制,一些對象不是原生的對象,其餘的類型檢查庫可能會致使意想不到的結果。例如在Mobx中,數組並非一個Array,而是一個類Array的對象,這是爲了能監聽到數據下標的賦值。相對的,在Vue中數組是一個Array,可是數組下標賦值要使用splice
來進行,不然沒法被檢測到。
因爲Mobx的原理,要作到精確的按需更新,就要在正確的地方觸發getter,最簡單的辦法就是render要用到的數據只在render裏解構。mobx-react
從4.0開始,inject
接受的map函數中的結構也會被追蹤,所以能夠直接用相似react-redux
的寫法。注意,在4.0以前inject的map函數不會被追蹤。
響應式有額外的開銷,這些開銷在渲染大量數據時會對性能有影響(例如:長列表),所以要合理搭配使用observable.ref
、observable.shallow
(Mobx),types.frozen
(Mobx State Tree)。
本文首發於有贊技術博客。