• 我为什么从Redux迁移到了Mobx


    Redux是一个数据管理层,被广泛用于管理复杂应用的数据。但是实际使用中,Redux的表现差强人意,可以说是不好用。而同时,社区也出现了一些数据管理的方案,Mobx就是其中之一。

    Redux的问题

    Predictable state container for JavaScript apps

    这是Redux给自己的定位,但是这其中存在很多问题。 首先,Redux做了什么?看Redux的源码,createStore只有一个函数,返回4个闭包。dispatch只做了一件事,调用reducer然后调用subscribe的listener,这其中state的不可变或者是可变全部由使用者来控制,Redux并不知道state有没有发生变化,更不知道state具体哪里发生了变化。所以,如果view层需要知道哪一部分需要更新,只能通过脏检查。

    再看react-redux做了什么,在store.subscribe上挂回调,每次发生subscribe就调用connect传进去mapStateToPropsmapDispatchToProps,然后脏检测props的每一项。当然,我们可以利用不可变数据的特点,去减少prop的数量从而减少脏检测的次数,但是哪有props都来自同一个子树这么好的事呢?

    所以,如果有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()
        }
    // ...
    12345678910111213141516
    

      

    更糟糕的是,每次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文档的一张图:
    为每个组件创建一个Watcher,在数据的getter和setter上加钩子,当组件渲染的时候(例如,调用render方法)会触发getter,然后把这个组件对应的Watcher添加到getter相关的数据的依赖中(例如,一个Set)。当setter被触发时,就能知道数据发生了变化,然后同时对应的Watcher去重绘组件。

    这样,每个组件所需要的数据时精确可知的,因此当数据发生变化时,可以精确地知道哪些组件需要被重绘,数据变化时重绘的过程是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)。

     
  • 相关阅读:
    热爱工作 发财机会大增(这里不是选择软件还是硬件的问题,是自己的性格和追求的问题)——当你的老板不如你懂行的时候,还赚的盆满钵满的时候,你就可以考虑独立了 good
    TaintDroid:智能手机监控实时隐私信息流跟踪系统(三)
    Delphi读取文件属性
    创业者该如何看待腾讯的“跟进”?
    IBM总裁郭士纳总结的四类人
    迅雷程浩:企业外包服务,下一个大的风口?(2B业务一定要懂销售和营销的人,这点和2C 不一样)
    dddd
    android:minSdkVersion 之我见
    dddd
    请人不怕啦
  • 原文地址:https://www.cnblogs.com/ygunoil/p/16479273.html
Copyright © 2020-2023  润新知