rxjs究极化入门

为什么需要rxjs

  • 前提

    • 在网页的世界存取任何资源都是异步(Async)的,比如说我们希望拿到一个档案,要先发送一个请求,然后必须等到档案回来,再执行对这个档案的操作。这就是一个异步的行为,而随着网页需求的复杂化,我们所写的 JavaScript 就有各种针对异步的写法,例如使用 callback 或是 Promise 对象甚至是新的语法糖 async/await ------ 但随着应用需求愈来愈复杂,撰写非同步的代码仍然非常困难
  • 异步常见问题

    • 竞态条件 (Race Condition)

      • 当多次发送请求时, 请求先后顺序就会影响到最终接收到的结果不同
    • 内存泄漏 (Memory Leak)

      • 例如页面初始化时有对 DOM 注册监听事件,而没有在适当的时机点把监听的事件移除,就有可能造成 Memory Leak。比如说在 A 页面监听 body 的 scroll 事件,但页面切换时,没有把 scroll 的监听事件移除
    • 复杂的状态 (Complex State)

      • 当有异步时,应用程式的状态就会变得非常复杂!比如说一个列表需要有权限才能看见具体的数据,首先需要获取这个列表数据,然后在根据用户的权限去展示数据,用户也可能翻页进行快速操作,这些都是异步执行,这时就会各种复杂的状态需要处理
    • 异常处理 (Exception Handling)

      • JavaScript 的 try/catch 可以捕捉同步的异常处理,但异步的就没这么容易,尤其当我们的异步行为很复杂时,这个问题就愈加明显。
  • Promise: 可以减轻一些异步问题,如将回调函数变为串行的链式调用,统一同步和异步代码等,async/await 中也可以使用 try/catch 来捕获错误。但是对于复杂的场景,仍然难于处理。而且 Promise 还有其他的问题,一是只有一个结果,二是不可以取消。

  • 无法统一的写法

    • 我们除了要面对异步会遇到的各种问题外,还要烦恼很多不同的api写法,
      • DOM Events
      • XMLHttpRequest
      • fetch
      • WebSockets
      • Server Send Events
      • Service Worker
      • Node Stream
      • Timer
    • 上面列的api都是异步的,但他们都有各自的api及写法。如果我们使用rxjs,上面所有的api就可以透过rxjs来处理,能用同样的api来操作
    • 示例:监听点击事件,点击一次后不再监听
    • js
    ts 复制代码
    var handler = (e) => {
      console.log(e)
      document.body.removeEventListener('click', handler) // 结束监听
    }
    
    // 注册监听
    document.body.addEventListener('click', handler)
  • rxjs

ts 复制代码
Rx.Observable.fromEvent(document.body, 'click') // 注册监听
  .take(1) // 只取一次
  .subscribe(console.log)

基本介绍

RxJS 是一套借由 Observable sequences 来组合异步行为和事件基础程序的 Library, 这也被称为 Functional Reactive Programming,更切确地说是指 函数式编程(Functional Programming) 及 响应式编程(Reactive Programming )两个编程思想的结合。RxJS 主要有一个核心和三个重点

一个核心

Observable 再加上相关的 Operators(map, filter...),这个部份是最重要的,其他三个重点本质上也是围绕着这个核心在转

什么是Observable

Observable 是一个表示异步数据流的类,它可以发出多个值,并且可以被观察者订阅来接收这些值。Observable 可以用于处理各种异步操作,如 HTTP 请求、定时器、事件等简单来说在 RxJS 中 Observable 是可被观察者,观察者则是 Observer,它们通过 Observable 的 subscribe 方法进行关联。

要创建一个 Observable,只要给 new Observable 传递一个接收 observer 参数的回调函数,在这个函数中去定义如何发送数据。observer接受三个方法next、complete、error(observer环节会详细讲)

ts 复制代码
const source = new Observable((observer) => {
  observer.next(1)
  observer.next(2)
  observer.next(3)
  observer.complete()
})

const observer = {
  next: (item) => console.log(item),
  error: (e) => console.log(e, '错误'),
  complete: () => console.log('complete'),
}

source.subscribe(observer)
//输出1 2 3完成

上面的代码通过 new Observable 创建了一个 Observable,调用它的 subscribe 方法进行订阅,执行结果为依次输出 1 2 3 完成,

Observable和promise的对比

  • 输出不同:Promise只能resolve一次,输出单值 。Observable能多次next,输出多值
ts 复制代码
//promise
const p1 = new Promise((resolve, reject) => {
  resolve(1)
})
p1.then((x) => console.log(x))
//输出1

//Observable
const source = new Observable((observer) => {
  observer.next(1)
  observer.next(2)
  observer.next(3)
})

const observer = {
  next: (x) => console.log(x),
}

source.subscribe(observer)
//输出1 2 3
  • 执行时机不同:Promise 的代码会创建后立即执行 。而对于 Observable,它在订阅时才开始执行
ts 复制代码
//promise
console.log('start')
const p1 = new Promise((resolve, reject) => {
  console.log('执行')
  resolve('微任务')
})
p1.then((x) => console.log(x))
console.log('end')
//输出start 执行 end 微任务

//Observable
const source = new Observable((observer) => {
  observer.next(1)
  observer.next(2)
  observer.next(3)
})

const observer = {
  next: (x) => console.log(x),
}
console.log('start')

source.subscribe(observer)

console.log('end')
//输出start 1 2 3 end
  • 内部状态不同:promise只有resolve、reject两种状态,分别对应 Observable 的 complete 和 error。而observable加上next有三种
ts 复制代码
//Observable
const source = new Observable((observer) => {
  try {
    observer.next(1)
    observer.next(2)
    throw new Error('错误')
    observer.complete()
  } catch (e) {
    observer.error(e)
  }
})

const observer = {
  next: (x) => console.log(x),
  error: (e) => console.log(e),
  complete: () => console.log('complete'),
}
source.subscribe(observer)
//promise
const p1 = new Promise((resolve, reject) => {
  resolve('完成')
  // reject('失败')
})
p1.then((x) => console.log('完成')).catch((err) => console.log(err, '错误'))
  • 结束:Promise一旦开始无法结束,而Observable可以通过unsubscribe进行退订
ts 复制代码
const source = new Observable((observer) => {
  let number = 1
  setInterval(() => {
    observer.next(number++)
  }, 1000)
})

const observer = {
  next: (item) => console.log(item),
}

const subscription = source.subscribe(observer)

setTimeout(() => {
  subscription.unsubscribe()
}, 3000)
//因为三秒后退订,所以只输出了1 2 3

总之,RxJS 和 Promise 都是用于处理异步操作的工具,但是 RxJS 更加灵活和强大,更适合处理复杂的异步操作和数据流处理场景。而 Promise 则更加简单易用,适合处理简单的异步操作。

