资讯专栏INFORMATION COLUMN

我为什么从Redux迁移到了Mobx

DevYK / 2295人阅读

摘要:需要注意的是,在中,需要把数据声明为。同时还提供了运行时的类型安全检查。在利用了,使异步操作可以在一个函数内完成并且可以被追踪。例如在中,数组并不是一个,而是一个类的对象,这是为了能监听到数据下标的赋值。

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

Redux的问题

Predictable state container for JavaScript apps

这是Redux给自己的定位,但是这其中存在很多问题。
首先,Redux做了什么?看Redux的源码,createStore只有一个函数,返回4个闭包。dispatch只做了一件事,调用reducer然后调用subscribelistener,这其中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()
    }
// ...

更糟糕的是,每次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 (
            
{count}
) } } ReactDOM.render( );
性能

在这篇文章中,作者使用了一个一个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 (
            
{count}
) } } ReactDOM.render( );

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 Some Component
}

ReactDOM.render(
    
        
    ,
    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)。

本文首发于有赞技术博客。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90088.html

相关文章

  • 【译】Redux 还是 Mobx,让来解决你的困惑!

    摘要:我现在写的这些是为了解决和这两个状态管理库之间的困惑。这甚至是危险的,因为这部分人将无法体验和这些库所要解决的问题。这肯定是要第一时间解决的问题。函数式编程是不断上升的范式,但对于大部分开发者来说是新奇的。规模持续增长的应 原文地址:Redux or MobX: An attempt to dissolve the Confusion 原文作者:rwieruch 我在去年大量的使用...

    txgcwm 评论0 收藏0
  • 前端每周清单半年盘点之 React 与 ReactNative 篇

    摘要:前端每周清单半年盘点之与篇前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。与求同存异近日,宣布将的构建工具由迁移到,引发了很多开发者的讨论。 前端每周清单半年盘点之 React 与 ReactNative 篇 前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点;分为...

    Barry_Ng 评论0 收藏0
  • ELSE 技术周刊(2017.12.04期)

    摘要:版本支持动态,对比九月支持的静态,动态会返回请求模块命名空间的对象以供使用使用开发浏览器插件的过程与收获初次认识是在年阿里的论坛会上,只知道它是运行在天生支持跨平台性的语言,好像很值得关注。 团队分享 React 整洁代码最佳实践 作为开发人员不能仅仅满足于代码可以工作,而应该让代码更易于编写,阅读和维护,这篇文章介绍了很多 clean code 在 React 应用开发上的最佳实践。...

    Salamander 评论0 收藏0
  • React组件设计实践总结05 - 状态管理

    摘要:要求通过要求数据变更函数使用装饰或放在函数中,目的就是让状态的变更根据可预测性单向数据流。同一份数据需要响应到多个视图,且被多个视图进行变更需要维护全局状态,并在他们变动时响应到视图数据流变得复杂,组件本身已经无法驾驭。今天是 520,这是本系列最后一篇文章,主要涵盖 React 状态管理的相关方案。 前几篇文章在掘金首发基本石沉大海, 没什么阅读量. 可能是文章篇幅太长了?掘金值太低了? ...

    ideaa 评论0 收藏0
  • Redux 的问题:React、MobX 和 Realm 能解决吗?

    摘要:它是由一个非常聪明的人开发的,用来缓解在单页面应用中管理状态的问题。的问题没有一种适合所有场景的完美工具。为设计的是世界的另一个新增内容,但目前仅适用于。这将导致最后期限延长,并且留下更多需要我们维护的代码。 原文:The Problems with Redux: Can React, MobX, and Realm save us? 作者:Erich Reich 首先,我不讨厌 ...

    snifes 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<