RxJS详解:你想了解的RxJS知识这里都有

293 阅读10分钟

30天精通RxJS

响应式编程(Reactive Programming)

  • 响应式编程,即是使用异步数据流进行编程

在异步编程模式中,我们有两种获得任务执行结果的方式:

    • 主动轮训,我们把它称为 Proactive 方式。
    • 被动接收反馈,我们称为 Reactive 方式。

简单来说,在 Reactive 方式中,上一个任务的结果的反馈就是一个事件,这个事件的到来将会触发下一个任务的执行。这也就是 Reactive 的内涵。

RxJS初探

异步常见问题

标题-202404092137.png

异步API的多样性

我们除了要面对异步会遇到的各种问题外,还要烦恼很多不同的api写法,

  • DOM Events
  • XMLHttpRequest
  • fetch
  • WebSockets
  • Server Send Events
  • Service Worker
  • Node Stream
  • Timer

上面列的api都是异步的,但他们都有各自的api及写法。如果我们使用rxjs,就能用形式相近的api统一写法。

示例:监听点击事件,点击一次后不再监听

// 原生JS
var handler = (e) => {
  console.log(e)
  document.body.removeEventListener('click', handler) // 结束监听
}
// 注册监听
document.body.addEventListener('click', handler)
/* ============================================================================ */
// rxjs
Rx.Observable.fromEvent(document.body, 'click') // 注册监听
  .take(1) // 只取一次
  .subscribe(console.log)

RxJS基本介绍

  • RxJS 是一个通过使用可观察序列(Observable sequences),组合【异步行为】和【基于事件的程序】的库。
  • 函数式编程(Functional Programming) + 响应式编程(Reactive Programming),两个编程思想的结合。
  • 观察者模式(push) + 迭代器模式(pull),两种设计模式的结合(渐进式获取数据的思想)。

RxJS中的主要概念

RxJS提供了:

标题-202404092138.png

核心概念: Observable(可观察对象)

  • 在ES提案中,Observable是一个用来处理事件流的类,Observable的构造函数可以接受一个定义事件流的回调函数。
  • 在RxJS中,Observable 是多个值的惰性推送集合,它可以发出多个值,并且可以被观察者订阅来接收这些值。

Observable的创建(基于Observer)

RxJS有很多的创建Observable的方法(通常我们使用所谓的创建操作符, 像 of、from、interval、等等)。

我们以最基础的create为例:

const observable = Rx.Observable.create(function (observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
  try {
      observer.next(4);
  } catch(e) {
      observer.error(e);
  } finally {
      setTimeout(() => {
          observer.next(5);
          observer.complete();
      }, 3000);
  }
});

在创建Observable对象时,我们为create方法传入了一个函数,该函数的参数是一个Oberver对象,在该函数内部,我们可以用同步或异步代码,编写Oberver对象的回调逻辑。

Observable的订阅及清理(基于Subscription)

  • Observable对象创建时传入的函数,并不会立即执行,而是会等待一个Observer对象发起订阅时才执行。
  • Observer对象通过将自己传入Observable对象的subscribe方法发起订阅,返回一个Subscription对象。
  • 每次subscribe,Observable对象都会重新执行一次,类似于函数调用,每次执行有单独的上下文。
  • 通过Subscription对象,我们可以在有限的时间内中止Observable对象的执行。
const observer = {
    next: value => console.log(value), // next回调
    error: err => {}, // error回调
    complete: () => console.log('this is the end') // complete回调
};
const subscription = observable.subscribe(observer);
setTimeout(() => {
    subscription.unsubcribe(); // 终止Observable的执行
}, 1000);

操作符(Operators)

创建相关(of, from, fromEvent, fromPromise, never, empty, throw, interval, timer)

const _source = Rx.Observable.create(function(observer) {
    observer.next('A');
    observer.next('B');
    observer.next('C');
    observer.complete();
});
// of
const source_of = Rx.Observable.of('A', 'B', 'C');
// from
const _fromObj =
    ['A', 'B', 'C']
    || 'ABC'
    || new Set(['A', 'B', 'C']);