三个重点

  • Observer(观察者)

    • Observer 是一个用于处理 Observable 发出的值的接口。它是由三个回调函数组成:next、error 和 complete。

      ts 复制代码
      const observer = {
        next: (value) => console.log(value), // 处理 Observable 发出的值
        error: (error) => console.error(error), // 处理 Observable 发生的错误
        complete: () => console.log('Observable 完成'), // 处理 Observable 完成事件
      }
      • next 函数用于处理 Observable 发出的每个值。每当 Observable 发出一个新的值时,next 函数就会被调用,并将该值作为参数传入。在上述示例中,我们简单地将值打印到控制台。
      • error 函数用于处理 Observable 发生的错误。当 Observable 发生错误时,error 函数会被调用,并将错误信息作为参数传入。在上述示例中,我们简单地将错误信息打印到控制台。
      • complete 函数用于处理 Observable 完成事件。当 Observable 完成时,complete 函数会被调用,表示 Observable 不再发出任何值。在上述示例中,我们简单地打印一条完成消息到控制台。
    • 可以通过使用 subscribe 方法将 Observer 和 Observable 关联起来,从而对 Observable 进行订阅,开始接收它发出的值

      ts 复制代码
      import { from } from 'rxjs'
      const observable = from([1, 2, 3])
      observable.subscribe(observer)
      • 在上面的示例中,我们使用 from 方法创建了一个 Observable,该 Observable 发出数组 [1, 2, 3] 中的值。然后,我们调用 subscribe 方法,将 observer 对象传递给它,以订阅这个 Observable。
      • 当 Observable 发出值时,它会调用 observer 的 next 函数,并将值作为参数传递给它。如果 Observable 发生错误,它会调用 observer 的 error 函数,并将错误信息传递给它。当 Observable 完成时,它会调用 observer 的 complete 函数。
      • 总结来说,Observer 是一个用于处理 Observable 发出的值的接口,它由 next、error 和 complete 三个回调函数组成。我们可以通过调用 subscribe 方法并传入 Observer 对象,将 Observer 和 Observable 关联起来,从而对 Observable 进行订阅,处理它发出的值。
  • Subject(订阅者): Subject 是一种特殊的 Observable,它可以被用作观察者(Observer)和可观察对象(Observable)之间的桥梁。Subject 同时具备从 Observable 接收值和向多个观察者广播值的能力

    • Subject、BehaviorSubject 和 ReplaySubject
  • Scheduler(调度器):用于控制数据流中数据的推送节奏。

操作符

  • Operators(操作符):Operators 是 RxJS 中用于对 Observable 进行转换、过滤、操作等的函数式编程工具,主要分为两大类
    • Creation Operators:用于创建 Observable,例如 of()、from()、interval() 等。
    • Pipeable Operators:用于对 Observable 的数据流进行转换、过滤、操作等,例如 map()、filter()、tap()、mergeMap() 等。

创建操作符

  • of:多个序列

    ts 复制代码
    of('ces1', 'ces2')
      .pipe(map((name) => `Hello, ${name}!`))
      .subscribe(console.log)
  • from:数组、promise、字符串

    • 数组
    ts 复制代码
    from(['a', 'b', 'c']).subscribe(console.log)
    // 输出
    //a
    //b
    //c
    • 字符串
    ts 复制代码
    from('ces').subscribe(console.log)
    //输出
    //c
    //e
    //s
    • promise:如果我们传入promise事件实例,当正常回传时,就会送到next,并立即发送完通知,如果有错误则会送到error
    ts 复制代码
    from(
      new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve('hello')
        }, 100)
      }),
    ).subscribe(console.log)
    //输出hello
  • fromEvent:接受两个参数(下面代码示例,点击body打印event)

    • 第一个参数传入dom事件
    • 第二个参数传入监听的事件名称
    ts 复制代码
    fromEvent(document.body, 'click').subscribe(() => {
      console.log('hello')
    })
    //点击输出hello
  • interval:接收一个数值作为参数,参数代表的是间隔时间

    ts 复制代码
    interval(1000).subscribe(console.log)
    //输出0 1 2 3...每隔一秒输出一个值
  • timer:接收两个参数

    • 当只有一个参数时,表示发出第一个值要等待的时间,然后完成。可以是数值,也可以是日期(Date)
    ts 复制代码
    timer(1000).subscribe(console.log)
    //隔一秒输出0
    • 当有两个参数时,第一个参数代表要发出第一个值等待的时间,第二个参数代表第一次之后发送值的间隔时间
    ts 复制代码
    timer(1000).subscribe(console.log)
    //等一秒后输出第一个值0,然后每隔两秒输出值1、2、3...
  • empty:创建一个立即完成的observable。目前官方已经弃用,使用常量EMPTY(不是方法,是observable对象)

合并类

静态操作符

  • combinLatest:使用它可以取多个输入流中的最新值,并将这些值转换成一个单个值传递给结果流。RxJS 会缓存每个输入流中的最新值,只有当所有输入流都至少发出一个值后才会使用投射函数(从之前缓存中取出最新值)来计算出结果值,然后通过结果流将计算的结果值发出

    • 当所有输入流完成时,结果流就会完成
    • 如何任意输入流报错,那么结果流就会报错
    • 如果某个输入流没有完成的话,那么结果流便不会完成
    ts 复制代码
    const a = of('a', 1, 'ces1')
    const b = of('b', 2, 'ces2')
    combineLatest(a, b).subscribe(console.log)
    //输出
    //["ces1", "b"]
    //["ces1", 2]
    //["ces1", "ces2"]
  • forkJoin:有时候,有一组流,但你只关心每个流中的最后一个值。

    • 举个例子,你想要发起多个网络请求,并只想当所有请求都返回结果后再执行操作。
    • 此操作符的功能与 Promise.all 类似。但是,如果流中的值多于一个的话,除了最后一个值,其他都将被忽略掉。
    • 只有当所有输入流都完成时,结果流才会发出唯一的一个值。
    • 如果任意输入流不完成的话,那么结果流便永远不会完成,如何任意输入流报错的话,结果流也将报错。!
    ts 复制代码
    const a = of('a', 1, 'ces1')
    const b = of('b', 2, 'ces2')
    forkJoin(a, b).subscribe(console.log)
    //输出["ces1", "ces2"]
  • race:产生的observable会copy最先传出数据的observable(类似promise.race)

    ts 复制代码
    const obs1 = interval(1000).pipe(map(() => 'fast one'))
    const obs2 = interval(3000).pipe(map(() => 'second one'))
    const obs3 = interval(5000).pipe(map(() => 'last one'))
    
    race(obs3, obs1, obs2).subscribe((res) => console.log(res))
    //输出:fast one--fast one--fast one...
  • zip:zip会取每个observable相同顺位的元素并传入callback,也就是每个observable的第n个元素会一起传入callback组成数组

    ts 复制代码
    const source$ = interval(500).pipe(take(3))
    const newest$ = interval(300).pipe(take(6))
    
    zip(source$, newest$)
      .pipe(map((list) => list[0] + list[1]))
      .subscribe(console.log)
    //输出0 2 4
    • zip 会等到 source 跟 newest 都送出了第一个元素,再传入 callback,下次则等到 source 跟 newest 都送出了第二个元素再一起传入 callback,所以运行的步骤如下
    • newest 送出了第一个值 0,但此时 source 并没有送出第一个值,所以不会执行 callback。
    • source 送出了第一个值 0,newest 之前送出的第一个值为 0,把这两个数传入 callback 得到 0。
    • newest 送出了第二个值 1,但此时 source 并没有送出第二个值,所以不会执行 callback。
    • newest 送出了第三个值 2,但此时 source 并没有送出第三个值,所以不会执行 callback。
    • source 送出了第二个值 1,newest 之前送出的第二个值为 1,把这两个数传入 callback 得到 2。
    • newest 送出了第四个值 3,但此时 source 并没有送出第四个值,所以不会执行 callback。
    • source 送出了第三个值 2,newest 之前送出的第三个值为 2,把这两个数传入 callback 得到 4。
    • source 结束 example 就直接结束,因为 source 跟 newest 不会再有对应顺位的值!

