Pull 和 Push
是两种用来描述数据生产者(Data Producer)与数据消费者(Data Consumer)通信的不同协议。
什么是Pull模式?在一个Pull数据系统中,消费者Consumer决定了何时从生产者Producer接收数据,而生产者Producer本身对于何时传递数据是无感的。
比如我们最熟悉的Function,每一个JavaScript Function都是一个简单的Pull数据系统。函数本身就是一个数据生产者,而调用该函数的runtime通过pull该函数的single return value进行数据消费。
在ES2015中,引入了generator functions and iterators(function*),然后又多了另一种Pull数据系统的实现方式。执行迭代器iterator.next()就是从iterator这个数据生产者中pull数据进行消费。
Producer | Consumer | |
Pull | 被动:有数据请求到来时生产数据 | 主动:决定什么时候请求数据 |
Push | 主动:按照本身节奏生产数据 | 被动:响应式接收数据 |
什么是Push模式?在一个Push数据系统中,生产者Producer决定了何时发送数据给消费者Consumer。而消费者Consumer本身是对何时接收数据是无感的。
在现今的JS里,Promise是最常见的Push数据系统。在Promise里,Consumer通过then方法注册回调callbacks,而Promise作为Producer发送一个resolved value,但是和函数不同,Promise精确决定何时将数据push给callbacks。
RxJS引入Observable作为一种新的Push数据系统实现方式。每个Observable是一个包含多个甚至无限个数据的Producer,然后将这些数据push给Observer(Consumer)。
小结
以上对于两种不同数据通信方式的比较,各自存在几种不同的实现方式:
- Function是惰性执行的,只有调用时会在其执行中同步return单一数据
- generator本身也是Function,生成的iterator也是惰性执行,而与Function不同的是可以同步return(yield)零个或者无限个数据
- Promise可能return单一数据或者甚至not return anything
- Observable也是惰性执行,在其执行过程中,会不断同步或异步的return(next)零个、甚至无限个数据
Subscription
Function和Observable都是惰性执行的,如果你不调用函数或者不对Observable进行subscribe,是得不到value return或者side effects。
Subscribing to an Observable is analogous to calling a Function.
让一个Observable对一个observer进行subscribe,等价于调用一个Function;换句话说,Observable.subscribe是直接触发Observable data push的入口,这也是理解RxJS整体工作原理的关键。
下面所有用到的“Observable data push”都是指一个Observable实例subscribe了一个observer。
Function和Observable之间的区别是:Observable可以持续“return”多个值。例如
function foo() {
return 'one value';
return 'another value'; // dead code. the 'another value will never be returned'
}
// console
// 'one value'
而Observable可以
import { Observable } from 'rxjs';
const foo = new Observable(subscriber => {
subscriber.next('one value');
subscriber.next('another value'); // "return" another value
//...
});
foo.subscribe(console.log.bind(null));
// console
// 'one value'
// 'another value'Observable既可以同步也可以异步的传递数据
import { Observable } from 'rxjs';
const foo = new Observable(subscriber => {
subscriber.next('one value');
setTimeout(() => {
subscriber.next('another value'); // "return" another value, happens asynchronously
});
//...
});
foo.subscribe(console.log.bind(null));
console.log('after subscribe');
// console
// 'one value'
// 'after subscribe'
// 'another value' // returns asynchronously小结
- Function.call(),意味着同步得到一个return value
- Observable.subscribe,意味着同步或异步得到任意数量的return value
Observable
Observables,使得对异步调用或基于回调的代码组合更简单,它们是对包含多值集合的Lazy-Data-Push。
解剖Observable
Observables created using
new Observableor an creation operator(a special type of operator),are subscribed to with an Observer,execute to delivernext/error/completenotifications to the Observer。
Observable作为数据Producer,需要接受一个subscriber作为构造器参数,需要实现如下接口形式:
{
next: (value) => any,
error: (err) => any,
complete: () => any
}然后Observable.subscribe需要接收一个同样实现如上subscriber接口或者function next()的observer作为数据消费者,这样就形成了一个基于观察订阅模式的数据通信管道。这个管道的强大之处是因为Observable是一个pipeline,可以pipe各种各样的operator,从而在Observable data push过程中,对数据发送进行控制,包括数量限定(take、takeUntil)、类型转换(map、mapTo等)、时机控制、上下文切换(主要由scheduler完成)等方面。
Observable的pipeline机制
通过上面的分析,我们已经知道Observable是一个push数据系统,会不断的通过subscriber.next(value)的方式data push到已经订阅的subscriber这样的数据Consumer里。事实上,要想实现pipeline机制,要解决的问题无非就是如何让数据按顺序先流转到像operator这样的数据处理器里,然后最后到达被订阅的subscriber中。
一个subscriber是需要实现Subscription接口的,包含一个next方法。顾名思义,next即是“下一个”,正好符合pipeline的语意,只需要将所有operator通过next方法串联起来就可以了,那自然而然就想到只要在upstream subscriber的next方法中调用downstream subscriber的next方法即可。RxJS做了一个非常巧妙的设定,在生成operator实例时将downstream subscriber传入其构造器,通过destination属性串联起自身subscriber与刚传入的downstream subscriber,也可能是最终被订阅的subscriber,这样一个基于destination属性的subscriber linked list就建立起来了。
下面以mapoperator为例,observable.pipe(map(x => x+1)),从源码角度看看具体是如何实现的:
细节部分通过下面代码里的注释来说明
export function map<T, R>(project: (value: T, index: number) => R, thisArg?: any): OperatorFunction<T, R> {
return function mapOperation(source: Observable<T>): Observable<R> {
if (typeof project !== 'function') {
throw new TypeError('argument is not a function. Are you looking for `mapTo()`?');
}
// "source.lift" call the Observable which is passed through to lift the operator
// then create a new Observable instance
// and link the initial Observable which call the "pipe"
return source.lift(new MapOperator(project, thisArg));
};
}在Observable的pipe方法中:
pipe(...operations: OperatorFunction<any, any>[]): Observable<any> {
if (operations.length === 0) {
return this as any;
}
return pipeFromArray(operations)(this); // pass "this" of current Observable through
}export function pipeFromArray<T, R>(fns: Array<UnaryFunction<T, R>>): UnaryFunction<T, R> {
if (!fns) {
return noop as UnaryFunction<any, any>;
}
if (fns.length === 1) {
return fns[0];
}
// there the "input" is the initial instantiated Observable
// "fn(prev)" just call the "source.lift" above
return function piped(input: T): R {
return fns.reduce((prev: any, fn: UnaryFunction<T, R>) => fn(prev), input as any);
};
}通过对operator的lift方式,将最初实例化的Observable实例与新生成的Observable实例通过source属性链接起来生成Observable linked list,同时将当前的operator实例也挂载上去。这里的operator实例正是例如上面例子中提到的mapoperator函数中的new MapOperator(project, thisArg)。
lift(operator) {
const observable = new Observable();
observable.source = this;
observable.operator = operator;
return observable;
}当我们拿到最终的Observable实例链表后,开始subscribe某个具体的subscriber时,这个时候就会针对每个特定的operator实例生成自身的subscriber。还是以mapoperator为例,如下代码中operator的call方法中就生成了自己的new MapSubscriber(subscriber, this.project, this.thisArg),这里会将downstream subscriber传递进去, 同时会按顺序回溯Observable linked list中的Observable实例直到最初实例化的那个Observable实例。
export class MapOperator<T, R> implements Operator<T, R> {
constructor(private project: (value: T, index: number) => R, private thisArg: any) {
}
call(subscriber: Subscriber<R>, source: any): any {
// the "source" is just some very upstream Observable of the Observable linked list
return source.subscribe(new MapSubscriber(subscriber, this.project, this.thisArg));
}
}subscribe(observerOrNext?: PartialObserver<T> | ((value: T) => void) | null,
error?: ((error: any) => void) | null,
complete?: (() => void) | null): Subscription {
const { operator } = this;
const sink = toSubscriber(observerOrNext, error, complete);
// when the final Observable instance of the linked list of Observables subscribe some observer
// there the operator call the “call” instance method to make the subscriber of itself
if (operator) {
sink.add(operator.call(sink, this.source));
} else {
sink.add(
this.source || (config.useDeprecatedSynchronousErrorHandling && !sink.syncErrorThrowable) ?
this._subscribe(sink) :
this._trySubscribe(sink)
);
}
// ...
return sink;
}Observable和Scheduler的关系
什么是Scheduler
Scheduler控制Subscription什么时候开始以及数据消息什么时候传递,它主要由以下三部分组成:
Scheduler是一个数据结构。会依据action优先级或者其他因素诸如timer delay来存储和排列action。Scheduler是一个执行上下文。代表action的执行环境和执行时机(是立即执行,还是基于回调机制例如setTimeout、setInterval、process.nextTick或者animation frame)。Scheduler内部存在一个虚拟时钟。通过内部now()方法可以获取到当前timestamp。触发的action都可以通过这个虚拟时钟关联上这个timestamp。
Scheduler工作原理
A Scheduler lets you define in what execution context will an Observable deliver notifications to its Observer.Scheduler主要作用是决定Observable data push的执行上下文。
RxJS内建的scheduler主要有asyncScheduler、queueScheduler、asapScheduler和animationFrameScheduler。
queueScheduler、asapScheduler和animationFrameScheduler都是继承于asyncScheduler,都具有通过timer定时器setInterval的方式触发异步Observable data push。从接收到的timer delay来决定是启用timer还是自身实现的异步方式
asyncScheduler,维护了一个action queue,同时利用asyncAction以timer定时器setInterval的方式来将subscribe转换成异步执行。配合asyncScheduler的主要operator是subscribeOn。subscribeOn自身的work handler里是用于给upstream Observable source添加downstream subscriber的,利用asyncAction的timer方式实现了异步触发Observable data push。queueScheduler,只利用了asyncScheduler的action队列特性。主要的operator配合是observeOn。asapScheduler,内部是通过promise形式的micro task queue来实现异步。animationFrameScheduler,内部是通过requestAnimationFrame来实现异步。
Scheduler和Action
queueAction、asapAction和animationFrameAction都是继承于asyncAction
一个Action是一次调度动作。一次调度动作包含要执行动作的具体work handler和state,在work handler中对state进行处理。
asyncScheduler的asyncAction,内部work handler的执行是基于timer setInterval callback机制来触发。
protected requestAsyncId(scheduler: AsyncScheduler, id?: any, delay: number = 0): any {
return setInterval(scheduler.flush.bind(scheduler, this), delay);
}queueScheduler的queueAction,利用scheduler的action queue来按顺序保存当前的action,然后按队列中action顺序触发Observable data push。asapScheduler的asapAction,利用promise形式来触发work handler。Immediate.setImmediate内部是利用promise.resolve().then(cb)的形式模拟了setImmediate。
protected requestAsyncId(scheduler: AsapScheduler, id?: any, delay: number = 0): any {
// If delay is greater than 0, request as an async action.
if (delay !== null && delay > 0) {
return super.requestAsyncId(scheduler, id, delay);
}
// Push the action to the end of the scheduler queue.
scheduler.actions.push(this);
// If a microtask has already been scheduled, don't schedule another
// one. If a microtask hasn't been scheduled yet, schedule one now. Return
// the current scheduled microtask id.
return scheduler.scheduled || (scheduler.scheduled = Immediate.setImmediate(
scheduler.flush.bind(scheduler, undefined)
));
}- animationFrameScheduler的animationFrameAction,利用requestAnimationFrame来触发work hander。
protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: any, delay: number = 0): any {
// If delay is greater than 0, request as an async action.
if (delay !== null && delay > 0) {
return super.requestAsyncId(scheduler, id, delay);
}
// Push the action to the end of the scheduler queue.
scheduler.actions.push(this);
// If an animation frame has already been requested, don't request another
// one. If an animation frame hasn't been requested yet, request one. Return
// the current animation frame request id.
return scheduler.scheduled || (scheduler.scheduled = requestAnimationFrame(
() => scheduler.flush(undefined)));
}上面也提到Scheduler主要的作用是只是决定了Observable data push的执行上下文,而如何实现具体的调度schedule,需要用户自定义work handler,而在RxJS内部已经构造了几个特殊的operator,专门来配合Scheduler,比如subscribeOn、observeOn等。
下面就以subscribeOn为例来简单说明一下:
作为一个operator,subscribeOn也会经过lift然后call生成自身的subscriber
call(subscriber: Subscriber<T>, source: any): TeardownLogic {
return new SubscribeOnObservable<T>(
source, this.delay, this.scheduler
).subscribe(subscriber);
}在SubscribeOnObservable里面,传递了SubscribeOnObservable.dispatch作为work handler,而state正是{ source, subscriber },所以当配合asyncScheduler使用时,就可以在timer setInterval回调中执行source.subscribe(subscriber)从而实现了异步触发Observable data push。
static dispatch<T>(this: SchedulerAction<T>, arg: DispatchArg<T>): Subscription {
const { source, subscriber } = arg;
return this.add(source.subscribe(subscriber));
}
_subscribe(subscriber: Subscriber<T>) {
const delay = this.delayTime;
const source = this.source;
const scheduler = this.scheduler;
return scheduler.schedule<DispatchArg<any>>(SubscribeOnObservable.dispatch as any, delay, {
source, subscriber
});
}Subject
什么是Subject,它是一种特殊类型的Observable,它可以一次将数据传递给多个observer,而普通的Observable只能将数据push给单个observer。
A Subject is like an Observable, but can multicast to many Observers. Subjects are like EventEmitters: they maintain a registry of many listeners.
每个Subject是一个Observable,同样可以subscribe。而不同的是,在Subject内部,subscribe并不会马上触发Observable data push,而只是简单的将observer注册到自身的observer列表里去。类似于addEventListener。
_subscribe(subscriber: Subscriber<T>): Subscription {
//...
this.observers.push(subscriber);
return new SubjectSubscription(this, subscriber);
//...
}同时每个Subject又是一个Observer,具有next(v)、error(e)和complete()。当调用next(value)方法时,会把数据传递到所有已经注册的observer里去。可以说Subject是一个自生产自消费的数据系统。
因为Subject是一个特殊的Observable,所以它同样具有pipeline机制,但是和普通的Observable又有些不同,这点可以通过Subject类的lift方法可以很明显的看出来。
lift<R>(operator: Operator<T, R>): Observable<R> {
const subject = new AnonymousSubject(this, this);
subject.operator = <any>operator;
return <any>subject;
}从上面的lift方法中,会发现用于实现Observable pipeline机制的source属性并没有直接挂载到新产生的AnonymousSubject实例上。魔法就发生在这里的AnonymousSubject类,它是继承于Subject的,在构造实例时会接收当前Subject实例作为参数,而且它的构造器里只做了一件事,就是把当前的Subject实例挂载到新Subject的source属性上。
constructor(protected destination?: Observer<T>, source?: Observable<T>) {
super();
this.source = source;
}
next(value: T) {
const { destination } = this;
if (destination && destination.next) {
destination.next(value);
}
}
_subscribe(subscriber: Subscriber<T>): Subscription {
const { source } = this;
if (source) {
return this.source!.subscribe(subscriber);
} else {
return Subscription.EMPTY;
}
}从上面的代码实现我们就能知道,经过pipe之后生成的AnonymousSubject类实例链表,实际上无论是subscribe方法还是next方法,最终都是回溯到最初的Subject实例上去。由于Subject既是Observable又是Observer,所以AnonymousSubject类也正因为这个原因才要改写父类Subject的subscribe和next方法。
Observable和Redux
Observable的pipeline机制和redux的action处理流非常贴合,社区的redux-observable库也正是利用了RxJS的这个机制产出了另一种redux中间件。
const result$ = epic$.pipe(
map(epic => {
const output$ = epic(action$, state$, options.dependencies!);
// ...
return output$;
}),
mergeMap(output$ =>
from(output$).pipe(
subscribeOn(uniqueQueueScheduler),
observeOn(uniqueQueueScheduler)
)
)
);
result$.subscribe(store.dispatch);epic$作为一个Subject实例,通过下面的中间件的启动方式,action$这个Observable实例刚好经过了如上代码中的pipeline过程,同时actionSubject$作为action$的upstream source,通过action$的subscribe方法向上回溯从而成功订阅到里整个pipeline。同时redux-observable在整个pipeline中加入了queueScheduler来把所有的dispatch action推入队列中而按序执行。
epicMiddleware.run = rootEpic => {
// ...
epic$.next(rootEpic);
};在得到的中间件next函数中,当actionSubject$.next触发时,刚好通过上面得到的pipeline,action被传递到action$中,整个action处理流就开始运转。
action => {
// ...
actionSubject$.next(action);
// ...
};其实在redux本身库内,store上也存在一个observable方法,就是以上的最最简单版本实现。
总结
观察订阅模式是JavaScript设计模式中非常重要的一种,而且不只是存在于JavaScript世界中,使用场景非常广泛。在现今前端领域内,多数视图库、视图工具库或者视图框架都是基于该模式,像redux、react-redux、vue等等,对于前端程序员来说都非常熟悉。其实对于观察订阅模式背后的技术本质,我个人的理解它是一种数据通信方式,可以同步或异步,可以阻塞或者非阻塞。小到视图库,大到系统软件,它无处不在,加深对它的理解,可以让编写的应用更加健壮!