我爲何從Redux遷移到了Mobx

Redux是一個數據管理層,被普遍用於管理複雜應用的數據。可是實際使用中,Redux的表現差強人意,能夠說是很差用。而同時,社區也出現了一些數據管理的方案,Mobx就是其中之一。javascript

Redux的問題

Predictable state container for JavaScript appsjava

這是Redux給本身的定位,可是這其中存在不少問題。
首先,Redux作了什麼?看Redux的源碼,createStore只有一個函數,返回4個閉包。dispatch只作了一件事,調用reducer而後調用subscribelistener,這其中state的不可變或者是可變所有由使用者來控制,Redux並不知道state有沒有發生變化,更不知道state具體哪裏發生了變化。因此,若是view層須要知道哪一部分須要更新,只能經過髒檢查。react

再看react-redux作了什麼,在store.subscribe上掛回調,每次發生subscribe就調用connect傳進去mapStateToPropsmapDispatchToProps,而後髒檢測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能夠說是衆多數據方案中最完善的一個了。Mobx自己獨立,不與任何view層框架互相依賴,所以你能夠隨意選擇合適的view層框架(部分除外,例如Vue,由於它們的原理是同樣的)。閉包

目前Mobx(3.x)和Vue(2.x)採用了相同的響應式原理,借用Vue文檔的一張圖:
data.png
爲每一個組件建立一個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利用gettersetter(將來可能會出現一個平行的基於Proxy的版本)去收集組件實例的數據依賴關係,所以每單當一個點發生更新的時候,Mobx知道哪些組件須要被更新,決定哪一個組件更新的過程的時間複雜度是O(1)的,而Redux經過髒檢查每個connect的組件去獲得哪些組件須要更新,有n個組件connect這個過程的時間複雜度就是O(n),最終反映到Perf工具上就是JavaScript的執行耗時。

雖然在通過一系列優化後,Redux的版本能夠得到不輸Mobx版本的性能,當時Mobx不用任何優化就能夠獲得不錯的性能。而Redux最完美的優化是爲每個點創建單獨的store,這與Mobx等一衆精肯定位數據依賴的方案在思想上是相同的。

Mobx State Tree

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利用gettersetter來收集組件的數據依賴關係,從而在數據發生變化的時候精確知道哪些組件須要重繪,在界面的規模變大的時候,每每會有不少細粒度更新,雖然響應式設計會有額外的開銷,在界面規模大的時候,這種開銷是遠比對每個組件作髒檢查小的,所以在這種狀況下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.refobservable.shallow(Mobx),types.frozen(Mobx State Tree)。

本文首發於有贊技術博客

相關文章
相關標籤/搜索