非静态操作符

下面四个运算符都是来进行高阶处理的(Higher Order Observable),所谓的 Higher Order Observable 就是指一个 Observable 送出的元素还是一个 Observable,就像是二维数组一样,一个数组中的每个元素都是数组

  • concatAll:会处理完前一个 observable 才会在处理下一个 observable

    ts 复制代码
    fromEvent(document.body, 'click')
      .pipe(
        map((x) => interval(1000).pipe(take(3))),
        concatAll(),
      )
      .subscribe(console.log)
    //当点击时输出0 1 2,连续点击,会在上一个输出完后输出下一个
    sql 复制代码
    click  : ---------c-c------------------c--..
          map(e => interval(1000))
    source : ---------o-o------------------o--..
                       \ \
                        \ ----0----1----2...
                         ----0----1----2...
                         concatAll()
    example: ----------------0----1----2----0----1----2..
  • exhaustAll:当前的observable没处理完,接下来的observable会忽略,当前的处理完成,此时接收到新的才会处理

    ts 复制代码
    fromEvent(document.body, 'click')
      .pipe(
        map((x) => interval(1000).pipe(take(3))),
        exhaustAll(),
      )
      .subscribe(console.log)
    //点击后输出0 1 2
    //当3个数字没输出完再次点击不会有任何输出,当3个数字完输出再次点击会再次输出0 1 2
    sql 复制代码
    click  : ---------c-c------------------c--..
         map(e => interval(1000))
    source : ---------o-o-------------------------o--..
                      \ \                         \
                       \ ----0----1----2...(忽略) ----0----1----2...
                        ----0----1----2...
                        exhaustAll()
    example: ----------------0----1----2------------0----1----2..
  • switchAll:接收到新的observable后直接处理新的,不管前一个observable是否完成,每当接受到新的observable就会把旧的observable退订,永远只处理最新的observable

    ts 复制代码
    fromEvent(document.body, 'click')
      .pipe(
        map((e) => interval(1000).pipe(take(3))),
        switchAll(),
      )
      .subscribe(console.log)
    //当第一次点击刚输出一个值再次点击后,总输出:0 0 1 2
    sql 复制代码
    click  : ---------c-c------------------c--..
          map(e => interval(1000))
    source : ---------o--------o--..
                       \        \----0----1----2...
                        \
                         ----0----1----2...
                        switchAll()
    example: -----------------0----1------0----1----2...
  • mergeAll:merge 可以让多个 observable 同时送出元素,mergeAll 也是同样的道理,它会把二维的 observable 转成一维的,并且能够同时处理所有的 observable

    • 可以传入一个数值,这个数值代表他可以同时处理的 observable 数量
    • 所有的 observable 是并行(Parallel)处理的,也就是说 mergeAll 不会像 switch 一样退订(unsubscribe)原先的 observable 而是并行处理多个 observable。以我们的示例来说,当我们点击越多下,最后送出的频率就会越快
    ts 复制代码
    fromEvent(document.body, 'click')
      .pipe(
        map((e) => interval(1000).pipe(take(3))),
        mergeAll(2),
      )
      .subscribe(console.log)
    sql 复制代码
    click  : ---------c-c----------o----------..
          map(e => interval(1000))
    source : ---------o-o----------c----------..
                     \ \          \----0----1----2|
                      \ ----0----1----2|
                       ----0----1----2|
                       mergeAll(2)
    example: ----------------00---11---22---0----1----2--..
  • withLatestFrom: 使用这个操作符的observable发出值后,才会进行合并并产生数据

    ts 复制代码
    const source$ = interval(500).pipe(take(3))
    const newest$ = interval(300).pipe(take(5))
    
    source$.pipe(withLatestFrom(newest$)).subscribe(console.log)
    //输出[0, 0] [1, 2] [2, 4]
    • source发出0,newest发出0,结合为 [0, 0]
    • source发出1,newest此时发出2,结合为[1,2]发出
    • source发出2,newest此时发出4,结合[2,4]发出
    • source结束,整个observable完成
  • startWith :可以在 observable 的一开始塞要发送的元素

    ts 复制代码
    interval(1000).pipe(take(3), startWith(0)).subscribe(console.log)
    //输出 0 0 1 2

转换类

  • concatMap :其实就是map和concatAll的组合简化写法,concatMap 也会先处理前一个送出的 observable 在处理下一个 observable

    • concatMap 还有第二个参数是一个 callback,这个 callback 会传入四个参数

      • 外部 observable 送出的元素
      • 内部 observable 送出的元素
      • 外部 observable 送出元素的 index
      • 内部 observable 送出元素的 index
      ts 复制代码
      fromEvent(document.body, 'click')
        .pipe(concatMap((e) => interval(1000).pipe(take(3))))
        .subscribe(console.log)
      css 复制代码
      source : -----------c--c------------------...
          concatMap(c => interval(1000).pipe(take(3)))
      example: -------------0-1-2-0-1-2---------...
  • exhaustMap:其实就是map和exhaustAll的组合简化写法,exhaustMap在当前observable没处理完前直接忽略接下来的observable

    • exhaustMap 跟 concatMap 一样有第二个参数 selector callback 可用来回传我们要的值,这部分的行为跟 concatMap 是一样的,这里就不再赘述
    ts 复制代码
    fromEvent(document, 'click')
      .pipe(exhaustMap((x) => interval(1000).pipe(take(3))))
      .subscribe(console.log)
    css 复制代码
    source : -----------c-c------c------------...
         concatMap(c => interval(1000).pipe(take(3)))
    example: -------------0-1-2---0-1-2-----...
  • switchMap:其实就是map和switchAll的组合简化写法,switchMap 会在新的 observable 接受后直接退订上一个未处理完的 observable

    • switchMap 跟 concatMap 一样有第二个参数 selector callback 可用来回传我们要的值,这部分的行为跟 concatMap 是一样的,这里就不再赘述
    ts 复制代码
    fromEvent(document.body, 'click')
      .pipe(switchMap((e) => interval(1000).pipe(take(3))))
      .subscribe(console.log)
    css 复制代码
    source : -----------c--c-----------------...
          concatMap(c => interval(1000).pipe(take(3)))
    example: -------------0--0-1-2-----------...
  • mergeMap:其实就是 map 加上 mergeAll 简化的写法

    • mergeMap 也能传入第二个参数 selector callback,这个 selector callback 跟 concatMap 第二个参数也是完全一样的
    • mergeMap 的重点是我们可以传入第三个参数,来限制并行处理的数量
    ts 复制代码
    fromEvent(document.body, 'click')
      .pipe(mergeMap((e) => interval(1000).pipe(take(3))))
      .subscribe(console.log)
    css 复制代码
    source : -----------c-c------------------...
          concatMap(c => interval(1000).pipe(take(3)))
    example: -------------0-(10)-(21)-2----------...