const source_from = Rx.Observable.from(_fromObj);
// fromPromise
const _resolve = (str) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(str);
        }, 100);
    });
};
const source_fromPromise_0 = Rx.Observable.from(_resolve('This is fromPromise'));
const source_fromPromise = Rx.Observable.fromPromise(_resolve('This is fromPromise'));
// fromEvent
const source_fromEvent = Rx.Observable.fromEvent(document.body, 'click');
// fromEventPattern
const _ev = new CustomEvent(); // CustomEvent类需要同时具有 注册监听/移除监听 两种方法
const source_fromEventPattern = Rx.Observable.fromEventPattern(
    (handler) => _ev.addListener(handler), 
    (handler) => _ev.removeListener(handler)
);
// empty, never, throw
const source_empty = Rx.Observable.empty(); // 创建一个空Observable,立即complete
const source_never = Rx.Observable.never(); // 创建一个一直存在的Observable,什么都不做
const source_throw = Rx.Observable.throw('err'); // 创建一个Observable,立即error
// interval, timer
// setInterval,next一个从0开始递增的整数
const source_interval = Rx.Observable.interval(1000);
// 等待1000ms,输出0,然后每隔5000ms输出一个递增的整数
const source_timer = Rx.Observable.timer(1000, 5000);
// 从Date对象的时间开始发送第一个值
const source_timer_date = Rx.Observable.timer(new Date(), 5000);
// 等待1000ms,输出1,同时complete
const source_timer_cut = Rx.Observable.timer(1000)

其它常用操作符(迭代器处理)

// map
const source = Rx.Observable.interval(1000);
const newest = source.map(x => x + 2);
//    source: -----0-----1-----2-----3--...
//        map(x => x + 1)
//    newest: -----1-----2-----3-----4--...
/*  =============================================================  */
// mapTo
const source = Rx.Observable.interval(1000);
const newest = source.mapTo(2);
//    source: -----0-----1-----2-----3--...
//        mapTo(2)
//    newest: -----2-----2-----2-----2--...
/*  =============================================================  */
// filter
const source = Rx.Observable.interval(1000);
const newest = source.filter(x => x % 2 === 0);
//    source: -----0-----1-----2-----3-----4-...
//        filter(x => x % 2 === 0)
//    newest: -----0-----------2-----------4-...
/*  =============================================================  */
// take
// first 和take(1)一样
const source = Rx.Observable.interval(1000);
const example = source.take(3);
//    source : -----0-----1-----2-----3--..
//        take(3)
//    example: -----0-----1-----2|
/*  =============================================================  */
// takeUntil
const source = Rx.Observable.interval(1000);
const click = Rx.Observable.fromEvent(document.body, 'click');
const example = source.takeUntil(click);  
//    source : -----0-----1-----2------3--
//    click  : ----------------------c----
//        takeUntil(click)
//    example: -----0-----1-----2----|
/*  =============================================================  */
// concatAll
var click = Rx.Observable.fromEvent(document.body, 'click');
var source = click.map(e => Rx.Observable.of(1,2,3));
var example = source.concatAll();
//    click  : ------c------------c--------
//        map(e => Rx.Observable.of(1,2,3))
//    source : ------o------------o--------
//                    \            \
//                     (123)|       (123)|
//        concatAll()
//    example: ------(123)--------(123)------------
/*  =============================================================  */
// skip
const source = Rx.Observable.interval(1000);
const example = source.skip(3);
//    source : ----0----1----2----3----4----5--....
//        skip(3)
//    example: -------------------3----4----5--...
/*  =============================================================  */
// takeLast 等到整個 Observable 完成(complete)时输出
// last 和takeLast(1)等同
const source = Rx.Observable.interval(1000).take(6);
const example = source.takeLast(2);
//    source : ----0----1----2----3----4----5|
//        takeLast(2)
//    example: ------------------------------(45)|
/*  =============================================================  */
// concat
const source = Rx.Observable.interval(1000).take(3);
const source2 = Rx.Observable.of(3)
const source3 = Rx.Observable.of(4,5,6)
const example = source.concat(source2, source3);
const example1 = Rx.Observable.concat(source, source2, source3);
//    source : ----0----1----2|
//    source2: (3)|
//    source3: (456)|
//        concat()
//    example: ----0----1----2(3456)|
/*  =============================================================  */
// startWith
const source = Rx.Observable.interval(1000);
const example = source.startWith(0);
//    source : ----0----1----2----3--...
//        startWith(0)
//    example: (0)----0----1----2----3--...
/*  =============================================================  */
// merge
const source = Rx.Observable.interval(500).take(3);
const source2 = Rx.Observable.interval(300).take(6);
const example = source.merge(source2);
//    source : ----0----1----2|
//    source2: --0--1--2--3--4--5|
//        merge()
//    example: --0-01--21-3--(24)--5|
/*  =============================================================  */
// combineLatest 在source和newest有新的next时,触发example的next,输出expamle函数计算所得
const source = Rx.Observable.interval(500).take(3);
const newest = Rx.Observable.interval(300).take(6);
const example = source.combineLatest(newest, (x, y) => x + y);
//    source : ----0----1----2|
//    newest : --0--1--2--3--4--5|
//        combineLatest(newest, (x, y) => x + y);
//    example: ----01--23-4--(56)--7|
/*  =============================================================  */
// zip 当source和newest都已送出第n个元素时,输出example函数计算所得
// zip 会缓存需要等待的数据,所以需要考虑内存问题
const source = Rx.Observable.interval(500).take(3);
const newest = Rx.Observable.interval(300).take(6);
const example = source.zip(newest, (x, y) => x + y);
//    source : ----0----1----2|
//    newest : --0--1--2--3--4--5|
//        zip(newest, (x, y) => x + y)
//    example: ----0----2----4|
/*  =============================================================  */
// withLatestFrom
const main = Rx.Observable.from('hello')
    .zip(Rx.Observable.interval(500), (x, y) => x);
