什么是RxJS?
ReactiveX: Reactive Extensions的缩写,一般简写为Rx
Rx是一个编程模型,目标是提供一致的编程接口,帮助开发者更方便的处理异步数据流
ReactiveX宣言
ReactiveX不仅仅是一个编程接口,它是一种编程思想的突破,它影响了许多其它的程序库和框架以及编程语言。
- Java: RxJava
- JavaScript: RxJS
- C#: Rx.NET
- C#(Unity): UniRx
- Scala: RxScala
- Clojure: RxClojure
- C++: RxCpp
- Lua: RxLua
- Ruby: Rx.rb
- Python: RxPY
- Go: RxGo
- Groovy: RxGroovy
- JRuby: RxJRuby
- Kotlin: RxKotlin
- Swift: RxSwift
- PHP: RxPHP
- Elixir: reaxive
- Dart: RxDart
可见,rxjs几乎精通所有编程语言!当代奇才!
好吧,那什么是rxjs
理解rxjs的逻辑
ReactiveX combines the Observer pattern with the Iterator pattern and functional programming with collections to fill the need for an ideal way of managing sequences of events.
ReactiveX结合了Observer模式和Iterator模式,以及函数式编程和集合,以满足对管理事件序列的理想方式的需求。
每一个Observable都有一条时间线,在这条时间线上可能发出多个事件,下图箭头前面的竖线代表这个Observable在这个时间点complete了,也就代表它的时间线结束了,以后不会再有事件发出。
Observable
Single | Multiple | |
---|---|---|
Pull | Function | Iterator |
Push | Promise | Observable |
它是可观察对象,上面的表格说明了,可以理解成是一个可以发送多次数据/状态的Promise
值得一提的是虽然名字叫Observable,但是它并不是观察者模式的标准实现,它不会记录谁观察了它(其实是不需要记录,因为每一个观察者得到的都是一个新的流-从头开始播!(其实就是新执行一次Observable的构造器参数中传入的函数),感兴趣可以了解单播多播的区别Discussion: Multicast vs. Unicast · Issue #66 · tc39/proposal-observable)
import { Observable } from 'rxjs';
const observable = new Observable(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
setTimeout(() => {
subscriber.next(4);
subscriber.complete();
}, 1000);
});
observable.subscribe(observer)
Observer
观察者,没啥好说的,就相当于Promise的then,catch,finally
const observer = {
next: x => console.log('Observer got a next value: ' + x),
error: err => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
};
Operators
定义对流上数据或者行为的操作,就是操作符了。真正理解或者学会rxjs,就看对操作符的掌握了。但是操作符很多还可以自定义,你并不需要学会所有的操作符再开始使用rxjs,就好像你不需要先精通一门编程语言再开始编程一样。其实rxjs很简单!非常简单!
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
of(1, 2, 3)
.pipe(map((x) => x * x))
.subscribe((v) => console.log(`value: ${v}`));
// Logs:
// value: 1
// value: 4
// value: 9
操作符广义上分为两种,一种是创建类的操作符,这种操作符不需要在pipe里,就像上面的of一样,他会直接返回一个Observable,第二种是非创建类的操作符,需要在pipe里,就像上面的map一样,他需要一个Observable作为输入,输出一个新的Observable。
Subscription
记录订阅行为的一个对象,通常用来取消订阅(有别的用法不过暂时不需要关注)
import { interval } from 'rxjs';
const observable = interval(1000);
const subscription = observable.subscribe(x => console.log(x));
subscription.unsubscribe();
Subject
它也是一个可观察对象,它就是观察者模式的标准实现了,记录所有观察它的观察者,同时它也是Observer可以观察别的可观察对象,多播。It is hot,Observable is clod(冷热的区别benlesh.medium.com/hot-vs-cold…
import { Subject } from 'rxjs';
const subject = new Subject<number>();
subject.subscribe({
next: (v) => console.log(`observerA: ${v}`)
});
subject.subscribe({
next: (v) => console.log(`observerB: ${v}`)
});
subject.next(1);
subject.next(2);
// Logs:
// observerA: 1
// observerB: 1
// observerA: 2
// observerB: 2
好了你现在学会了rxjs了!
好吧还需要介绍一些常用/简单的操作符来让大家快速上手使用一下。
创建类操作符
fromEvent
generate
interval
of
Transformation | Filtering
map
filter
debounceTime
throttleTime
switchMap
mergeMap
exhaustMap
操作符对比
感觉比较容易出现概念不清的就是如下几个了
swtichMap, exhaustMap, mergeMap, concatMap
这三个操作符都是针对higher-order-observable的,会订阅source observable,每当source observable 发送一个事件,就会发送一个新的Observable,然后打平整个流,让高阶流变成正常流。只不过内部的处理逻辑不同。
可以先看一个switchAll的弹珠图帮助理解higher-order-observable
再附上三个对应的弹珠图,来方便解释
swtichMap
简单说就是新的observable来了就不要老得了,喜新厌旧,渣男, 呸!
mergeMap
简单说就是小孩子才做选择,成年人当然是选择全都要,渣男, 呸!
exhaustMap
简单说就是上一个Observable没结束就不接收下一个Observable,这得错过多少好姑娘啊,老实人,活该你接盘,呸!
其实除了Map系列用来处理Higher-order Observables以外还存在All系列,如:swichAll。大家可以自行了解。
concatMap
如果别的女孩给你抛媚眼了先让她等等,等你跟现女友分手之后再无缝衔接
pipe原理
直接上源码,就是一个reduce,没啥黑魔法。函数式编程基本操作,也从侧面说明了Operator应该是一个纯函数。
pipe(...operations: OperatorFunction<any, any>[]): Observable<any> {
return pipeFromArray(operations)(this);
}
/** @internal */
export function pipeFromArray<T, R>(fns: Array<UnaryFunction<T, R>>): UnaryFunction<T, R> {
if (fns.length === 0) {
return identity as UnaryFunction<any, any>;
}
if (fns.length === 1) {
return fns[0];
}
return function piped(input: T): R {
return fns.reduce((prev: any, fn: UnaryFunction<T, R>) => fn(prev), input as any);
};
}
概念上和MobX的差异
见: 什么是RxJS,我个人的理解就是,RxJS更底层,更灵活,更适合用来 处理-异步-数据-流
MobX 对自己的定位就是专门用来做状态管理的。
比如说。我对MobX的状态管理方案不满意,我可以用RxJS自定义一个,但是做的是不是比MobX更好,就不一定了。我认为,RxJS更通用/底层一些。
适用场景和实践
个人觉得至少在我们业务系统中可以有如下几个用处:
状态管理:
export function useSubscribe<T>(
observable: Observable<T>,
callback: (value: T) => void
): void {
const ref = useRef(callback);
ref.current = callback;
useEffect(() => {
const subscription = observable.subscribe((value) => {
ref.current(value);
});
return () => {
subscription.unsubscribe();
};
}, [observable]);
}
export function useRxState<T>(subject: BehaviorSubject<T>): T;
export function useRxState<T>(subject: Subject<T>, initState: T): T;
export function useRxState<T>(subject: Subject<T>): T | undefined;
export function useRxState<T>(
subject: Subject<T>,
initState?: T
): T | undefined {
const [state, setState]: [T, Dispatch<T>] = useState(
subject instanceof BehaviorSubject
? cloneDeep(subject.getValue())
: cloneDeep(initState)
);
useSubscribe(subject, (value) => {
setState(cloneDeep(value));
});
return state;
}
export function selector<T, K>(fn: (value: T) => K) {
return (observable: Observable<T>): Observable<K> =>
observable.pipe(
map((value) => fn(value)),
distinctUntilChanged((x, y) => isEqual(x, y))
);
}
复杂异步交互:网络请求
export function fetchOperator<T = any>(request: RequestInfo, requestInit: RequestInit) {
return new Observable<T>((subscriber) => {
const abortController = new AbortController();
requestInit.signal = abortController.signal;
fetch(request, requestInit).then((res) => {
return res.json();
}).then((result) => {
subscriber.next({
data: result
});
subscriber.complete();
}).catch((err) => {
subscriber.error(err);
});
return () => {
if (!abortController.signal.aborted) {
abortController.abort();
}
}
})
}
export function fetchStream<R>(request: RequestInfo, requestInit: RequestInit, mapOprator: (project: (value: any, index: number) => any) => OperatorFunction<any, any> = switchMap) {
return (observable: Observable<any>) => {
return observable.pipe(mapOprator((value, index) => {
let requestClone = cloneDeep(request);
const requestInitClone = cloneDeep(requestInit);
if (requestInitClone.method === 'GET') {
if (typeof requestClone === 'string') {
if (requestClone.indexOf('?') === -1) {
requestClone = `${requestClone}?${qs.stringify(value)}`
} else {
requestClone = `${requestClone}&${qs.stringify(value)}`
}
}
} else {
requestInitClone.headers = Object.assign({
'Content-Type': 'application/json'
}, requestInitClone.headers);
if (!requestInitClone.body) {
requestInitClone.body = JSON.stringify(value);
}
}
if (!requestInitClone.credentials) {
requestInitClone.credentials = 'include';
}
return fetchOperator<R>(requestClone, requestInitClone)
}));
}
}
抽象复杂业务逻辑(管理副作用?)
其实这三个使用方法都已经有了比较成熟的一些方案,在angular的生态中有ngrs,ngxs。react生态中也有react-rxjs,longyinan大佬写的sigi等。我这里只是一个简单例子
import React, { useState, useEffect, useRef } from 'react';
import logo from './logo.svg';
import './App.css';
import { fetchStream, useBehaviorSubject, useSubject } from './rx/useRx';
import { debounce, debounceTime } from 'rxjs';
interface ResultType {
data: number;
}
function useBusinessEffect<T extends HTMLElement>(): [React.RefObject<T>, number | undefined] {
const state$ = useSubject<{
id: number;
name: string;
}>();
const [data, setData] = useState<number>();
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
const subscription = state$.pipe(
debounceTime(500),
fetchStream<ResultType>('https://www.fastmock.site/mock/4779aa4fe803e0771d94a8708f315730/test/api/test01', { method: 'GET' })).subscribe({
next: (result) => {
setData(result);
},
complete: (...args) => {
console.log('complete', args);
},
error: (...args) => {
console.log('error', args);
}
})
return () => {
subscription.unsubscribe();
}
}, []);
const ref = useRef<T>(null);
useEffect(() => {
const cb = () => {
const num = Math.round(Math.random() * 100)
state$.next({
id: num,
name: `${num}`
})
}
ref.current?.addEventListener('click', cb);
return () => {
ref.current?.removeEventListener('click', cb);
}
}, []);
return [ref, data];
}
function App() {
const [ref, data] = useBusinessEffect<HTMLButtonElement>();
return (
<div className="App">
<header className="App-header">
<button ref={ref}>点击</button>
<div>data: {data}</div>
</header>
</div>
);
}
export default App;
写在最后
字节跳动房产垂类招人,前端、node、webgl方向hc都挺多,在线等,挺急的。 如有意向可以扫描下方二维码,或者直接将简历发送到我的邮箱:qinyequan@bytedance.com