使用RxJS管理React应用状态的实践分享

随着前端应用的复杂度愈来愈高,如何管理应用的数据已是一个不可回避的问题。当你面对的是 业务场景复杂、需求变更频繁、各类应用数据互相关联依赖的大型前端应用时,你会如何去管理应用的状态数据呢?

咱们认为应用的数据大致上能够分为四类:javascript

  • 事件:瞬间产生的数据,数据被消费后当即销毁,不存储。
  • 异步:异步获取的数据;相似于事件,是瞬间数据,不存储。
  • 状态:随着时间空间变化的数据,始终会存储一个当前值/最新值。
  • 常量:固定不变的数据。

RxJS天生就适合编写异步和基于事件的程序,那么状态数据用什么去管理呢?仍是用RxJS吗? 合不合适呢?前端

咱们去调研和学习了前端社区已有的优秀的状态管理解决方案,也从一些大牛分享的关于用RxJS设计数据层的构想和实践中获得了启发:java

  1. 使用RxJS彻底能够实现诸如Redux,Mobx等管理状态数据的功能。
  2. 应用的数据不是只有状态的,还有事件、异步、常量等等。若是整个应用都由observable来表达,则能够借助RxJS基于序列且可响应的的特性,以流的方式自由地拼接和组合各类类型的数据,可以更优雅更高效地抽象出可复用可扩展的业务模型。

出于以上两点缘由,最终决定基于RxJS来设计一套管理应用的状态的解决方案。react

原理介绍

对于状态的定义,一般认为状态须要知足如下3个条件:git

  1. 是一个具备多个值的集合。
  2. 可以经过event或者action对值进行转换,从而获得新的值。
  3. 有“当前值”的概念,对外通常只暴露当前值,即最新值。

那么,RxJS适合用来管理状态数据吗?答案是确定的!github

首先,由于Observable自己就是多个值的推送集合,因此第一个条件是知足的!promise

其次,咱们能够实现一个使用dispatch action模式来推送数据的observable来知足第二个条件!异步

众所周知,RxJS中的observable能够分为两种类型:函数

  1. cold observable: 推送值的生产者(producer)来自observable内部。学习

    • 将会推送几个值以及推送什么样的值已在observable建立时被定义下来,不可改变。
    • producer与观察者(observer) 是一对一的关系,便是单播的。
    • 每当有observer订阅时,producer都会把预先定义好的若干个值依次推送给observer
  2. hot observable: 推送值的producer来自observable外部。

    • 将会推送几个值、推送什么样的值以及什么时候推送在建立时都是未知的。
    • producerobserver是一对多的关系,便是多播的。
    • 每当有observer订阅时,会将observer注册到观察者列表中,相似于其余库或语言中的addListener的工做方式。
    • 当外部的producer被触发或执行时,会将值同时推送给全部的observer;也就是说,全部的observer共享了hot observable推送的值。

RxJS提供的BehaviorSubject就是一种特殊的hot observable,它向外暴露了推送数据的接口next函数;而且有“当前值”的概念,它保存了发送给observer的最新值,当有新的观察者订阅时,会当即从BehaviorSubject那接收到“当前值”。

那么这说明使用BehaviorSubject来更新状态并保存状态的当前值是可行的,第三个条件也知足了。

简单实现

请看如下的代码:

import { BehaviorSubject } from 'rxjs';

// 数据推送的生产者
class StateMachine {
  constructor(subject, value) {
    this.subject = subject;
    this.value = value;
  }

  producer(action) {
    let oldValue = this.value;
    let newValue;
    switch (action.type) {
      case 'plus':
        newValue = ++oldValue;
        this.value = newValue;
        this.subject.next(newValue);
        break;
      case 'toDouble':
        newValue = oldValue * 2;
        this.value = newValue;
        this.subject.next(newValue);
        break;
    }
  }
}

const value = 1;  // 状态的初始值
const count$ = new BehaviorSubject(value);
const stateMachine = new StateMachine(count$, value);

// 派遣action
function dispatch(action) {
  stateMachine.producer(action);
}

count$.subscribe(val => {
  console.log(val);
});

setTimeout(() => {
  dispatch({
    type: "plus"
  });
}, 1000);

setTimeout(() => {
  dispatch({
    type: "toDouble"
  });
}, 2000);

执行代码控制台会打印出三个值:

Console

 1
 2
 4