const some = Rx.Observable.from([0,1,0,0,0,1])
    .zip(Rx.Observable.interval(300), (x, y) => x);
const example = main.withLatestFrom(some, (x, y) => {
    return y === 1 ? x.toUpperCase() : x;
});
//    main   : ----h----e----l----l----o|
//    some   : --0--1--0--0--0--1|
//        withLatestFrom(some, (x, y) =>  y === 1 ? x.toUpperCase() : x);
//    example: ----h----e----l----L----O|

简易的鼠标拖拽监听Demo

const dragDOM = document.getElementById('drag');
const body = document.body;
const mouseDown = Rx.Observable.fromEvent(dragDOM, 'mousedown');
const mouseUp = Rx.Observable.fromEvent(body, 'mouseup');
const mouseMove = Rx.Observable.fromEvent(body, 'mousemove');
mouseDown
  .map(event => mouseMove.takeUntil(mouseUp)) // mouseDown时转换成mouseMove监听,并在mouseUp时结束
  .concatAll() //拍平成一维
  .map(event => ({ x: event.clientX, y: event.clientY })) // 输出事件的鼠标x,y坐标
  .subscribe(pos => {
  	dragDOM.style.left = pos.x + 'px';
    dragDOM.style.top = pos.y + 'px';
  })

Operators的执行特点

标题-202404092139.png

附属类型:Subject

Subject类型详解

  • 对于一般的Observable,每次订阅时都是一次全新的执行,每个订阅间都是完全独立的。而对于某些事件,我们希望多个订阅之间可以共享事件或数据流,这时我们就需要Subject的参与。
  • Subject继承自Observable,实际作为订阅的中间人,Subject既是Observable,又是Observer。
  1. Subject订阅sourceObservable。
  2. Subject在内部维护一个Observer列表,每当sourceObservable有元素push时,遍历Observer列表并主动push元素。
  3. Subject对象的subscribe方法,实际上就是向Observer列表中加入新的Observer对象。
  4. Subject对象的connect方法,实际上才对应着Observable的subscribe,即决定执行时机。

Subject对象的分类

  • Subject

    • 基础的Subject对象,实现了多播,并不维护数据或事件的状态。
  • BehaviorSubject

    • 有一个“当前值”的概念。它保存了发送给消费者的最新值,当有新的观察者订阅时,会立即从BehaviorSubject那接收到“当前值”,在定义一个BehaviorSubject时需要有初始值。
  • ReplaySubject

    • 类似于BehaviorSubject,可以发送旧值给新的订阅者,但是不仅是‘当前值’,还可以是之前的旧值。在定义时可以传入需要缓存最后几个值,还可以指定 window time (以毫秒为单位)来确定多久之前的值可以记录,取交集。
  • AsyncSubject

    • 只有当Observable执行完成时(complete),它才会将执行的最后一个值发送给观察者,和last操作符类似。

