写在前面,读完文章你将获得:
- Rxjs 适合的使用场景
- 对于 Rxjs 的使用有个初步认知
背景
最近接手了一个类聊天窗口的项目,项目接收消息主要是通过ws被动更新,但包含着 mtop 等主动发消息的场景。而在17年,在业内出现了一股响应式变成的讨论,笔者也是在那时第一次了解到这个概念(初次知道了Rx.js),但当时只是在demo场景中浅尝则之,而根据「消息场景是天然适合响应式编程」这一映像,开始尝试在实际业务中使用Rx.js。
Wikipedia:
在计算中,响应式编程或反应式编程(英语:Reactive programming)是一种面向数据流和变化传播的声明式编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。
A | B | C A | B | C A | B | C
--------- ------------ ------------
Server 前端的数据层 前端的action
------------ ------------
Server 前端的reducer
------------
Server
数据层发展历程: React、Reflux、Redux,但对于异步流程的竞态问题,数据层框架依然没有很好地解决。
为了解决上述问题,MS 提出了响应式编程的概念,出现了ReactiveX的大家族,它的目标是建立一个库,专门处理异步流程中的流问题,可以被看作为loadash for stream。
结论
适合的场景
业务场景抽象出来的状态机,包含各种复杂状态,每个状态在时间维度上有先后关系,每个状态有 推/拉 操作驱动状态流转,状态的推/拉操作又是异步为主。
针对这种复杂的、多状态、异步、注重时序控制的场景,天然就适合Rx
- 实时数据更新:股票市场行情更新、实时聊天应用、在线游戏中的状态同步
- 异步事件处理:支持用户交互的前端应用程序,比如反应性UI、流媒体服务,处理视频和音频流
- 高并发和高吞吐量的系统:社交媒体动态更新流
- 复杂的数据流处理:数据处理流水线(Data pipeline),例如ETL过程
- 依赖异步IO操作的应用
缺点:在处理复杂的单页面数据层问题上是一个好用的工具,只是上手的复杂度较高(初次看代码的话不太能理解,需要了解操作符和按流的方式思考),调试不方便。所以,目前看来rxjs相对于它所带来的好处,成本代价太高,只适用于强实时的重型单页面架构。
场景Case
异步流程竞态
Case 1
在 UI 界面上会展示一个提示词,这个提示有三种状态,图一为普通状态,当选择了商品或者图片时,提示词要随着选择的事物做出改变。
import { useAppStore } from '@/infrastructure/store';
import { MessageType } from '@/sdk';
import { IMessageSender } from '@/types';
import { catchError, EMPTY, from, retry, Subject, switchMap } from 'rxjs';
import { container } from 'tsyringe';
interface IAssociateWord {
msgType?: MessageType;
}
const associateWord$ = new Subject<IAssociateWord>();
export const associateWordTrigger = (msgType?: MessageType) => associateWord$.next({ msgType });
associateWord$
.pipe(
switchMap(({ msgType }) =>
from(container.resolve<IMessageSender>(IMessageSender).getAssociate(msgType)).pipe(
retry(2),
catchError((err) => {
console.error('getAssociate error:', err);
return EMPTY;
}),
),
),
)
.subscribe((associateWords) => {
useAppStore.setState({ associateWords });
});
Case 2
有一个实时数据的面板,第一次打开要求要有数据。且每次打开都要刷新数据并且解读数据,且右上角的圆圈要求有loading状态。
import { useAppStore } from '@/infrastructure/store';
import { EMtop_Name, index } from '@/utils/mtop';
import { exhaustMap, filter, from, map, retry, shareReplay, Subject, tap } from 'rxjs';
const { setDatePreviewList } = useAppStore.getState();
const trigger$ = new Subject<{ needOperate: boolean }>();
export const dataPreviewTrigger = (needOperate = false) => {
trigger$.next({ needOperate });
};
export const dataPreview$ = trigger$.pipe(
exhaustMap(({ needOperate }) => from(
index(EMtop_Name.dataQuery, {})).pipe(map((data) => ({ data, needOperate })))
),
tap(({ data }) => setDatePreviewList(data.result)),
filter(({ needOperate }) => needOperate),
exhaustMap(() => from(index(EMtop_Name.dataOperate, {}))),
retry(2),
shareReplay(1),
);
dataPreview$.subscribe(
(value) => console.log('dataPreview$', value),
(error) => console.error('dataPreview$'),
);
const reloadData = () => {
setIsLoading(true);
dataPreviewTrigger(true);
};
useEffect(() => {
const subscription = dataPreview$.subscribe(
() => setIsLoading(false),
(err) => setIsLoading(false),
);
reloadData();
return () => subscription.unsubscribe();
}, []);
合流
Case 1
import { EEnterEvent } from '@/types';
import { EMtop_Name, index } from '@/utils/mtop';
import { history } from 'ice';
import { BehaviorSubject, combineLatest, from, map } from 'rxjs';
/* 商业化指引打开页面 */
type TOpenPageVal = {
eventName: EEnterEvent;
};
const fromServerOpenPage$ = from(index(EMtop_Name.enterEvent, {}, {}))
.pipe(map((val) => val.result));
const openPage$ = new BehaviorSubject<TOpenPageVal>({ eventName: EEnterEvent.None });
export function openPageTrigger(val: TOpenPageVal) {
openPage$.next(val);
}
combineLatest([openPage$, fromServerOpenPage$])
.pipe(map(([a, b]) => (a.eventName === EEnterEvent.None ? b : a)))
.subscribe((val: TOpenPageVal) => {
if (val?.eventName === EEnterEvent.NewGuidance) {
// goto guide page
history?.push('/guide');
}
});
Case 2
卡片的按钮区和选择区是两个上下文,按钮除了选择提交外,还可能有其他功能。
并且卡片在点击之后,会出现消失自己或者消失一整排或者不消失的情况。
const msgActButtonClk$ = actionButtonClick$.pipe(
tap((val) => {
messageActionLog('actionButtonClick$ filter', msgId);
sendAesEvent({ id: AesEventId.聊天页面_卡片_行动点, type: AesEventType.CLK, args: [msgId] });
}),
filter((i) => i.msgId === msgId),
share(),
);
const _commit$ = msgActButtonClk$.pipe(
filter((i) => i.actionButton.action.eventName === EEventNameType.CommitSelect),
withLatestFrom(
commitSelect$.pipe(
filter((i) => i.msgId === msgId),
startWith(null),
),
),
filter(([a, b]) => {
if (!b) Toast.show('至少选择一个选项');
return !!b;
}),
share(),
);
const _postMessage$ = msgActButtonClk$.pipe(
filter((i) => i.actionButton.action.eventName === EEventNameType.PostMessage),
share(),
);
const _openUrl$ = msgActButtonClk$.pipe(
filter((i) => i.actionButton.action.eventName === EEventNameType.OpenUrl),
share(),
);
const clickSubscription = merge(_commit$, _postMessage$, _openUrl$)
.pipe(
map((v) => {
if (Array.isArray(v)) {
return v[1] ? v[0] : null;
} else {
return v;
}
}),
)
.subscribe((val) => {
if (val) {
const { msgId, actionButton, index } = val;
const { clickStrategy } = actionButton;
switch (clickStrategy) {
case EClickStrategy.None:
break;
case EClickStrategy.HiddenAll:
dataManager.hideMessageActionButton(msgId);
break;
case EClickStrategy.HiddenSelf:
dataManager.hideMessageActionButton(msgId, index);
break;
}
}
});
总结
个人感觉 Rxjs 比较合适用在数据层,不必将所有行为都抽象成流的方式,例如原来 view 层处理用户事件可继续按照原来的方式。