上面的代码简单实现了一个简单管理状态的例子:

  • 状态的初始值: 1
  • 执行plus以后的状态值: 2
  • 执行toDouble以后的状态值: 4

实现方法挺简单的,就是使用BehaviorSubject来表达状态的当前值:

  • 第一步,经过调用dispatch函数使producer函数执行
  • 第二部,producer函数在内部调用了BehaviorSubjectnext函数,推送了新数据,BehaviorSubject的当前值更新了,也就是状态更新了。

不过写起来略微繁琐,咱们对其进行了封装,优化后写法见下文。

使用操做符来建立状态数据

咱们自定义了一个操做符state用来建立一个可以经过dispatch action模式推送新数据的BehaviorSubject,咱们称她为stateObservable

const count$ = state({
  // 状态的惟一标识名称
  name: "count",
    
  // 状态的默认值
  defaultValue: 1,
    
  // 数据推送的生产者函数
  producer(next, value, action) {
    switch (action.type) {
      case "plus":
        next(value + 1);
        break;
      case "toDouble":
        next(value * 2);
        break;
    }
  }
});

更新状态

在你想要的任意位置使用函数dispatch派遣action便可更新状态!

dispatch("count", {
  type: "plus"
})

异步数据

RxJS的一大优点就在于可以统一同步和异步,使用observable处理数据你不须要关注同步仍是异步。

下面的例子咱们使用操做符frompromise转换为observable

指定observable做为状态的初始值(首次推送数据)

const todos$ = state({
  name: "todos",
    
  // `observable`推送的数据将做为状态的初始值
  initial: from(getAsyncData())
    
  //...
  
});

producer推送observable

const todos$ = state({
  name: "todos",
    
  defaultValue: []
    
  // 数据推送的生产者函数
  producer(next, value, action) {
    switch (action.type) {
      case "getAsyncData":
        next(
          from(getAsyncData())
        );
        break;
    }
  }
});

执行getAsyncData以后,from(getAsyncData())的推送数据将成为状态的最新值。

衍生状态

因为状态todos$是一个observable,因此能够很天然地使用RxJS操做符转换获得另外一个新的observable。而且这个observable的推送来自todos$;也就是说只要todos$推送新数据,它也会推送;效果相似于Vue的计算属性。

// 未完成任务数量
const undoneCount$ = todos$.pipe(
  map(todos => {
    let _conut = 0;
    todos.forEach(item => {
      if (!item.check) ++_conut;
    });
    return _conut;
  })
);

React视图渲染

咱们可能会在组件的生命周期内订阅observable获得数据渲染视图。

class Todos extends React.Component {
  componentWillMount() {
    todos$.subscribe(data => {
      this.setState({
        todos: data
      });
    });
  }
}

咱们能够再优化下,利用高阶组件封装一个装饰器函数@subscription,顾名思义,就是为React组件订阅observable以响应推送数据的变化;它会将observable推送的数据转换为React组件的props

@subscription({
  todos: todos$
})
class TodoList extends React.Component {
  render() {
    return (
      <div className="todolist">
        <h1 className="header">任务列表</h1>
        {this.props.todos.map((item, n) => {
          return <TodoItem item={item} key={item.desc} />;
        })}
      </div>
    );
  }
}

总结

使用RxJS越久,越使人受益不浅。

  • 由于它基于observable序列提供了较高层次的抽象,而且是观察者模式,能够尽量地减小各组件各模块之间的耦合度,大大减轻了定位BUG和重构的负担。
  • 由于是基于observable序列来编写代码的,因此遇到复杂的业务场景,总能按照必定的顺序使用observable描述出来,代码的可读性很强。而且当需求变更时,我可能只须要调整下observable的顺序,或者加个操做符就好了。不再必由于一个复杂的业务流程改动了,须要去改好几个地方的代码(并且还容易改出BUG,笑~)。

因此,以上基于RxJS的状态管理方案,对咱们来讲是一个必需品,由于咱们项目中大量使用了RxJS,若是状态数据也是observable,对咱们抽象可复用可扩展的业务模型是一个很是大的助力。固然了,若是你的项目中没有使用RxJS,也许ReduxMobx是更合适的选择。

这套基于RxJS的状态管理方案,咱们已经用于开发公司的商用项目,反馈还不错。因此咱们决定把这套方案整理成一个js lib,取名为:Floway,并在github上开源:

欢迎你们star,更欢迎你们来共同交流和分享RxJS的使用心得!




参考文章:

相关文章
相关标签/搜索