聊一聊Observable和RxJS

2,109 阅读12分钟

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

FunctionObservable都是惰性执行的,如果你不调用函数或者不对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。

FunctionObservable之间的区别是: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 Observable or an creation operator(a special type of operator),are subscribed to with an Observer,execute to deliver next / error / complete notifications 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就建立起来了。

Observable-Pipeline.png

下面以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的执行上下文。

Scheduler.png

RxJS内建的scheduler主要有asyncSchedulerqueueSchedulerasapScheduleranimationFrameScheduler

queueSchedulerasapScheduleranimationFrameScheduler都是继承于asyncScheduler,都具有通过timer定时器setInterval的方式触发异步Observable data push。从接收到的timer delay来决定是启用timer还是自身实现的异步方式

  • asyncScheduler,维护了一个action queue,同时利用asyncAction以timer定时器setInterval的方式来将subscribe转换成异步执行。配合asyncScheduler的主要operator是subscribeOnsubscribeOn自身的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

queueActionasapAction和animationFrameAction都是继承于asyncAction

一个Action是一次调度动作。一次调度动作包含要执行动作的具体work handler和state,在work handler中对state进行处理。

  • asyncSchedulerasyncAction,内部work handler的执行是基于timer setInterval callback机制来触发。

protected requestAsyncId(scheduler: AsyncScheduler, id?: any, delay: number = 0): any {
  return setInterval(scheduler.flush.bind(scheduler, this), delay);
}

  • queueSchedulerqueueAction,利用scheduler的action queue来按顺序保存当前的action,然后按队列中action顺序触发Observable data push。
  • asapSchedulerasapAction,利用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,比如subscribeOnobserveOn等。

下面就以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的subscribenext方法。

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等等,对于前端程序员来说都非常熟悉。其实对于观察订阅模式背后的技术本质,我个人的理解它是一种数据通信方式,可以同步或异步,可以阻塞或者非阻塞。小到视图库,大到系统软件,它无处不在,加深对它的理解,可以让编写的应用更加健壮!