跳到主要內容
黯羽輕揚每天積累一點點

MobX

免費2017-11-19#JS#MobX与Redux#MobX入门指南#MobX tutorial#MobX源码分析#MobX怎么读

比起 Redux 和 Vuex, MobX 有什麼優勢?

一.目標定位

Simple, scalable state management

一個簡單,夠用的狀態管理庫。還是想要解決應用狀態(數據)管理的問題

二.設計理念

Anything that can be derived from the application state, should be derived. Automatically.

源於應用狀態的所有東西,都應該自動得到。比如 UI,數據序列化,服務通訊

也就是說,只要知道哪些東西是狀態相關的(源於應用狀態),在狀態發生變化時,就應該自動完成狀態相關的所有事情,自動更新 UI,自動快取數據,自動通知 server

這種理念看似新奇,其實就是數據驅動,細想一下, 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.

參考了 MeteorJStrackerknockout 以及 Vue ,這幾個東西的共同特點是都內建了資料繫結,屬於所謂的 MVVM 架構,分別借鑒了:

  • MeteorJS 的設計理念:自動追蹤依賴( tracker, autorun 等等),不用再宣告依賴,讓很多事情變得更簡單

  • knockout 的資料繫結: ko.observable

  • 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 ,比如讓組件的數據依賴視覺化

mobx-react-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,至少需要:

  1. 把共享狀態都提出來,作為 state

  2. 把對應的操作也都提出來,作為 reducer 和 saga,並保證 reducer 結構與 state 一致

  3. 定義 action,把數據和操作關聯起來

  4. 在合適的地方插入 Container

  5. 把所有修改 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

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論