适用场景

  • concatMap 用在可以确定内部的 observable 结束时间比外部 observable 发送时间来快的情境,并且不希望有任何并行处理行为,适合少数要一次一次完成到底的的 UI 动画或特别的 HTTP request 行为。
  • switchMap 用在只要最后一次行为的结果,适合绝大多数的使用情境。
  • mergeMap 用在并行处理多个 observable,适合需要并行处理的行为,像是多个 I/O 的并行处理
  • 当前操作还在进行时,要忽略新的Observable,exhaustMap就是干这个的

其他转换类

  • scan:scan其实就是observable版的reduce

    ts 复制代码
    const source$ = from('hello')
    const example$ = interval(1000)
    zip(source$, example$)
      .pipe(
        map((list) => list[0]),
        scan((origin, next) => origin + next, ''),
      )
      .subscribe(console.log)
    //输出:h he hel hell hello
    • 例子:按钮点击

      ts 复制代码
      const add = document.getElementById('add') //获取add按钮
      const minus = document.getElementById('minus') //获取minus按钮
      const count = document.getElementById('count') //获取count
      
      const add$ = fromEvent(add, 'click').pipe(map((e) => 1))
      const minus$ = fromEvent(minus, 'click').pipe(map((e) => -1))
      
      const res = merge(EMPTY.pipe(startWith(0)), add$, minus$)
        .pipe(scan((origin, next) => origin + next))
        .subscribe((text: any) => (count.textContent = text))
      • 这里我们用了两个 button,一个是 add 按钮,一个是 minus 按钮。
      • 把这两个按钮的点击事件各建立了 addClick, minusClick 两个 observable,这两个 observable 直接 (map((e) => 1)) 跟 (map((e) => -1)),代表被点击后会各自送出的数字
      • 接着用 EMPTY 代表画面上数字的状态,搭配 startWith(0) 来设定初始值,接着用 merge 把两个 observable 合并透过 scan 处理之后的逻辑,最后在 subscribe 来更改画面的显示
  • buffer:buffer 接收一个 Observable 作为 参数,当 参数observable 发出数据时,将 缓存的数据传给下游

    ts 复制代码
    interval(1000)
      .pipe(buffer(interval(2000)))
      .subscribe(console.log)
    //输出[0,1] [2,3] [4,5] [6,7]...
    //把interval(1000)输出的元素缓存在数组中,等interval(2000)输出元素后,触发将缓存的元素输出
  • bufferTime:用时间来控制,多长时间后送出一组数据

    ts 复制代码
    interval(900).pipe(bufferTime(2000)).subscribe(console.log)
    //输出[0,1] [2,3] [4,5] [6,7]...
  • bufferCount:用数量控制时机,多少个为一组

    ts 复制代码
    interval(1000).pipe(bufferCount(2)).subscribe(console.log)
    //输出[0,1] [2,3] [4,5] [6,7]...
  • 使用buffer来做某个事件的过滤,只有在500毫秒连点两下,才能触发打印

    ts 复制代码
    const add = document.getElementById('add')
    const click$ = fromEvent(add, 'click')
    click$
      .pipe(
        bufferTime(500),
        filter((arr) => arr.length >= 2),
      )
      .subscribe((s) => {
        console.log('触发')
      })
    //500ms内连续点击两次触发click事件

筛选类

  • debounceTime:传入毫秒。会在收到元素后等待一段时间,很适合用来处理间歇行为,间歇行为就是指这个行为是一段一段的,例如要做AutoComplete时,我们搜索会不断地打字,可以等我们停了一小段时间后再发起请求, 不用打一个字就请求一次

    • eg1:我们每 300 毫秒就会送出一个数值,但我们的 debounceTime 是 1000 毫秒,也就是说每次 debounce 收到元素还等不到 1000 毫秒,就会收到下一个新元素,然后重新等待 1000 毫秒,如此重复直到第五个元素送出时,observable 结束(complete)了,debounceTime 就直接输出元素
    ts 复制代码
    interval(300).pipe(take(5), debounceTime(1000)).subscribe(console.log)
    //输出4
    • eg2: AutoComplete优化
    ts 复制代码
    $input
      .pipe(
        map((e: any) => e.target.value),
        debounceTime(300),
      )
      .subscribe(console.log)
    //没debounce前打一个字输出一次:c ce ces,debounce后打字间隔300ms内不输出:ces
  • throttleTime:和debounce不同的是,throttle会先进性触发,等时间过了后再次触发

    ts 复制代码
    interval(300).pipe(take(5), throttleTime(1000)).subscribe(console.log)
    //输出:0 4
  • distinct:去除重复值

    • 传值(回调)
    ts 复制代码
    from([{ value: 'a' }, { value: 'b' }, { value: 'a' }])
      .pipe(distinct((x) => x.value))
      .subscribe(console.log)
    //输出:
    //{value: "a"}
    //{value: "b"}
    • 不传,直接处理
    ts 复制代码
    from(['a', 'b', 'a']).pipe(distinct()).subscribe(console.log)
    //输出:
    //a
    //b
  • distinctUntilChanged:和distinct一样会把相同元素过滤掉,但distinctUntilChanged只会和上一次的比较,不会每次都比

    ts 复制代码
    from(['a', 'a', 'b', 'c', 'b']).pipe(distinctUntilChanged()).subscribe(console.log)
    //输出:
    //a
    //b
    //c
    //b
  • take:传入几取第几个元素

    ts 复制代码
    interval(1000).pipe(take(3)).subscribe(console.log)
    //输出:0 1 2
  • first:取observable发送的第一个元素之后就直接结束,行为和take(1)一致

    ts 复制代码
    interval(1000).pipe(first()).subscribe(console.log)
    //输出:0
  • takeUntil:可以在某件事情发生时,让一个 observable 完成

    ts 复制代码
    interval(1000)
      .pipe(takeUntil(fromEvent(document.body, 'click')))
      .subscribe(console.log)
    //点击body后interval完成,不再输出值
  • skip:和take相反,略过前n个发送元素

    ts 复制代码
    interval(1000).pipe(skip(2)).subscribe(console.log)
    //输出2 3 4 5...
  • takeLast:倒过来取最后几个

    ts 复制代码
    interval(1000).pipe(take(6), takeLast(2)).subscribe(console.log)
    //输出:4 5
  • last:取最后一个,和takeLast(1)相同

    ts 复制代码
    interval(1000).pipe(take(6), last()).subscribe(console.log)
    //输出:5

