对于开发者来说,rxjs 中最常接触的 api 莫过于 operator 了,按照功能的不同,目前版本的 rxjs 内置的 operator 分为十类:Creation、Join Creation、Transformation、Filtering、Join、Multicasting、Error Handling、Utility、Conditional and Boolean、Mathematical and Aggregate,在数量上则超过 100 个,能够满足几乎所有的场景,如果碰到无法很好适应的场景,也可以自定义一个属于自己的 operator
限于篇幅原因,所以不可能把这一百多个 operator 源码全部过一遍,也没必要,因为很多逻辑都共用的,我会从这十类 operator中,每类挑选几个作为范例进行源码解读
Creation
from
from 能够从 Array、array-like、Promise、iterable、Observable-like、readable-stream-like 这几类对象上创造出 Observable(即将这几类对象作为数据源),这几类对象几乎可以囊括所有的 js 对象了,所以 rxjs 给 from 的注释是:Converts almost anything to an Observable
例如,从 Array:
import { from } from 'rxjs';
const array = [10, 20, 30];
const result = from(array);
result.subscribe(x => console.log(x));
// Logs:
// 10
// 20
// 30
从 iterable:
import { from } from 'rxjs';
import { take } from 'rxjs/operators';
function* generateDoubles(seed) {
let i = seed;
while (true) {
yield i;
i = 2 * i;
}
}
const iterator = generateDoubles(3);
const result = from(iterator).pipe(take(10));
result.subscribe(x => console.log(x));
// Logs:
// 3
// 6
// 12
// 24
// 48
// 96
// 192
// 384
// 768
// 1536
既然能处理各种对象,那么其内部肯定是有对应的条件分支来做判断的,然后根据不同的对象类型,进入不同的处理方法,最终都生成一个 Observable
// node_modules/rxjs/src/internal/observable/from.ts
export function from<T>(input: ObservableInput<T>, scheduler?: SchedulerLike): Observable<T> {
return scheduler ? scheduled(input, scheduler) : innerFrom(input);
}
from 接收两个函数,第一个是数据源,第二个是 scheduler,之前文章已经说过这个东西了,就是让开发者有能力调度数据的流动逻辑,一般情况下,都是只传入第一个参数,那么就会执行 innerFrom方法
// node_modules/rxjs/src/internal/observable/innerFrom.ts
export function innerFrom<T>(input: ObservableInput<T>): Observable<T> {
if (input instanceof Observable) {
return input;
}
if (input != null) {
if (isInteropObservable(input)) {
return fromInteropObservable(input);
}
if (isArrayLike(input)) {
return fromArrayLike(input);
}
if (isPromise(input)) {
return fromPromise(input);
}
if (isAsyncIterable(input)) {
return fromAsyncIterable(input);
}
if (isIterable(input)) {
return fromIterable(input);
}
if (isReadableStreamLike(input)) {
return fromReadableStreamLike(input);
}
}
throw createInvalidObservableTypeError(input);
}
innerFrom 方法用于将输入的对象 input 转为 observable。其根据传入的对象类型,选择不同的处理逻辑的方法,isXXX是用于判定类型的方法,fromXXX 则是将不同的对象转为 observable的方法,例如对于 fromPromise
export function fromPromise<T>(promise: PromiseLike<T>) {
return new Observable((subscriber: Subscriber<T>) => {
promise
.then(
(value) => {
if (!subscriber.closed) {
subscriber.next(value);
subscriber.complete();
}
},
(err: any) => subscriber.error(err)
)
.then(null, reportUnhandledError);
});
}
内部就是new Observable,然后再将这个实例返回
fromEvent
fromEvent 的类型签名还是比较多的,但总体来说就是四个参数
target
target的合法类型有三种: HasEventTargetAddRemove、NodeStyleEventEmitter、JQueryStyleEventEmitter
export interface HasEventTargetAddRemove<E> {
addEventListener(
type: string,
listener: ((evt: E) => void) | EventListenerObject<E> | null,
options?: boolean | AddEventListenerOptions
): void;
removeEventListener(
type: string,
listener: ((evt: E) => void) | EventListenerObject<E> | null,
options?: EventListenerOptions | boolean
): void;
}
对于 HasEventTargetAddRemove,很明显,是针对 HTMLElement的,所以可以传入 dom元素,类似的,NodeStyleEventEmitter允许传入 nodejs 的 emitter对象,JQueryStyleEventEmitter则允许传入一个 jq 元素对象
eventName
事件名称,例如 click、focus
options
类型签名:
export interface EventListenerOptions {
capture?: boolean;
passive?: boolean;
once?: boolean;
}
就是针对 target 是 dom元素的情况下,addEventListener的第三个参数
resultSelector
相当于一个拦截器,可以决定最终传递给 subscribe 的值是什么,例如
let i = 0
fromEvent(div, 'click', (v) => {
if (i++ % 2 === 0) {
return v
}
return null
}).subscribe(e => {
console.log('click', e);
})
第偶数次 click事件的触发,subscribe接收到的 e 是点击事件,而如果是第奇数次,则 e 就是 null
下面看源码
// rxjs/src/internal/observable/fromEvent.ts
export function fromEvent<T>(
target: any,
eventName: string,
options?: EventListenerOptions | ((...args: any[]) => T),
resultSelector?: (...args: any[]) => T
): Observable<T> {
// ...
const [add, remove] =
// If it is an EventTarget, we need to use a slightly different method than the other two patterns.
isEventTarget(target)
? eventTargetMethods.map((methodName) => (handler: any) => target[methodName](eventName, handler, options as EventListenerOptions))
: // In all other cases, the call pattern is identical with the exception of the method names.
isNodeStyleEventEmitter(target)
? nodeEventEmitterMethods.map(toCommonHandlerRegistry(target, eventName))
: isJQueryStyleEventEmitter(target)
? jqueryMethods.map(toCommonHandlerRegistry(target, eventName))
: [];
}
isEventTarget、isNodeStyleEventEmitter、isJQueryStyleEventEmitter分别用于判断 target 是 dom元素还是 node event还是 jq对象,判断完了之后对不同类型的 target 执行对应的方法,返回 add 和 remove
// rxjs/src/internal/observable/fromEvent.ts
const nodeEventEmitterMethods = ['addListener', 'removeListener'] as const;
const eventTargetMethods = ['addEventListener', 'removeEventListener'] as const;
const jqueryMethods = ['on', 'off'] as const;
这里其实就是拿到 target 的添加监听事件方法和移除监听事件方法,然后在订阅的时候给 target 注册上监听事件,并在订阅清除的时候同时移除监听事件
// rxjs/src/internal/observable/fromEvent.ts
export function fromEvent<T>(
target: any,
eventName: string,
options?: EventListenerOptions | ((...args: any[]) => T),
resultSelector?: (...args: any[]) => T
): Observable<T> {
//...
return new Observable<T>((subscriber) => {
const handler = (...args: any[]) => subscriber.next(1 < args.length ? args : args[0]);
add(handler);
return () => remove!(handler);
});
}
timer & interval
timer: 延迟发送通知(
Used to emit a notification after a delay)interval: 周期性地发送通知(即轮询)(
Emits incremental numbers periodically in time)
// rxjs/src/internal/observable/timer.ts
export function timer(
dueTime: number | Date = 0,
intervalOrScheduler?: number | SchedulerLike,
scheduler: SchedulerLike = asyncScheduler
): Observable<number> {}
timer 的第一个参数是延迟时间,即决定延迟多长时间后执行,所以这是一个异步操作,内部会用到 scheduler
if (due < 0) {
due = 0;
}
let n = 0;
return scheduler.schedule(function () {
// ...
}, due);
关于 scheduler 之前文章已经说过了,这里就不再多说了,这里 scheduler.schedule(function () {}, due);你可以看成是 setTimeout(function () {}, due);
第二个参数 intervalOrScheduler,如果传入的是一个数字,则表示在延迟 dueTime 时间之后,每隔 intervalOrScheduler 时间执行一次,就相当于是 setInterval,而实现轮询的方法,就是在 schedule 里再调用 schedule
return scheduler.schedule(function () {
if (!subscriber.closed) {
// Emit the next value and increment.
subscriber.next(n++);
if (0 <= intervalDuration) {
// If we have a interval after the initial timer,
// reschedule with the period.
this.schedule(undefined, intervalDuration);
} else {
// We didn't have an interval. So just complete.
subscriber.complete();
}
}
}, due);
你也可以传入一个 scheduler,用于自定义调度,而如果你第二个参数传入了数字,并且还想传入一个 scheduler 的话,那么可以在第三个参数传入
interval 是 timer 的一个封装版,可以看成是 rxjs 中的 setInterval
// /rxjs/src/internal/observable/interval.ts
export function interval(period = 0, scheduler: SchedulerLike = asyncScheduler): Observable<number> {
if (period < 0) {
period = 0;
}
return timer(period, period, scheduler);
}
Join Creation Operators
concat
concat 可以将数个 Observables 组合成一个新的 Observable,并且在每个 Observable 结束后才继续执行下一個 Observable
const timer1 = interval(300).pipe(take(1));
const timer2 = interval(500).pipe(take(2));
const timer3 = interval(500).pipe(take(2));
对于上述代码,想让 timer1 执行完了再开始执行 timer2,timer2 执行完了再开始执行 timer3,如果不借助 concat 可以这么写:
timer1.subscribe({
next: console.log,
complete: () => timer2.subscribe({
next: console.log,
complete: () => timer3.subscribe({
next: console.log
})
})
})
这是我们应当避免写的 Callback Hell,如果借助 concat 就好多了
const result = concat(timer1, timer2, timer3)
result.subscribe(x => console.log(x))
// /rxjs/src/internal/observable/concat.ts
export function concat(...args: any[]): Observable<unknown> {
return concatAll()(from(args, popScheduler(args)));
}
// /rxjs/src/internal/util/args.ts
export function popScheduler(args: any[]): SchedulerLike | undefined {
return isScheduler(last(args)) ? args.pop() : undefined;
}
concatAll下面会提到,from 前文说过,就是将输入的值转换为 Observable的方法,args 是 concat 的参数,也就是即将合并的数据流
那么源码的意思就比较明显了,即将 concat接收的数据流一个个地传递给 concatAll,由于 concatAll 具有顺序化执行数据流的功能,所以 concat传入的数据流也就必须一个个执行,上一个数据流结束了才能轮到下一个
Transformation Operators
mergeMap
把所有
Observable合并到同一个数据流內,不会退订上一次的Observable,也不会等待上一次的Observable结束,平行处理所有的Observable
const letters = of('a', 'b', 'c')
const result = letters.pipe(
mergeMap(x => interval(1000).pipe(map(i => x + i)))
)
result.subscribe(x => console.log(x))
// a0
// b0
// c0
// a1
// b1
// c1
// ...
等价于
const letters = of('a', 'b', 'c')
letters.subscribe({
next: x => {
interval(1000).pipe(map(i => x + i)).subscribe(x => console.log(x))
}
})
所以实际上,mergeMap 就是进一步的封装(当然了,rxjs内基本所有操作符都是进一步封装的结果),如果存在 resultSelector,那么无需你在手动去 subscribe了,直接 mergeMap就行了
看 mergeMap 的源码,看到主要是调用了 mergeInternals,所以直接看这个
// /rxjs/src/internal/operators/mergeInternals.ts
const doInnerSub = (value: T) => {
// ...
innerFrom(project(value, index++)).subscribe(
new OperatorSubscriber(
subscriber,
(innerValue) => {
// 。。。
subscriber.next(innerValue);
},
// ...
)
主要的就是 subscriber.next(innerValue) 这一句
project 是作为参数传入 mergeMap 的函数,value 是原 Observable 发出的值,index 代表是 Observable 第几次(从 0 开始)发出的事件,innerFrom 上面说过了,是将传入的参数转为 Observable 的方法
在这里,对 mergeMap 传入的 Observable进行了 subscribe,每次 Observable 发出值的时候,就会执行 subscriber.next(innerValue),而 subscriber 就是最外面 mergeMap 隶属的那条数据流
那么数据流动就很清晰了,mergeMap 对所有内部产生的数据流进行了订阅,并且在这些数据流产生数据的同时,对数据进行加工,再传给最外面唯一的主数据流,即数据的出口只有一个,从而给外界产生单数据流的结果
map
类似于 Array.prototype.map(),将
Observable每次的值通过转换函数转成另外的值
const letters = of('a', 'b', 'c');
const result = letters.pipe(map((v, i) => v + i));
result.subscribe(x => console.log(x));
// a0
// b1
// c2
// /rxjs/src/internal/operators/map.ts
export function map<T, R>(project: (value: T, index: number) => R, thisArg?: any): OperatorFunction<T, R> {
return operate((source, subscriber) => {
let index = 0;
source.subscribe(
new OperatorSubscriber(subscriber, (value: T) => {
subscriber.next(project.call(thisArg, value, index++));
})
);
});
}
operate 是 rxjs 内部创建操作符的通用方法,很多操作符都会使用这个方法来创建,其内部主要是调用了 Observable 的 lift 方法,方便操作符之间的链式调用的,这个之前文章已经说过了,至于 OperatorSubscriber 也就是一个 Subscriber,不再多说
map 里也是调用了 Observable 的 subscribe 方法,主要是 project.call(thisArg, value, index++) 这一句,project 就是 map传入的那个函数参数,这个函数接收两个参数,第一个参数就是原始 Observable发出的值,第二个参数记录目前的值是 Observable 第几次(从 0 开始)发出的事件,经过 map 处理之后的结果再交给 subscriber.next 做为最终值发出
可以看到,map回调函数的第二个参数 index,其初始化是在 subscribe 外面,所以每 subscribe 一次,其值就会递增
scan
作用类似于
reduce,并且会在每次更新的时候抛出当前的累计值
of(1, 2, 3).pipe(
scan((total, c) => total + c, 0)
).subscribe(console.log)
// 1
// 3
// 6
scan 内部调用了 scanInternals,直接看这个
// /rxjs/src/internal/operators/scanInternals.ts
let hasState = hasSeed;
let state: any = seed;
let index = 0;
source.subscribe(
createOperatorSubscriber(
subscriber,
(value) => {
const i = index++;
state = hasState
? // We already have state, so we can get the new state from the accumulator
accumulator(state, value, i)
: // We didn't have state yet, a seed value was not provided, so
// we set the state to the first value, and mark that we have state now
((hasState = true), value);
// Maybe send it to the consumer.
emitOnNext && subscriber.next(state);
},
// ..
)
);
scanInternals 内部维护了两个变量 state、index,分别用户存储当前计算的累计值和当前累计的次数(从 0 开始),每次订阅的时候,都会将 state 累加上新传入的值,且将 index递增
switchMap
将上游的
Observable映射成另外的Observables,并且每次接到上游数据流的时候,都会取消对前一个映射数据流的订阅
timer(0, 2000).pipe(
map(() => interval(500)),
mergeAll(),
).subscribe(console.log)
// 0,1,2,3... 0,1,2,3...
每隔 2000ms,上游就会产生新的数据流,被 switchMap 转换成其他的数据流,同时会取消对前一个数据流的订阅,所以上述代码会循环输出 0 1 2 3 4
而如果每次生成新数据流的时候,不取消对前一个数据流的订阅,那么就会出现多个数据流同时存在的情况,输出的结果就是多条数据流订阅结果的混合了,这个特性也正是 switchMap 和其他打平操作符的主要区别
// /rxjs/src/internal/operators/switchMap.ts
innerSubscriber?.unsubscribe();
let innerIndex = 0;
const outerIndex = index++;
// Start the next inner subscription
innerFrom(project(value, outerIndex)).subscribe(
(innerSubscriber = createOperatorSubscriber(
subscriber,
(innerValue) => subscriber.next(resultSelector ? resultSelector(value, innerValue, outerIndex, innerIndex++) : innerValue),
() => {
innerSubscriber = null!;
checkComplete();
}
))
);
innerSubscriber 就是上一个订阅的数据流,每次转换新的数据流之前,会调用 innerSubscriber?.unsubscribe();,也就是取消对数据流的订阅,innerFrom前面说过了,就是生成 observable 的方法,取消和重新生成的逻辑就在这短短几行代码里了
Filtering Operators
take
设定只从
Observable中接收N次事件的值。当订阅开始后,如果发生过的事件次数已经达到我们设定的,就会结束当前数据流
interval(500).pipe(
take(3)
).subscribe(console.log)
// 0
// 1
// 2
// /rxjs/src/internal/operators/take.ts
export function take<T>(count: number): MonoTypeOperatorFunction<T> {
return count <= 0
() => EMPTY
: operate((source, subscriber) => {
let seen = 0;
source.subscribe(
createOperatorSubscriber(subscriber, (value) => {
if (++seen <= count) {
subscriber.next(value);
if (count <= seen) {
subscriber.complete();
}
}
})
);
});
}
// /rxjs/src/internal/observable/empty.ts
export const EMPTY = new Observable<never>((subscriber) => subscriber.complete());
当设定次数小于 0 时,立即结束(完成)当前数据流,这是个边界判断
否则,会有个变量 seen 进行计数,每次 next之前,此值会递增,当 count <= seen 的时候,说明已经执行了我们预设的数字,此时结束(完成)当前数据流
Join Operators
mergeAll
mergeAll 和 mergeMap 的作用很相似,实际上 mergeAll 内部就调用了 mergeMap,区别点在于,上游的数据流在经过 mergeMap 的时候,会被 mergeMap 内部传入的事先定义好的函数进一步处理,得到新的数据结果,然后再让数据结果继续往下流动;而 mergeAll 只是对上游的数据进行兼容处理,无法做额外的数据处理,实际上,mergeMap 完全可以取代 mergeAll,源码层面,mergeAll 也是调用了 mergeMap,算是进一步的功能封装
概念不好理解,看例子会更清晰些
of(1).pipe(
map(v => v)
).subscribe(console.log)
// 1
上述代码的输出明显,就是 1,现在改一下
of(1).pipe(
map(v => of(v))
).subscribe(console.log)
// Observable2 {_subscribe: ƒ}
这时输出的是一个 Observable,想要取得这个 Observable的值,还得再次 subscribe一次
of(1).pipe(
map(v => of(v)),
).subscribe(v => v.subscribe(console.log))
而如果借助 mergeAll
of(1).pipe(
map(v => of(v)),
mergeAll()
).subscribe(console.log)
// 1
就不需要进一步 subscribe,本例子还可以使用 mergeMap
of(1).pipe(
map(v => of(v)),
mergeMap(v => v)
).subscribe(console.log)
// 1
但 mergeMap 能做得更多,例如,可以改变原数据的值
of(1).pipe(
map(v => of(v)),
mergeMap(v => v.pipe(map(n => n *2)))
).subscribe(console.log)
// 2
原数据是 1,经过 mergeMap处理后变成了 2
mergeAll 可以接收一个参数,这个参数能够控制同时最多并发订阅的 observable对象数量
of(0, 1, 2, 3).pipe(
map(v => of(v).pipe(delay(1000))),
mergeAll()
)
.subscribe(console.log)
上述代码将在执行后 1000ms的时候,同时输出 0 1 2 3
of(0, 1, 2, 3).pipe(
map(v => of(v).pipe(delay(1000))),
mergeAll(2)
)
.subscribe(console.log)
上述代码将在执行后 1000ms的时候,同时输出 0 1,然后再等待 1000ms,再同时输出 2 3
由于 mergeMap 是完全可以取代 mergeAll的,所以 mergeMap 也可以实现
of(0, 1, 2, 3).pipe(
map(v => of(v).pipe(delay(1000))),
mergeMap(v => v, 2)
)
.subscribe(console.log)
mergeAll 内部调用了 mergeMap,最终都是调用了 mergeInternals
// /rxjs/src/internal/operators/mergeInternals.ts
const outerNext = (value: T) => (active < concurrent ? doInnerSub(value) : buffer.push(value));
这里有个判断 active < concurrent,active 初始值为 0,并且每执行一次 doInnerSub(value)就会累计 1,concurrent 是 mergeAll 的参数,如果不传这个值就是 Infinity,也就是说,如果 mergeAll不传参,则这个表达式永远执行 doInnerSub(value)
doInnerSub 就是执行数据流订阅的方法,当同时在执行中的 observable 数量大于 concurrent 时,就会将后面的数据流数据先暂存到 buffer中,当有 observable 执行完毕后,则将 active--,并对 buffer里暂存的数据生成 observable 并进行订阅,即执行 doInnerSub
// /rxjs/src/internal/operators/mergeInternals.ts
try {
active--;
while (buffer.length && active < concurrent) {
const bufferedValue = buffer.shift()!;
if (innerSubScheduler) {
executeSchedule(subscriber, innerSubScheduler, () => doInnerSub(bufferedValue));
} else {
doInnerSub(bufferedValue);
}
}
checkComplete();
} catch (err) {
subscriber.error(err);
}
concatAll
concatAll 实际上是 concat的高阶 Observable操作符,就是说 concatAll 不需要亲自一个个地去接收数据流作为参数,然后再合并了,它只要在 pipe中无脑接收上游数据流就行
// /rxjs/src/internal/observable/concat.ts
export function concatAll<O extends ObservableInput<any>>(): OperatorFunction<O, ObservedValueOf<O>> {
return mergeAll(1);
}
经过前面 mergeMap 和 mergeAll 源码的讲解,concatAll就很清晰了,就是执行了 mergeAll(1),意思就是每次只能同时订阅一个数据流,上一个数据流结束了才能开始下一个
of(0, 1, 2).pipe(
map(v => of(v).pipe(delay(1000))),
concatAll()
).subscribe(console.log)
上述代码执行后,经过 1s输出 0,再经过 1s输出 1,再经过 1s输出 2,结束
Utility Operators
tap
拦截源上的每个
observable的数据并对数据进行修改,接着再把这个observable继续传递下去
一般而言,operator 在处理完上游的数据之后,会产生一个新的 observable 往下继续传递,但是 tap 不同,它虽然也会修改数据流,但并不会产生新的 observable,它只是修改上游的 observable,然后再把修改后的 observable 传递下去,所以会有 副作用
tap 接收的参数和 observable.subscribe 一样,所以你可以把它看成是一个订阅操作
interval(500).pipe(
tap(console.log),
map(n => n % 2 ? 'even' : 'odd')
).subscribe(console.log)
// 0
// even
// 1
// add
// ...
// /rxjs/src/internal/operators/tap.ts
source.subscribe(
createOperatorSubscriber(
subscriber,
(value) => {
tapObserver.next?.(value);
subscriber.next(value);
},
() => {
isUnsub = false;
tapObserver.complete?.();
subscriber.complete();
},
(err) => {
isUnsub = false;
tapObserver.error?.(err);
subscriber.error(err);
},
() => {
if (isUnsub) {
tapObserver.unsubscribe?.();
}
tapObserver.finalize?.();
}
)
);
subscriber 执行什么操作, tapObserver 就执行什么操作,并且 tapObserver的操作在 subscriber之前。 subscriber 外部是拿不到的,而外部却能控制 tapObserver,所以算是一种拦截处理
Mathematical and Aggregate Operators
reduce & max
reduce 和 scan 很类似,二者都是调用了 scanInternals,只是参数值不同罢了,唯一的区别是,scan 会在每次遍历时将当前累加值 emit出来,而 reduce 只在数据流完成(complete)的时候,才会把最终的累计值 emit 出来,所以使用 reduce的时候,必须要保证数据流能够结束(complete),否则 reduce 不会有结果的
max 类似于 Math.max,用于查找当前数据流中最大的数据值,当然,最大 这个概念实际上取决于你传入的 comparer 函数
利用 reduce
// /rxjs/src/internal/operators/max.ts
export function max<T>(comparer?: (x: T, y: T) => number): MonoTypeOperatorFunction<T> {
return reduce(isFunction(comparer) ? (x, y) => (comparer(x, y) > 0 ? x : y) : (x, y) => (x > y ? x : y));
}
interval(100).pipe(
map(() => of(Math.random())),
mergeAll(),
take(5),
max((a, b) => a < b ? -1 : 1),
).subscribe(console.log)
// 0.9138789699353005
小结
刚开始看 operator 的时候,会感觉很乱,但当摸清楚了套路的时候,就知道哪些是主干哪些是无关紧要的细枝末节了,operator 是 rxjs 中使用频率最高的 api,了解了其实现原理,有助于我们自行实现定制化的 operator