Observable 是 RxJS 的核心概念,是一切响应式操作的基础类,下面是 RxJS 文档中给出的基本示例:
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);
});
console.log('just before subscribe');
observable.subscribe({
next(x) { console.log('got value ' + x); },
error(err) { console.error('something wrong occurred: ' + err); },
complete() { console.log('done'); }
});
console.log('just after subscribe');
Observable 实例化的入参是一个函数,该函数接收 subscriber 作为参数,函数体内调用该参数的方法,如 next,error,complete。Observable 实例的使用方式是调用其 subscribe 方法,该方法的参数包含next、error、complete的具体实现。如果把 Observable 实例比作 JS 中的函数,那么给 Observable 实例绑定 subscribe 方法就相当于调用函数。你可能觉得有点奇怪,但事实上 Observable 只是函数和类的组合,下面我们来推导一下。
最初的实现
你可能会在项目中做一些“执行器”和“执行者”的抽象,比如把大流程的关键步骤抽象成函数A(执行器),把包含相似功能的小模块提取成函数B(执行者),这样在后面的维护中你就可以通过更改执行者来定制执行器所完成的工作。下面的函数实现了一个基本的“执行器”。
function executor(next: (n: number) => void) {
next(1);
next(2);
next(3);
const id = setTimeout(() => {
next(4);
}, 1000);
return () => {
clearTimeout(id);
};
}
executor 函数接收 next 函数作为参数,next 也就是“执行者”,它的值如果是 console.log
则控制台将依次打印 1234
,如果是 document.writeln
,则浏览器窗口将输入 1 2 3 4
。
后面迭代过程中,你可能觉得只有一个 next 参数已经不够用了,“执行器”需要新增报错和中断功能,于是你把入参变更了一下:
function executorPlus(init: {
next: (n: number) => void;
error: (e: Error) => void;
complete: () => void;
}) {
const { next, error, complete } = init;
next(1);
next(2);
next(3);
complete();
const id = setTimeout(() => {
next(4);
console.log(1);
}, 1000);
return () => {
clearTimeout(id);
};
}
现在“执行器”接收一个对象作为参数,该对象包含next, error, complete
三个方法,分别对应流程的进行、报错和完成。
接下来做一点 ts 层面的小重构,我们不妨把 init 对象的类型抽象为 Observer,添加泛型让它更加通用;executorPlus 返回值的类型则抽象为 Finalizer。
interface Observer<T> {
next: (n: T) => void;
error: (e: Error) => void;
complete: () => void;
}
type Finalizer = () => void;
function executorPlus(init: Observer<number>): Finalizer {
const { next, error, complete } = init;
next(1);
next(2);
next(3);
complete();
const id = setTimeout(() => {
next(4);
console.log(1);
}, 1000);
return () => {
clearTimeout(id);
};
}
抽象成 Observable
现在的 executorPlus 还是一个函数,但是它所实现的功能已经和 Observable.subscribe 很接近了,下面我们继续抽象,得到 Observable 类。
class Observable<T> {
constructor(private _init: (observer: Observer<T>) => Finalizer) {}
subscribe(observer: Observer<T>) {
return this._init(observer);
}
}
const instance = new Observable(executorPlus);
// 其实就相当于 executorPlus({ next: console.log, error: console.error, complete: console.info })
ObservableInstance.subscribe({ next: console.log, error: console.error, complete: console.info });
上面我们可以看到,Observable 实例只是将 executorPlus 进行了一个包装,执行 ObservableInstance.subscribe
和直接执行 executorPlus
没有差别。
现在"执行器”可以跑起来了,但在调用 complete 和 error 之后,我们其实是希望整个流程停下来的,然而此时“执行器”却能够继续运行。通过分析发现,Observer 应该存在“停止”和“运行”两种状态,当流程停止时所有的任务调度都将失效。怎么去实现这个功能呢?如果交给Observer 去处理,那就给 Observer 的提供者增加了心智负担 —— 他得去维护一个当前运行是否停止的状态,而 Observer 和 Observable 类之间的接口协议又是已经定下来的,所以明智的做法应该是使用一个中间层,它既解决了 Observer 的状态问题,又可以兼容 Observable 的接口协议。
下面继续抽象出 SafeSubscriber 类,它使用装饰器模式实现了 Observer 的状态管理,同时保留了 Observer 的接口特征。
class SafeSubscriber<T> implements Observer<T> {
private _stopped = false;
constructor(private _observer: Observer<T>) {}
// 这里使用 class properties 是因为 executorPlus 中进行了 init 的解构,需要保留this引用
next = (n: T) => {
if (!this._stopped) {
this._observer.next(n);
}
};
error = (err: Error) => {
if (!this._stopped) {
this._stopped = true;
this._observer.error(err);
}
};
complete = () => {
if (!this._stopped) {
this._stopped = true;
this._observer.complete();
}
};
}
Observable 需要应用这个中间层:
class Observable<T> {
constructor(private _init: (observer: Observer<T>) => Finalizer) {}
subscribe(observer: Observer<T>) {
const subscriber = new SafeSubscriber<T>(observer);
return this._init(subscriber);
}
}
副作用清除
通过上面的改造,由于 executorPlus 的 next(4)
发生在 complete()
之后,所以它已经不起作用了,但是console.log(1);
这句代码还是运行了,这是为什么呢?可能你已经注意到 executorPlus 执行时返回了一个清除 Timeout 的函数,Timeout 这类副作用通常在“执行器”流程运行时被注册,然后“执行器”返回一个清除副作用的函数,需要在 error 或者 complete 执行之后运行它。所以我们需要修改 SafeSubscriber 中间层,来达到这一效果。
class SafeSubscriber<T> implements Observer<T> {
private _stopped = false;
private _finalizerSet = new Set<Finalizer>();
constructor(private _observer: Observer<T>) {}
next = (n: T) => {
if (!this._stopped) {
this._observer.next(n);
}
};
error = (err: Error) => {
if (!this._stopped) {
this._stopped = true;
this._observer.error(err);
this.unsubscribe();
}
};
complete = () => {
if (!this._stopped) {
this._stopped = true;
this._observer.complete();
this.unsubscribe();
}
};
unsubscribe = () => {
for (const finalizer of this._finalizerSet) {
finalizer();
}
this._finalizerSet.clear();
};
add(finalizer: Finalizer) {
this._finalizerSet.add(finalizer);
}
}
SafeSubscriber 中新加入了 _finalizerSet
私有属性,收集所有“执行器”返回的副作用函数的集合。在 complete 或 error 执行后,调用 unsubscribe 清理所有副作用。Observable 类也需要对应的修改,往这个集合中添加副作用。
class Observeable<T> {
constructor(private _init: (observer: Observer<T>) => Finalizer) {}
subscribe(observer: Observer<T>) {
const subscriber = new SafeSubscriber<T>(observer);
const finalizer = this._init(subscriber);
subscriber.add(finalizer);
return {
unsubscribe: subscriber.unsubscribe
};
}
}
这样改造完后我们就拥有了一个可以清除副作用的 Observeable,现在看似一切都没有问题,但是再次运行代码我们发现 console.log(1);
这句代码仍然运行了,这是为什么呢?你可以暂停想一想问题出在哪。
function executorPlus(init: Observer<number>) {
const { next, error, complete } = init;
next(1);
next(2);
next(3);
complete();
const id = setTimeout(() => {
next(4);
console.log(1);
}, 1000);
return () => {
clearTimeout(id);
};
}
这个其实是 complete 同步执行带来的问题,仔细分析流程发现在 executorPlus 中 complete 语句的执行是同步的,执行 complete 后立即进行副作用清理,这都没问题,但是需要注意到 complete 执行时executorPlus函数还没有返回,_finalizerSet 还是空的,所以这里的 complete 并未清理掉 executorPlus 返回的 finalizer。整个执行链路如下图:
但是如果 complete 是异步的,就不存在这样的问题,执行链路如下:
那么如何去改进代码,使其在同步情况下依然成立呢?只需要更改一下 subscriber.add 的逻辑即可,在 finalizer 添加时判断一下,如果此时流程已经终止则直接清理掉副作用即可。
add(finalizer: Finalizer) {
if (this._stopped) {
finalizer();
} else {
this._finalizerSet.add(finalizer);
}
}
这样 Observable 的基本功能已经实现,下面是完整代码:
interface Observer<T> {
next: (n: T) => void;
error: (e: Error) => void;
complete: () => void;
}
type Finalizer = () => void;
function executorPlus(init: Observer<number>) {
const { next, error, complete } = init;
next(1);
next(2);
next(3);
complete();
const id = setTimeout(() => {
next(4);
console.log(1);
}, 1000);
return () => {
clearTimeout(id);
};
}
class SafeSubscriber<T> implements Observer<T> {
private _stopped = false;
private _finalizerSet = new Set<Finalizer>();
constructor(private _observer: Observer<T>) {}
next = (n: T) => {
if (!this._stopped) {
this._observer.next(n);
}
};
error = (err: Error) => {
if (!this._stopped) {
this._stopped = true;
this._observer.error(err);
this.unsubscribe();
}
};
complete = () => {
if (!this._stopped) {
this._stopped = true;
this._observer.complete();
this.unsubscribe();
}
};
unsubscribe = () => {
for (const finalizer of this._finalizerSet) {
finalizer();
}
this._finalizerSet.clear();
};
add(finalizer: Finalizer) {
if (this._stopped) {
finalizer();
} else {
this._finalizerSet.add(finalizer);
}
}
}
class Observable<T> {
constructor(private _init: (observer: Observer<T>) => Finalizer) {}
subscribe(observer: Observer<T>) {
const subscriber = new SafeSubscriber<T>(observer);
const finalizer = this._init(subscriber);
subscriber.add(finalizer);
return {
unsubscribe: subscriber.unsubscribe
};
}
}
const ob = new Observable(executorPlus);
ob.subscribe({
next: (...args) => console.log(args),
error: (...args) => console.error(args),
complete: (...args) => console.info(args),
});
相信阅读本文后你已经对 Observable 的来龙去脉有了一点了解,其实你可以看到并没有什么黑魔法,它不过是函数和类的组合封装而已。我们经常把 RxJS 和 Promise 对比,它们两者的确有很多相似点,但不同的是 Promise 是伴随浏览器微任务机制而生的 API,新建 Promise对象时提供的函数是同步执行的,而 Promise.then 的执行又是异步的,所以对于新手来说 Promise 的理解和使用反而要更困难一些。如果你的项目包含复杂的接口时序管理、副作用清除,或者拥有较多复杂动画和交互、又带点实时性的游戏,可以考虑使用一下 RxJS,最直接的收益是你的主逻辑代码不会被一大堆状态相关的flag管理语句淹没,对你来说有可能是一种解放。