使用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的使用心得!




參考文章:

相關文章
相關標籤/搜索