一.目標定位
Simple, scalable state managementvue
一個簡單,夠用的狀態管理庫。仍是想要解決應用狀態(數據)管理的問題react
二.設計理念typescript
Anything that can be derived from the application state, should be derived. Automatically.
源於應用狀態的全部東西,都應該自動獲得。好比UI,數據序列化,服務通訊redux
也就是說,只要知道哪些東西是狀態相關的(源於應用狀態),在狀態發生變化時,就應該自動完成狀態相關的全部事情,自動更新UI,自動緩存數據,自動通知serverapi
這種理念看似新奇,其實就是數據驅動,細想一下,React體系(react + react-redux + redux + redux-saga)也知足這種理念,狀態變化(dispatch action引起stateChange)後,UI自動更新(Container update),自動觸發緩存數據,通知server等反作用(saga)數組
三.核心實現緩存
MobX is inspired by reactive programming principles as found in spreadsheets. It is inspired by MVVM frameworks like in MeteorJS tracker, knockout and Vue.js. But MobX brings Transparent Functional Reactive Programming to the next level and provides a stand alone implementation. It implements TFRP in a glitch-free, synchronous, predictable and efficient manner.
參考了MeteorJS的tracker,knockout以及Vue,這幾個東西的共同特色是都內置了數據綁定,屬於所謂的MVVM架構,分別借鑑了:babel
MeteorJS的設計理念:自動追蹤依賴(tracker, autorun等等),不用再聲明依賴,讓不少事情變得更簡單架構
knockout的數據綁定:ko.observableapp
Vue的運行時依賴收集和computed:基於getter&setter數據綁定實現
因此,MobX的核心實現與Vue很是類似,能夠看作把Vue的數據綁定機制單拎出來,再作加強和擴展:
加強:observable不只支持Array, Object,還支持Map及不可變的Value(對應boxed value)
擴展:提供observer(把數據變化暴露出來),spy(把內部狀態暴露出來),action(規範約束,或是爲了迎合Flux)
P.S.從功能上來看,有observable和observer就能保證可用了。action算是對靈活性的約束,spy用於DevTools接入,都不重要
另外,MobX還利用ES Decorator語法讓監聽變化與OOP結合起來,看起來至關優雅,例如:
import { observable, computed } from "mobx"; class OrderLine { @observable price = 0; @observable amount = 1; @computed get total() { return this.price * this.amount; } }
若是沒有這種類註解語法,那就一點也不漂亮了:
var OrderLine = function() { extendObservable(this, { price: observable.ref(0), amount: observable.ref(1), total: computed(function() { return this.price * this.amount; }) }); }
這樣用起來感受麻煩了不少,遠不及註解形式優雅。利用Decorator把observable和OOP體系結合起來,算是MobX的一大亮點
P.S.Decorator特性目前還處於new proposal階段,屬於很不穩定的特性,所以大多隻用通常形式:
function myDecorator(target, property, descriptor){}
從babel轉換結果上看,算是對Object.defineProperty的攔截(因此Decorator方法簽名與Object.defineProperty徹底一致)
P.S.其實Vue生態也有相似的與OOP結合的東西,例如vuejs/vue-class-component
四.結構
modify update trigger action ------> state ------> computed -------> reaction
對比Flux
保留了Flux的action,新增了一層computed,提出了reaction的概念
這裏的action比Flux的action概念要厚得多,至關於action + dispatcher + store裏負責響應action修改state的部分,簡言之,MobX的action是動詞,Flux的action是名詞。MobX的action是一個動做,直接修改狀態,Flux的action只是個事件消息,由事件接收方(store裏負責響應action修改state的部分)修改狀態
computed與Vue的computed含義相同,都是指依賴state的衍生數據(能根據state算出來的數據),state變化後,自動從新計算computed。另外,computed在概念上被稱爲derivation,也就是「衍生」,由於computed依賴state,是從state衍生出來的數據
reaction指的是對state變化作出的響應,好比更新視圖,或者通知server(利用autorun)。與computed最大的區別是computed產生新數據不含反作用(而reaction含反作用但不產生新數據)
與Flux的(state, action) => state思路基本一致,computed能夠看做上層state,而reaction裏的一個重要部分就是更新視圖,那麼就簡化成了:
modify trigger action ------> state -------> views
對比Flux的結構:
action 傳遞action update state ------> dispatcher ---------> stores ------------> views
如上面提到的,action + dispatcher + store裏負責響應action修改state的部分纔等價於MobX的action
對比Redux
call new state action --> store ------> reducers -----------> view
(引自Redux)
Redux裏的reducer在MobX裏都給塞進action了,不用再拿reducer來描述state結構,也不用再關注reducer純不純(MobX只要求computed是純函數)
computed在Redux裏是片空白,因此由reactjs/reselect來填補,一樣爲了複用數據衍生邏輯,一樣自帶緩存。因此MobX至少至關於Redux + reselect
對比Vuex
commit mutate render action ------> mutation ------> state ------> view
Vuex的特色是從設計上區分了同步/異步action,分別對應mutation和action
比起MobX,剛好是兩個極端。Vuex嫌Flux的action不夠細化,沒有考慮異步場景,才提出了mutation之上的action,而MobX嫌區分同步異步,純與不純太麻煩,才提出了動詞action,囊括異步和反作用
computed在Vuex裏叫作getter,兩者沒什麼太大區別。Vuex也是一開始就考慮了state衍生數據,不像Redux須要reselect來填補空白
五.優點
從實現上看,只有MobX內置了數據變化監聽,也就是把數據綁定的核心工做提到了數據層,這樣作的最大好處是修改state變得很天然,不須要dispatch,也不用造action,想改就直接按直覺去改
狀態修改方式符合直覺
React示例:
@observer class TodoListView extends Component { render() { return <div> <ul> {this.props.todoList.todos.map(todo => <TodoView todo={todo} key={todo.id} /> )} </ul> Tasks left: {this.props.todoList.unfinishedTodoCount} </div> } } const TodoView = observer(({todo}) => <li> <input type="checkbox" checked={todo.finished} {/* 想改就直接改 */} onClick={() => todo.finished = !todo.finished} />{todo.title} </li> )
(完整示例見React components)
不用爲了改狀態去定義action(甚至爲了定義狀態去添reducer),要改直接改,不用經過類庫API。這一點與Vue數據綁定的優點相同,類庫本身能監聽到數據變化,不須要用戶手動通知變化,業務寫起來方便了
更強大的DevTools
Flux中action層的核心做用是讓狀態變化可追溯,action做爲狀態變化的緣由能夠被記錄下來(DevTools或logger),而MobX把函數名做爲action攜帶的緣由信息,經過spy實現狀態變化可追溯,能夠實現更強大的DevTools,好比讓組件的數據依賴可視化
組件級的精確數據綁定
相比react-redux,mobx-react能作到更精確的視圖更新,組件粒度的精確重渲染,不像react-redux須要從外部(Container)向下diff找到須要從新渲染的View,MobX明確知道數據依賴關係,不用找。那麼從性能上看,至少節省了找dirty View的成本
另外一個性能點是mobx-react去掉了Container的概念,其實是經過劫持組件生命週期的方式來實現的(具體見下面源碼簡析部分),這樣就減小了React組件樹深度,理論上性能會稍好一些
另外,由於依賴收集是由MobX完成的,帶來的好處是能分析出實際須要的數據依賴,避免了人爲產生的沒必要要的Container帶來的性能損耗
P.S.關於運行時依賴收集機制的更多信息,請查看運行時依賴收集機制
不限制state的結構
Flux要求state是個純對象,這樣不只強迫用戶花精力去設計state的結構,還強制把數據和相應操做分開了,用MobX的話來說:
But this introduces new problems; data needs to be normalized, referential integrity can no longer be guaranteed and it becomes next to impossible to use powerful concepts like prototypes.
限制state不能被隨意修改,這樣創建在數據模型上的一些原有優點就沒了,好比原型
而MobX對state的結構及類型都沒有什麼限制,MobX裏state的定義是:
Graphs of objects, arrays, primitives, references that forms the model of your application.
不要求單一狀態樹,也不要求純對象,例如:
class ObservableTodoStore { @observable todos = []; @observable pendingRequests = 0; constructor() { mobx.autorun(() => console.log(this.report)); } @computed get completedTodosCount() { return this.todos.filter( todo => todo.completed === true ).length; } @computed get report() { if (this.todos.length === 0) return "<none>"; return `Next todo: "${this.todos[0].task}". ` + `Progress: ${this.completedTodosCount}/${this.todos.length}`; } addTodo(task) { this.todos.push({ task: task, completed: false, assignee: null }); } } const observableTodoStore = new ObservableTodoStore();
這樣的state定義是MobX的基本玩法,不用從業務中剝離出共享數據,也不用擔憂當前的state結構可否知足未來的場景(之後有多條數據怎麼辦,數據量太大了怎麼辦,state結構要怎麼調整)……數據和相應操做能夠關聯在一塊兒,愛怎麼組織都行(用class,或者保持Bean + Controller)
在遷移現有項目時,更能突顯出不限制state結構的優點,不改變原有的model定義,侵入性很小,只須要添一些註解,就能得到狀態管理層帶來的好處,何樂不爲?想象一下給一個複雜的老項目上Redux,至少須要:
把共享狀態都提出來,做爲state
把對應的操做也都提出來,做爲reducer和saga,並保證reducer結構與state一致
定義action,把數據和操做關聯起來
在合適的地方插入Container
把全部修改state的部分都換成dispatch
……算了,成本極高,不建議重構
六.源碼簡析
mobx
核心部分是Observable,也就是負責完成@observable裝飾動做的部分:
export class IObservableFactories { box<T>(value?: T, name?: string): IObservableValue<T> { if (arguments.length > 2) incorrectlyUsedAsDecorator("box") return new ObservableValue(value, deepEnhancer, name) } shallowBox<T>(value?: T, name?: string): IObservableValue<T> { if (arguments.length > 2) incorrectlyUsedAsDecorator("shallowBox") return new ObservableValue(value, referenceEnhancer, name) } array<T>(initialValues?: T[], name?: string): IObservableArray<T> { if (arguments.length > 2) incorrectlyUsedAsDecorator("array") return new ObservableArray(initialValues, deepEnhancer, name) as any } shallowArray<T>(initialValues?: T[], name?: string): IObservableArray<T> { if (arguments.length > 2) incorrectlyUsedAsDecorator("shallowArray") return new ObservableArray(initialValues, referenceEnhancer, name) as any } map<T>(initialValues?: IObservableMapInitialValues<T>, name?: string): ObservableMap<T> { if (arguments.length > 2) incorrectlyUsedAsDecorator("map") return new ObservableMap(initialValues, deepEnhancer, name) } //...還有不少 }
(摘自mobx/src/api/observable.ts)
遞歸向下給數據身上都掛上getter&setter,例如Class Decorator的實現:
const newDescriptor = { enumerable, configurable: true, get: function() { if (!this.__mobxInitializedProps || this.__mobxInitializedProps[key] !== true) typescriptInitializeProperty( this, key, undefined, onInitialize, customArgs, descriptor ) return get.call(this, key) }, set: function(v) { if (!this.__mobxInitializedProps || this.__mobxInitializedProps[key] !== true) { typescriptInitializeProperty( this, key, v, onInitialize, customArgs, descriptor ) } else { set.call(this, key, v) } } } // 定義getter&setter if (arguments.length < 3 || (arguments.length === 5 && argLen < 3)) { Object.defineProperty(target, key, newDescriptor) }
(摘自mobx/src/utils/decorators.ts)
數組的變化監聽見mobx/src/types/observablearray.ts,與Vue的實現沒太大區別
mobx-react
「Container」的實現以下:
// 注入的生命週期邏輯 const reactiveMixin = { componentWillMount: function() {}, componentWillUnmount: function() {}, componentDidMount: function() {}, componentDidUpdate: function() {}, shouldComponentUpdate: function(nextProps, nextState) {} } // 劫持組件的生命週期 function mixinLifecycleEvents(target) { patch(target, "componentWillMount", true) ;["componentDidMount", "componentWillUnmount", "componentDidUpdate"].forEach(function( funcName ) { patch(target, funcName) }) if (!target.shouldComponentUpdate) { target.shouldComponentUpdate = reactiveMixin.shouldComponentUpdate } }
(摘自mobx-react/src/observer.js)
劫持組件聲明週期主要有3個做用:
把數據更新與UI更新關聯起來
把組件狀態暴露出去,接入DevTools
內置shouldComponentUpdate優化
react-redux經過setState({})來觸發Container更新,而mobx-react經過forceUpdate來觸發被劫持的View更新:
const initialRender = () => { if (this.__$mobxIsUnmounted !== true) { let hasError = true try { isForcingUpdate = true if (!skipRender) Component.prototype.forceUpdate.call(this) hasError = false } finally { isForcingUpdate = false if (hasError) reaction.dispose() } } }
(摘自mobx-react/src/observer.js)
接入DevTools的部分:
componentDidMount: function() { if (isDevtoolsEnabled) { reportRendering(this) } }, componentDidUpdate: function() { if (isDevtoolsEnabled) { reportRendering(this) } }
內置的shouldComponentUpdate:
shouldComponentUpdate: function(nextProps, nextState) { if (this.state !== nextState) { return true } return isObjectShallowModified(this.props, nextProps) }
(摘自mobx-react/src/observer.js)
參考資料
mobxjs/mobx
MobX
Ten minute introduction to MobX and React:結合React使用的例子