错误处理

  • catchError 用来捕获上游传递过来的错误。

    • 接受回调作为参数:当捕获到上游错误时调用这个回调,返回的observable发出的数据会传递给下游

      • err:捕获到的错误信息
      ts 复制代码
      from(['a', 'b', 2])
        .pipe(
          map((x: any) => x.toUpperCase()),
          catchError((err) => of(err)),
        )
        .subscribe(console.log)
      //输出
      //A
      //B
      //TypeError{}
      //返回EMPTY输出A B
      • observable:表示上游的observable对象,回调返回这个参数时,就会进行重试
      ts 复制代码
      from(['a', 'b', 2])
        .pipe(
          map((x: any) => x.toUpperCase()),
          catchError((err, obs) => obs),
        )
        .subscribe(console.log)
      //输出 A B A B A B A B A B....
  • retry:当一个observable出现错误进行重新尝试

    • 什么都不传会一直进行重试
    ts 复制代码
    from(['a', 'b', 1])
      .pipe(
        map((i: any) => i.toUpperCase()),
        retry(),
      )
      .subscribe(console.log)
    //输出 A B A B A B A B A B....
    • 传数字是重试的次数
    ts 复制代码
    from(['a', 'b', 1])
      .pipe(
        map((i: any) => i.toUpperCase()),
        retry(1),
      )
      .subscribe(console.log)
    //输出 A B A B Error: i.toUpperCase is not a function
  • retryWhen:retryWhen传入一个 callback,这个 callback 有一个参数会传入一个 observable,这个 observable 不是原本的 observable(example) 而是例外事件送出的错误所组成的一observable,我们可以对这个由错误所组成的 observable 做操作,等到这次的处理完成后就会重新订阅我们原本observable

    ts 复制代码
    interval(1000)
      .pipe(
        take(6),
        map((x) => {
          if (x == 4) throw new Error('not four')
          return x
        }),
        retryWhen((err) => err.pipe(delay(1000), take(2))),
      )
      .subscribe(console.log)
    //延迟一秒 重试2次,输出:0 1 2 3 0 1 2 3
  • repeat:和retry一样重试的效果,但是不需要有错误才进行重试

    • 设置参数:重试次数
    ts 复制代码
    from(['a', 'b', 'c']).pipe(take(3), repeat(2)).subscribe(console.log)
    //输出:a b c a b c
    • 不设置参数:一直重试
  • 示例:模仿在即时同步断线时,利用 catch 返回一个新的 observable,这个 observable 会先送出错误讯息并且把原本的 observable 延迟 5 秒再做合并,虽然这只是一个模仿,但它清楚的展示了 RxJS 在做错误处理时的灵活性

    ts 复制代码
    from(['a', 'b', 1])
      .pipe(
        take(3),
        map((i: any) => i.toUpperCase()),
        catchError((err, obs) => {
          return concat(EMPTY.pipe(startWith('出现错误,请5s后重连')), obs.pipe(delay(5000)))
        }),
      )
      .subscribe(console.log)
    //输出:A B 出现错误,请5s后重连 A B 出现错误,请5s后重连...

工具操作符

  • delay:delay可以延迟observable一开始发送元素的时间点

    ts 复制代码
    interval(300).pipe(take(5), delay(1000)).subscribe(console.log)
    //首个元素1s输出,其余的300ms输出 ----------0---1---2---3---4
  • delayWhen:和delay相似,最大的差别是delayWhen可以影响每个元素,而且需要传一个callback返回个observable

组合示例

自动完成 (Auto Complete)示例

  • 准备 input#search 以及 ul#suggest-list 的 HTML 与 CSS
  • 在 input#search 输入文字时,等待 100 毫秒再无输入,就发送 HTTP Request
  • 当 Response 还没回来时,使用者又输入了下一个文字就舍弃前一次的并再发送一次新的 Request
  • 接受到 Response 之后显示建议选项
  • 选择item后替代 input#search 的文字
ts 复制代码
const url = 'https://api.github.com/search/repositories?sort=stars&order=desc'

const getSuggestList = (keyword) =>
  fetch(url + '&q=' + keyword, { method: 'GET', mode: 'cors' }).then((res) => res.json())

const input = document.querySelector('#search')
const suggestList = document.querySelector('#suggest-list')

const input$ = fromEvent(input, 'input')
const selectItem$ = fromEvent(suggestList, 'click')
//替换更新内容
const render = (suggestArr = []) => {
  return (suggestList.innerHTML = suggestArr
    .map((item) => '<li>' + item.full_name + '</li>')
    .join(''))
}

//搜索
input$
  .pipe(
    debounceTime(100),
    distinctUntilChanged(),
    switchMap((e: any) => from(getSuggestList(e.target.value)).pipe(retry(3))),
    map((res) => res.items),
  )
  .subscribe((list) => render(list))
//下拉项选中
selectItem$
  .pipe(
    filter((e: any) => e.target.matches('li')),
    map((e) => e.target.innerText),
  )
  .subscribe((text) => {
    input.value = text
    render()
  })