关于Subject的操作符

// multicast
// 挂载subject,返回一个可connect的observable
// publish是multicast的简化写法
const source = Rx.Observable.interval(1000).take(3)
    .multicast(new Rx.Subject());
const observerA = {
    next: value => console.log('A next: ' + value),
    error: error => console.log('A error: ' + error),
    complete: () => console.log('A complete!')
}
const observerB = {
    next: value => console.log('B next: ' + value),
    error: error => console.log('B error: ' + error),
    complete: () => console.log('B complete!')
}
source.subscribe(observerA); // subject.subscribe(observerA)
source.connect(); // source.subscribe(subject)
setTimeout(() => {
    source.subscribe(observerB); // subject.subscribe(observerB)
}, 1000);
// refCount
// 必须搭配multicast,建立一个只要有订阅就会自动connect的observable
// publish + refCount 可以简写为share
const source = Rx.Observable.interval(1000)
             .do(x => console.log('send: ' + x))
             .multicast(new Rx.Subject())
             .refCount();
const subscriptionA = source.subscribe(observerA);
// 订阅数 0 => 1
let subscriptionB;
setTimeout(() => {
    subscriptionB = source.subscribe(observerB);
    // 订阅数 0 => 2
}, 1000);

附属类型:Scheduler

Scheduler是什么

  • 调度器控制着何时启动Subscription和何时发送通知。它由三部分组成:

    • 调度器是一种数据结构。 它知道如何根据优先级或其他标准来存储任务和将任务进行排序。
    • 调度器可以充当执行上下文。 它表示在何时何地执行任务(立即执行,回调函数机制(setTimeout,process.nextTick等),动画帧)。
    • 调度器有一个(虚拟的)时钟。 调度器功能通过它的getter方法now()提供了“时间”的概念。在具体调度器上安排的任务将严格遵循该时钟所表示的时间。

Scheduler类型

  • null

    • 特点:立即执行任务,无延迟。
    • 用途:用于同步执行任务,适合计算量小且不需要延迟的操作。
  • queueScheduler

    • 特点:在当前事件循环中顺序执行任务。
    • 用途:适用于需要顺序执行的任务,尤其是在递归或迭代操作中,以避免调用栈溢出。
    • 备注: trampoline scheduler是一种特殊的调度器,用于在当前事件循环中管理任务的执行。它的工作方式类似于弹簧,先进入队列的任务会被推迟执行,直到当前的执行堆栈被清空。这意味着它会在当前事件循环的末尾执行任务,而不是立即执行,这有助于避免递归调用导致的堆栈溢出问题。
  • asapScheduler

    • 特点:利用微任务队列,比setTimeout更快地异步执行任务。
    • 用途:适用于需要快速响应的操作,但又不想在当前调用栈中执行的任务,如处理UI事件或进行微小的后台计算。
  • asyncScheduler

    • 特点:使用setInterval来异步执行任务。
    • 用途:适合于需要延迟或定时执行的任务。可以用来替代setTimeout和setInterval。
    • 备注: 时间相关的操作符中,会默认使用该调度。
  • animationFrameScheduler

    • 特点:利用requestAnimationFrame来同步浏览器的重新渲染,适合动画和UI渲染。
    • 用途:在进行动画或要求高性能的UI更新时使用,可以保证任务在每个渲染帧开始时执行。

Scheduler的使用

  • subscribeOn

    • 用途: subscribeOn用于指定Observable的订阅(subscribe)应该在哪个Scheduler上发生。
    • 作用方式: 使用subscribeOn(scheduler),你可以延迟订阅或在指定的调度器上安排订阅动作。这里的scheduler是你提供的参数,指示订阅操作应该在哪个调度器上执行。这意味着,无论Observable何时被创建,通过使用subscribeOn,订阅动作的执行可以被延迟或安排在特定的时间点执行,而不是立即发生。
  • observeOn

    • 用途:observeOn用于指定Observable发出的通知应该在哪个调度器上传递给观察者。
    • 作用方式:使用observeOn(scheduler),在sourceObservable和Observer之间引入了一个中间人(mediator Observer)。这个中间人使用给定的Scheduler来安排对Observer的调用。这样,即使sourceObservable同步地生成和发出值,使用observeOn也可以确保Observer的回调函数在指定的调度器上异步地发生。