Definition / 定义
响应式编程是一种面向数据流 和变化传播 的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。
**引证解释 **
例如,对于 a=b+c 这个表达式的处理,在命令式编程中,会先计算 b+c 的结果,再把此结果赋值给 变量a,因此 b,c 两值的变化不会对 变量a 产生影响。但在响应式编程中,变量a 的值会随时跟随 b,c 的变化而变化。
Reactive programming is programming with asynchronous data streams.
Data Stream / 数据流
数据流可以看成是时间维度的数组
流的创建
JavaScript
import { from, of, fromEvent, interval, fromFetch } from 'rxjs';
// 以下返回类型都是observable
// 同步
from([1, 2, 3, 4, 5]); // 1,2,3,4,5
of(1, 2, 3, 4, 5); // 1,2,3,4,5
range(1, 5); // 从1开始,依次递增输出5个数,1,2,3,4,5
from('Hello World'); //'H','e','l','l','o',' ','W','o','r','l','d'
// 异步
fromEvent(document, 'click');
fromEvent(document, 'onmousemove');
from(new Promise(resolve => resolve('Hello World!'))); // 'Hello World!'
interval(1000); // 每秒依次输出0,1,2,3,4,5,6....
fromFetch(`https://api.github.com/search/users`);
流的操作
转换
JavaScript
range(1,3)
.pipe(map((v) => v * 10))
.subscribe((r) => {
console.log(r); // 10,20,30
});
过滤
JavaScript
range(0, 5)
.pipe(filter((x) => x % 2 === 1))
.subscribe((r) => {
console.log(r); // 1,3
});
组合
JavaScript
const stream1 = timer(0, 2000).pipe(
take(3),
map((v, index) => ["a", "b", "c"][index])
);
const stream2 = timer(1000, 2000).pipe(
take(3),
map((v, index) => ["d", "e", "f"][index])
);
merge(stream1, stream2).subscribe((r) => {
console.log(r); // 每秒依次输出a, d, b, e, c, f
});
Design Pattern / 设计模式
观察者模式
监听数据流的过程就是订阅,接受订阅的对象(即订阅拿到数据后的处理方法)叫做观察者/订阅者 实例:Vue数据双向绑定
发布订阅模式
发布 --- 订阅模式是基于观察者模式进行通用化设计,松散耦合,灵活度更高
订阅者不感知发布者
实例:Vue事件总线 EventBus
Implementation / 落地
Rxjs (ReactiveX.js)
Observable
Observable 是多个值的惰性推送集合。它填补了下面表格中的空白
单个值 | 多个值 | |
---|---|---|
拉取 | Function | Iterator |
推送 | Promise (立即执行) | Observable (惰性) |
-
什么是拉取? 在拉取体系中,由消费者来决定何时从生产者那里接收数据。生产者本身不知道数据是何时交付到消费者手中的。
-
什么是推送? 在推送体系中,由生产者来决定何时把数据发送给消费者。消费者本身不知道何时会接收到数据。
JavaScript
const observer = {
next: x => console.log(x),
error: err => console.error('something wrong occurred: ' + err),
complete: () => console.log('done'),
}
const observable1 = new Observable<number>(
subscriber =>{
subscriber.next(1);
subscriber.next(2);
subscriber.complete();
})
const observable2 = interval(1000).pipe(take(5));
observable1.subscribe(observer); // 1, 2, done
observable2.subscribe(observer);// 每秒依次输出0, 1, 2, 3, 4, done
observer -> 观察者
observable -> 可观察对象
subscriber -> 订阅者 == 观察者,一般在库内使用,外部api都用observer
subscribe() -> 订阅方法
Observable部分源码
JavaScript
export class Observable<T> implements Subscribable<T> {
......
constructor(subscribe?: (this: Observable<T>, subscriber: Subscriber<T>) => TeardownLogic) {
if (subscribe) {
this._subscribe = subscribe;
}
}
subscribe(observer?: Partial<Observer<T>>): Subscription {
const subscriber = new Subscriber(observer);
subscriber.add(this._trySubscribe(subscriber));
return subscriber;
}
protected _trySubscribe(sink: Subscriber<T>): TeardownLogic {
try {
return this._subscribe(sink);
} catch (err) {
sink.error(err);
}
}
......
}
-
Cold Observable
- 当cold Observable被observer订阅时,才开始推送数据
- Cold Observable推送的数据不在订阅者中共享
- 录播
- e.g http请求
-
Hot Observable
- 在订阅开始前就已经是在推送数据的状态了,订阅后收不到之前推送的数据
- Hot Observable推送的数据在所有订阅者中共享
- 直播
- e.g 鼠标移动事件
Subject (Hot Observable)
既是Observable,也是Observer
JavaScript
const observer1 = (v) => { console.log('observer1', v); }
const observer2 = (v) => { console.log('observer2', v); }
const subject = new Subject();
// 作为Observable
subject.suscribe(observer1);
subject.suscribe(observer2);
subject.next(1);
// observer1和observer2都输出1
// 作为Observer
Interval(1000).subscribe(subject); // observer1和observer2每秒输出1,2,3,4...
普通的Observable并不具备多路推送的能力(每一个Observer都有自己独立的执行环境),而Subject可以共享一个执行环境
子孙后代:BehaviorSubject, ReplaySubject ....
Operators / 操作符
@formily/reactive
主要是利用es6的Proxy去深度劫持响应式对象(Observable)
JavaScript
import { observable, autorun } from '@formily/reactive'
const obs = observable({
aa: {
bb: 123,
},
})
autorun(() => {
console.log(obs.aa.bb)
})
obs.aa.bb = 321
// 输出:
// 123
// 321
Examples / 举例一些适用场景
单个场景
双击事件
需求:
在某个时间段内(250ms)
-
连续点击三次甚至更多,应当被当成一次双击事件
-
只点击一次不能被当成双击事件
JavaScript
import { buffer, filter, fromEvent, debounceTime } from 'rxjs';
const clicks$ = fromEvent(document, 'click');
const doubleClicks$ = clicks$.pipe(
buffer(clicks$.pipe(debounceTime(250))),
map(bufferArr => bufferArr.length),
filter(len => len >= 2)
);
doubleClicks$.subscribe((r) => {
console.log('double click');
});
自动补全 / Typehead
当有新的输入时便不再关心之前请求的响应结果
只处理最新的请求的响应,以防旧的请求响应在新的请求响应之后返回
JavaScript
fromEvent(document.getElementById('myInput'), 'keyup')
.pipe(
map((e: KeyboardEvent) => (e.target as HTMLInputElement).value),
switchMap((value) => fromFetch(`https://api.github.com/search/users?q=${value}`))
)
.subscribe((data) => console.log(data));
swicthMap在同一时间只维护一个内部订阅;它可以取消正在进行的网络请求
switchMap : rxmarbles.com/#switchMap
Http请求重试
需求:
请求失败后需要至少重试2次,如果第一次重试通过,就不进行第二次重试
JavaScript
const request$ = fromFetch(`https://api.github.com/search/users?qa=w`);
request$.pipe(retry(2)).subscribe((r) => {
console.log(r);
});
综合场景
-
IM,即时通讯工具,聊天app
-
游戏 cocos creator+rxjs
-
......
总结
总的来说,响应式编程会非常适用于复杂的异步场景/事件驱动的场景,它可以帮助我们创建更娇小、更灵活并且更易于理解的代码,从而使我们更易于处理复杂的异步场景。
RxJS 是响应式编程在 JavaScript 中的实现,它为处理异步和基于事件的程序提供了强大而灵活的工具。
响应式编程在前端开发中有很多应用,如在 Vue 和 Angular 中,都有对响应式编程的应用,用于处理 UI 的不断变化。