简易拖拽示例

  • 首先页面上有一个标签(#drag)
  • 当鼠标在标签(#drag)上按下左键(mouseDown)时,开始监听鼠标移动(mouseMove)的位置
  • 当鼠标左键触发(mouseUp)时,结束监听鼠标移动
  • 当鼠标移动(mouseMove)被监听时,跟着修改标签的样式属性
ts 复制代码
const dom = document.getElementById('drag')
const body = document.body

const mouseDown = fromEvent(dom, 'mousedown')
const mouseUp = fromEvent(body, 'mouseup')
const mouseMove = fromEvent(body, 'mousemove')

//鼠标按下->转成鼠标移动 触发鼠标抬起->结束鼠标移动
mouseDown
  .pipe(
    concatMap((event: any) =>
      mouseMove.pipe(
        //每次鼠标按下,转成鼠标移动数据流
        map((moveEvent: any) => ({
          left: moveEvent.clientX - event.offsetX,
          top: moveEvent.clientY - event.offsetY,
        })),
        takeUntil(mouseUp), //鼠标松开后,结束对鼠标移动的监听
      ),
    ),
  )
  .subscribe((res) => {
    dom.style.left = res.left + 'px'
    dom.style.top = res.top + 'px'
  })

视频拖拽:当页面滑动到视频出页面时视频 fixed 定位,这是可以拖拽移动视频位置

  • 定义变量
ts 复制代码
//获取页面元素
const eleVideo = document.querySelector('#video')
const eleAnchor = document.querySelector('#anchor')
const eleBody = document.body

const windowH = window.document.documentElement.clientHeight
const windowW = window.document.documentElement.clientWidth

//获取页面视频的左上右下妃别想对于浏览器视口的位置
const getVideoH = () => eleVideo.getBoundingClientRect().height
const getVideoW = () => eleVideo.getBoundingClientRect().width
const getValidValue = (value, min, max) => Math.min(Math.max(value, min), max)

//定义滚动一节小窗口拖拽的事件observable
const scroll$ = fromEvent(document, 'scroll')
const mouseDown$ = fromEvent(eleVideo, 'mousedown')
const mouseMove$ = fromEvent(eleBody, 'mousemove')
const mouseUp$ = fromEvent(eleBody, 'mouseup')
  • 小窗口拖动
ts 复制代码
mouseDown$
  .pipe(
    filter((mouseDownEvent) => eleVideo.classList.contains('video-fixed')),
    concatMap((mouseDownEvent) => mouseMove$.pipe(takeUntil(mouseUp$))),
    withLatestFrom(mouseDown$, (move, down) => {
      const maxBottom = windowH - getVideoH()
      const maxRight = windowW - getVideoW()
      const right = windowW - getVideoW() - (move.clientX - down.offsetX)
      const bottom = windowH - getVideoH() - (move.clientY - down.offsetY)
      return {
        right: getValidValue(right, 0, maxRight),
        bottom: getValidValue(bottom, 0, maxBottom),
      }
    }),
  )
  .subscribe((pos) => {
    eleVideo.style.right = pos.right + 'px'
    eleVideo.style.bottom = pos.bottom + 'px'
  })
  • 滚动到元素距离底部为0时出现小窗口
ts 复制代码
scroll$
  .pipe(map((scrollEvent) => eleAnchor.getBoundingClientRect().bottom < 0))
  .subscribe((boolean) => {
    if (boolean) {
      eleVideo.classList.add('video-fixed')
    } else {
      eleVideo.classList.remove('video-fixed')
    }
  })

计时五秒,统计累计点击次数

  • 点击按钮开始5s计时
  • 5s结束后输出累计点击次数
  • 再次点击后重新计时,以及重新累计点击次数
ts 复制代码
const add = document.getElementById('add')

let res = 0
//计时流
const timer$ = fromEvent(add, 'click').pipe(exhaustMap((x) => timer(5000)))
//点击流
const click = fromEvent(add, 'click')
  .pipe(
    mapTo(1),
    scan((x, y) => x + y), //累加
    takeUntil(timer$), //5秒结束完成点击流
    repeat(), //点击后再次重试
  )
  .subscribe((count) => {
    res = count
  })
//最后5s后输出
timer$.subscribe(() => {
  console.log(`5s: 共点击${res}次`)
})
//输出:5s: 共点击10次

Hot Observable 和 Cold Observable(冷热流)

其实前面的代码示例,每个observable都只订阅了一次,实际上observable是可以多次订阅的

ts 复制代码
const source = interval(1000).pipe(take(3))

source.subscribe({
  next: (val) => console.log('A next:' + val),
  complete: () => console.log('A完成'),
})
source.subscribe({
  next: (val) => console.log('B next:' + val),
  complete: () => console.log('B完成'),
})
//输出
//A next:0
//B next:0
//A next:1
//B next:1
//A next:2
//A完成
//B next:2
//B完成

上面这段代码,分别用 observerA 跟 observerB 订阅了 source,从 log 可以看出来 observerA 跟 observerB 都各自收到了元素,但请记得这两个 observer 其实是分开执行的也就是说他们是完全独立的,我们把 observerB 延迟订阅来证明看看

ts 复制代码
const source = interval(1000).pipe(take(3))

source.subscribe({
  next: (val) => console.log('A next:' + val),
  complete: () => console.log('A完成'),
})
setTimeout(() => {
  source.subscribe({
    next: (val) => console.log('B next:' + val),
    complete: () => console.log('B完成'),
  })
}, 1000)

//输出
//A next:0
//A next:1
//B next:0
//A next:2
//A完成
//B next:1
//B next:2
//B完成

这里我们延迟一秒再用 observerB 订阅,可以从 log 中看出 1 秒后 observerA 已经印到了 1,这时 observerB 开始印却是从 0 开始,而不是接着 observerA 的进度,代表这两次的订阅是完全分开来执行的,或者说是每次的订阅都建立了一个新的执行

这样的行为在大部分的情境下适用,但有些案例下我们会希望第二次订阅 source 不会从头开始接收元素,而是从第一次订阅到当前处理的元素开始发送,

简而言之,对于已错过的数据其实有两种处理策略

  • 错过的就让它过去,只要订阅之后生产的数据就好
  • 不能错过,订阅之前生产的数据也要

第一种策略类似于直播,第二种和点播相似。

  • 使用第一种策略的 Observable 叫做 Cold Observable,因为每次都要重新生产数据,是 "冷"的,需要重新发动。
  • 第二种,因为一直在生产数据,只要使用后面的数据就可以了,所以叫 Hot Observable。

RxJS 中如 interval、range 这些方法产生的 Observable 都是 Cold Observable,产生 Hot Observable 的是由 Promise、Event 这些转化而来的 Observable,它们的数据源都在外部,和 Observer 无关。

Cold Observable 如果没有订阅者连数据都不会产生,对于 Hot Observable,数据仍会产生,但是不会进入管道处理。

Hot Observable 用多播来处理,对于 Cold Observable,每次订阅都重新生产了一份数据流。

如果想要实现多播,就要使用 RxJS 中 Subject

Subject

手动建立subject

其实我们可以建立一个中间层订阅source然后再由中间人转送数据,就可以达到我们想要的效果

ts 复制代码
const source = interval(1000).pipe(take(3))

const observerA = {
  next: (val) => console.log('A next:' + val),
  complete: () => console.log('A完成'),
  error: (err) => console.log(err),
}
const observerB = {
  next: (val) => console.log('B next:' + val),
  complete: () => console.log('B完成'),
  error: (err) => console.log(err),
}

const subject = {
  observers: [],
  addObserver: function (observer) {
    this.observers.push(observer)
  },
  next: function (value) {
    this.observers.forEach((x) => x.next(value))
  },
  error: function (err) {
    this.observers.forEach((x) => x.error(err))
  },
  complete: function () {
    this.observers.forEach((x) => x.complete())
  },
}

subject.addObserver(observerA)
source.subscribe(subject)
setTimeout(() => {
  subject.addObserver(observerB)
}, 1000)
//输出
//A next:0
//A next:1
//B next:1
//A next:2
//B next:2
//A完成
//B完成

从上面的代码可以看到,我们先建立了一个实例叫 subject,这个实例具备 observer 所有的方法(next, error, complete),并且还能 addObserver 把 observer 加到内部的清单中,每当有值送出就会遍历清单中的所有 observer 并把值再次送出,这样一来不管多久之后加进来的 observer,都会是从当前处理到的元素接续往下走,就像范例中所示,我们用 subject 订阅 source 并把 observerA 加到 subject 中,一秒后再把 observerB 加到 subject,这时就可以看到 observerB 是直接收 1 开始,这就是**多播(multicast)**的行为。

虽然上面是自己手写的subject,但运行方式和RxJS的Subject示例是几乎一样的,我们把前面的示例换成RxJS提供的Subject试一下

ts 复制代码
const source = interval(1000).pipe(take(3))

const observerA = {
  next: (val) => console.log('A next:' + val),
  complete: () => console.log('A完成'),
  error: (err) => console.log(err),
}
const observerB = {
  next: (val) => console.log('B next:' + val),
  complete: () => console.log('B完成'),
  error: (err) => console.log(err),
}

const subject = new Subject()
subject.subscribe(observerA)
source.subscribe(subject)

setTimeout(() => {
  subject.subscribe(observerB)
}, 1000)
//输出
//A next:0
//A next:1
//B next:1
//A next:2
//B next:2
//A完成
//B完成

大家会发现使用方式跟前面是相同的,建立一个 subject 先拿去订阅 observable(source),再把我们真正的 observer 加到 subject 中,这样一来就能完成订阅,而每个加到 subject 中的 observer 都能整组的接收到相同的元素

什么是Subject

首先 Subject 可以拿去订阅 Observable(source) 代表他是一个 Observer,同时 Subject 又可以被 Observer(observerA, observerB) 订阅,代表他是一个 Observable。

总结成两句话

  • Subject 同时是 Observable 又是 Observer
  • Subject 会对内部的 observers 进行组播(multicast)

其实 Subject 就是 观察者模式(Observer Pattern )的实例并且继承自 Observable

Subject应用

上面讲了Subject其实就是观察者模式(Observer Pattern )的实例,他会在内部维护一份observer的清单,并在接收到值时,遍历这份清单并送出值

ts 复制代码
const subject = new Subject()

const observerA = {
  next: (val) => console.log('A next:' + val),
  complete: () => console.log('A完成'),
  error: (err) => console.log(err),
}
const observerB = {
  next: (val) => console.log('B next:' + val),
  complete: () => console.log('B完成'),
  error: (err) => console.log(err),
}

subject.subscribe(observerA)
subject.subscribe(observerB)

subject.next(1)
//A next:1
//B next:1
subject.next(2)
//A next:2
//B next:2

这里我们可以直接用 subject 的 next 方法传送值,所有订阅的 observer 就会接收到

下面我们来看看 Subject 的三个变形

BehaviorSubject

很多时候我们会希望Subject能代表当下的状态,而不是单纯的时间发送,也就是说如果有个新的订阅,我们希望Subject能立即给出最新值,而不是没有回应,譬如下面的例子

ts 复制代码
const subject = new Subject()

const observerA = {
  next: (val) => console.log('A:' + val),
  error: (err) => console.log('A:' + err),
  complete: () => console.log('A完成'),
}

const observerB = {
  next: (val) => console.log('B:' + val),
  error: (err) => console.log('B:' + err),
  complete: () => console.log('B完成'),
}

subject.subscribe(observerA)

subject.next(1)
//A:1
subject.next(2)
//A:2
setTimeout(() => {
  subject.subscribe(observerB)
}, 2000)
//2秒后订阅,observerB 不会收到任何值

以上面这个例子来说,observerB 订阅的之后,是不会有任何元素送给 observerB 的,因为在这之后没有执行任何 subject.next(),但很多时候我们会希望 subject 能够表达当前的状态,在一订阅时就能收到最新的状态是什么,而不是订阅后要等到有变动才能接收到新的状态,以这个例子来说,我们希望 observerB 订阅时就能立即收到 2,希望做到这样的效果就可以用 BehaviorSubject

BehaviorSubject 跟 Subject 最大的不同就是 BehaviorSubject 是用来呈现当前的值,而不是单纯的发送事件。BehaviorSubject 会记住最新一次发送的元素,并把该元素当作目前的值,在使用上 BehaviorSubject 需要传入一个参数来代表起始的状态,示例如下

ts 复制代码
const subject = new BehaviorSubject(0)

const observerA = {
  next: (val) => console.log('A:' + val),
  error: (err) => console.log('A:' + err),
  complete: () => console.log('A完成'),
}

const observerB = {
  next: (val) => console.log('B:' + val),
  error: (err) => console.log('B:' + err),
  complete: () => console.log('B完成'),
}

subject.subscribe(observerA)
//A:0
subject.next(1)
//A:1
subject.next(2)
//A:2
setTimeout(() => {
  subject.subscribe(observerB)
  //B:2
}, 2000)

从上面这个示例可以看得出来 BehaviorSubject 在建立时就需要给定一个初始值,并在之后任何一次订阅,就会先送出最新的状态。

ReplaySubject

在某些时候我们会希望 Subject 代表事件,但又能在新订阅时重新发送最后的几个元素,这时我们就可以用 ReplaySubject

ts 复制代码
const subject = new ReplaySubject(2) //重新发送最后两个元素

const observerA = {
  next: (val) => console.log('A:' + val),
  error: (err) => console.log('A:' + err),
  complete: () => console.log('A完成'),
}

const observerB = {
  next: (val) => console.log('B:' + val),
  error: (err) => console.log('B:' + err),
  complete: () => console.log('B完成'),
}

subject.subscribe(observerA)
subject.next(1)
//A:1
subject.next(2)
//A:2
subject.next(3)
//A:3
setTimeout(() => {
  subject.subscribe(observerB)
  //B:2
  //B:3
}, 2000)

AsyncSubject

AsyncSubject 比较难理解,会在 subject 结束后送出最后一个值

ts 复制代码
const subject = new AsyncSubject()

const observerA = {
  next: (val) => console.log('A:' + val),
  error: (err) => console.log('A:' + err),
  complete: () => console.log('A完成'),
}

const observerB = {
  next: (val) => console.log('B:' + val),
  error: (err) => console.log('B:' + err),
  complete: () => console.log('B完成'),
}

subject.subscribe(observerA)
subject.next(1)
subject.next(2)
subject.next(3)
subject.complete()
//A:3
//A完成
setTimeout(() => {
  subject.subscribe(observerB)
  //B:3
  //B完成
}, 2000)

从上面的代码可以看出来,AsyncSubject 会在 subject 完成后才送出最后一个值,其实这个行为跟 Promise 很像,都是等到事情结束后送出一个值,但实务上我们非常非常少用到 AsyncSubject,绝大部分的时候都是使用 BehaviorSubject 跟 ReplaySubject 或 Subject

多播操作符

前面的代码我们用 subject 订阅了 source,再把 observerA 跟 observerB 一个个订阅到 subject,这样就可以让 observerA 跟 observerB 共用同一个执行。但这样的写法会让代码看起来太过复杂。所以接下来我们可以使用多播操作符。

share前的操作符v8版本将舍弃,v7 的多播被简化为 share、connect 和 connectable

  • multicast

    • 这个操作符返回了一个 connectable 的 Observable。等到执行 connect() 才会用真的 subject 订阅 source,并开始发送数据,如果没有 connect,Observable 是不会执行的

    • multicast接受两个参数接收一个 subject 或者 subject factory。

      • 第一个参数可以是一个 subject,还可以是一个 subject factory,即返回 subject 的函数。当返回subject factory,相当于使用了不同的中间人,每个观察者订阅时都重新生产数据,适用于退订了上游之后再次订阅的场景

        ts 复制代码
        const source = interval(1000).pipe(take(3), multicast(new Subject()))
        //subject factory
        //const source = interval(1000).pipe(take(3), multicast(()={return new Subject()})
        const observerA = {
          next: (val) => console.log('A:', val),
          error: (err) => console.log('A:', err),
          complete: () => console.log('A完成'),
        }
        const observerB = {
          next: (val) => console.log('B:', val),
          error: (err) => console.log('B:', err),
          complete: () => console.log('B完成'),
        }
        source.subscribe(observerA)
        //connect后会真的用 subject 订阅 source,并开始送出元素,如果没有执行 connect() observable 是不会真正执行的
        source.connect()
        setTimeout(() => {
          source.subscribe(observerB)
        }, 1000)
        
        //输出
        //A:0
        //A:1
        //B:1
        //A:2
        //B:2
        //A完成
        //B完成
        • 另外需要注意,如果要退订,需要把connect()回传的subscription 退订才会真正停止 observable 的执行
        ts 复制代码
        const source = interval(1000).pipe(multicast(new Subject()))
        const observerA = {
          next: (val) => console.log('A:', val),
          error: (err) => console.log('A:', err),
          complete: () => console.log('A完成'),
        }
        const observerB = {
          next: (val) => console.log('B:', val),
          error: (err) => console.log('B:', err),
          complete: () => console.log('B完成'),
        }
        const subA = source.subscribe(observerA)
        const connectSub = source.connect()
        let subB
        setTimeout(() => {
          subB = source.subscribe(observerB)
        }, 1000)
        
        setTimeout(() => {
          subA.unsubscribe()
          subB.unsubscribe()
          // 这里虽然 A 跟 B 都退订了,但 source 还会继续送元素
        }, 3000)
        
        setTimeout(() => {
          connectSub.unsubscribe()
          //这里 source 才会真正停止送元素,结束
        }, 4000)
      • multicast 还可以接收可选的第二个参数,称为 selector 参数。它可以使用上游数据流任意多次,而不会重复订阅上游的数据。当使用了这个参数时,multicast 不会返回 connectable Observable,而是这个参数(回调函数)返回的 Observable。selecetor 回调函数有一个参数,通常叫做 shared,即 multicast 第一个参数所代表的 subject 对象

      ts 复制代码
      const selector = (shared) => {
        return concat(shared, of('done'))
      }
      const source = interval(1000).pipe(take(3), multicast(new Subject(), selector))
      
      const observerA = {
        next: (x) => console.log('Observer A: ' + x),
        error: null,
        complete: () => console.log('Observer A completed'),
      }
      const observerB = {
        next: (x) => console.log('Observer B: ' + x),
        error: null,
        complete: () => console.log('Observer B completed'),
      }
      
      source.subscribe(observerA)
      setTimeout(() => {
        source.subscribe(observerB)
      }, 5000)
      // Observer A: 0
      // Observer A: 1
      // Observer A: 2
      // Observer A: done
      // Observer A completed
      // Observer B: done
      // Observer B: completed
  • refCount

    • 上面使用了 multicast,但是还是有些麻烦,还需要去手动 connect。这时我们可以再搭配 refCount 操作符创建只要有订阅就会自动 connect 的 Observable,立即执行并发送元素。只需要去掉 connect 方法调用,在 multicast 后面再加一个 refCount 操作符
    ts 复制代码
    const source = interval(1000).pipe(take(3), multicast(new Subject()), refCount())
    const observerA = {
      next: (val) => console.log('A:', val),
      error: (err) => console.log('A:', err),
      complete: () => console.log('A完成'),
    }
    const observerB = {
      next: (val) => console.log('B:', val),
      error: (err) => console.log('B:', err),
      complete: () => console.log('B完成'),
    }
    source.subscribe(observerA)
    // 订阅数 0 => 1
    
    setTimeout(() => {
      source.subscribe(observerB)
      // 订阅数 0 => 2
    }, 1000)
    • 上面这段代码,当 source 一被 observerA 订阅时(订阅数从 0 变成 1),subject 订阅上游数据流,我们就不需要再额外执行 connect。同样的在退订时只要订阅数变成 0 时退订上游数据流
  • publish

    • 其实 multicast(new Rx.Subject()) 很常用到,我们有一个简化的写法那就是 publish,下面这两段代码是完全等价的

      ts 复制代码
      var source = Rx.Observable.interval(1000).publish().refCount()
      
      var source = Rx.Observable.interval(1000).multicast(new Rx.Subject()).refCount()
  • share

    • share 是 multicast 和 refCount 的简写。但是,share 传给 multicast 的是工厂函数( subject factory),share() 等同于在 pipe 中先调用了 multicast(() => new Subject()),然后再调用 refCount()。
    ts 复制代码
    const source = interval(1000).pipe(take(3), share())
    // const source = Rx.Observable.interval(1000)
    //             .publish()
    //             .refCount();
    
    // const source = Rx.Observable.interval(1000)
    //             .multicast(new Rx.Subject())
    //             .refCount();
    const observerA = {
      next: (val) => console.log('A:', val),
      error: (err) => console.log('A:', err),
      complete: () => console.log('A完成'),
    }
    const observerB = {
      next: (val) => console.log('B:', val),
      error: (err) => console.log('B:', err),
      complete: () => console.log('B完成'),
    }
    source.subscribe(observerA)
    
    setTimeout(() => {
      source.subscribe(observerB)
    }, 1000)
    //输出
    //A:0
    //A:1
    //B:1
    //A:2
    //B:2
    //A完成
    //B完成
  • ShareReplay

    • shareReplay等同于multicast(() => new ReplaySubject()),refCount(),订阅者在事件发出后订阅也可以获得值
      • 接收一个参数:
        • 当不传时, 缓存并且重播所有值
        • 当传数字(n)时,指定缓存并重播最近的 n个值。
    • 它可以将热流转换为冷流,并且支持共享订阅和重播最近发出的值。使用 shareReplay 操作符可以避免多次订阅时重新执行源 Observable,同时还可以使每个订阅者都能收到相同的值序列。这对于需要缓存和共享已经发出的值的情况非常有用。
    ts 复制代码
    const source = interval(1000).pipe(take(3), shareReplay())
    const observerA = {
      next: (val) => console.log('A:', val),
      error: (err) => console.log('A:', err),
      complete: () => console.log('A完成'),
    }
    const observerB = {
      next: (val) => console.log('B:', val),
      error: (err) => console.log('B:', err),
      complete: () => console.log('B完成'),
    }
    source.subscribe(observerA)
    
    setTimeout(() => {
      source.subscribe(observerB)
    }, 3000)
    //输出
    //A:0
    //A:1
    //A:2
    //A完成
    //B:0
    //B:1
    //B:2
    //B:完成

文章参考: rxjs6学习指南 rxjs中文网 30天精通rxjs 理解publish和share操作符

相关推荐
如若12328 分钟前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~1 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语1 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport1 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg1 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全
胡西风_foxww1 小时前
【es6复习笔记】rest参数(7)
前端·笔记·es6·参数·rest
m0_748254881 小时前
vue+elementui实现下拉表格多选+搜索+分页+回显+全选2.0
前端·vue.js·elementui
星就前端叭2 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
m0_748234522 小时前
前端Vue3字体优化三部曲(webFont、font-spider、spa-font-spider-webpack-plugin)
前端·webpack·node.js
Web阿成2 小时前
3.学习webpack配置 尝试打包ts文件
前端·学习·webpack·typescript