使用RxJS管理React應用狀態的實踐分享
隨着前端應用的複雜度越來越高,如何管理應用的數據已經是一個不可迴避的問題。當你面對的是業務場景複雜、需求變動頻繁、各種應用數據互相關聯依賴的大型前端應用時,你會如何去管理應用的狀態數據呢?
我們認爲應用的數據大體上可以分爲四類:
- 事件:瞬間產生的數據,數據被消費後立即銷燬,不存儲。
- 異步:異步獲取的數據;類似於事件,是瞬間數據,不存儲。
- 狀態:隨着時間空間變化的數據,始終會存儲一個當前值/最新值。
- 常量:固定不變的數據。
RxJS
天生就適合編寫異步和基於事件的程序,那麼狀態數據用什麼去管理呢?還是用RxJS
嗎? 合不合適呢?
我們去調研和學習了前端社區已有的優秀的狀態管理解決方案,也從一些大牛分享的關於用RxJS
設計數據層的構想和實踐中得到了啓發:
- 使用
RxJS
完全可以實現諸如Redux
,Mobx
等管理狀態數據的功能。 - 應用的數據不是隻有狀態的,還有事件、異步、常量等等。如果整個應用都由
observable
來表達,則可以藉助RxJS
基於序列且可響應的的特性,以流的方式自由地拼接和組合各種類型的數據,能夠更優雅更高效地抽象出可複用可擴展的業務模型。
出於以上兩點原因,最終決定基於RxJS
來設計一套管理應用的狀態的解決方案。
原理介紹
對於狀態的定義,通常認爲狀態需要滿足以下3個條件:
- 是一個具有多個值的集合。
- 能夠通過
event
或者action
對值進行轉換,從而得到新的值。 - 有“當前值”的概念,對外一般只暴露當前值,即最新值。
那麼,RxJS
適合用來管理狀態數據嗎?答案是肯定的!
首先,因爲Observable
本身就是多個值的推送集合,所以第一個條件是滿足的!
其次,我們可以實現一個使用dispatch action
模式來推送數據的observable
來滿足第二個條件!
衆所周知,RxJS
中的observable
可以分爲兩種類型:
-
cold observable
: 推送值的生產者(producer
)來自observable
內部。- 將會推送幾個值以及推送什麼樣的值已在
observable
創建時被定義下來,不可改變。 -
producer
與觀察者(observer
) 是一對一的關係,即是單播的。 - 每當有
observer
訂閱時,producer
都會把預先定義好的若干個值依次推送給observer
。
- 將會推送幾個值以及推送什麼樣的值已在
-
hot observable
: 推送值的producer
來自observable
外部。- 將會推送幾個值、推送什麼樣的值以及何時推送在創建時都是未知的。
-
producer
與observer
是一對多的關係,即是多播的。 - 每當有
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
函數在內部調用了BehaviorSubject
的next
函數,推送了新數據,BehaviorSubject
的當前值更新了,也就是狀態更新了。不過寫起來略微繁瑣,我們對其進行了封裝,優化後寫法見下文。
使用操作符來創建狀態數據
我們自定義了一個操作符state
用來創建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
處理數據你不需要關注同步還是異步。
下面的例子我們使用操作符from
將promise
轉換爲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
,也許Redux
和Mobx
是更合適的選擇。
這套基於RxJS
的狀態管理方案,我們已經用於開發公司的商用項目,反饋還不錯。所以我們決定把這套方案整理成一個js lib
,取名爲:Floway
,並在github
上開源:
歡迎大家star
,更歡迎大家來共同交流和分享RxJS
的使用心得!
參考文章: