深入浅出 RxJS 之 操作符

2023-05-17 20:13:27 浏览数 (1)

# 为什么要有操作符

一个操作符是返回一个 Observable 对象的函数,不过,有的操作符是根据其他 Observable 对象产生返回的 Observable 对象,有的操作符则是利用其他类型输入产生返回的 Observable 对象,还有一些操作符不需要输入就可以凭空创造一个 Observable 对象。

所有操作符中最容易理解的可能就是 mapfilter ,因为 JavaScript 的数组对象就有这样两个同名的函数 mapfilter

代码语言:javascript复制
const source = [1, 2, 3, 4];

const result = source.filter(x => x % 2 === 0).map(x => x * 2);

console.log(result);
// [4, 8]

几个关键点:

  • filtermap 都是数组对象的成员函数
  • filtermap 的返回结果依然是数组对象
  • filtermap 不会改变原本的数组对象

因为上面的特点,filtermap 可以链式调用,一个复杂的功能可以分解为多个小任务来完成,每个小任务只需要关注问题的一个方面。此外,因为数组对象本身不会被修改,小任务之间的耦合度很低,这样的代码就更容易维护。

代码语言:javascript复制
const result$ = source$.filter(x => x % 2 === 0).map(x => x * 2);
result$.subscribe(console.log);

在 RxJS 的世界中,filtermap 这样的函数就是操作符,每个操作符提供的只是一些通用的简单功能,但通过链式调用,这些小功能可以组合在一起,用来解决复杂的问题。

# 操作符的分类

# 功能分类

  • 创建类(creation)
  • 转化类(transformation)
  • 过滤类(filtering)
  • 合并类(combination)
  • 多播类(multicasting)
  • 错误处理类(error Handling)
  • 条件分支类(conditional&boolean)
  • 数学和合计类(mathmatical&aggregate)
  • 其他
    • 背压控制类(backpressure control)
    • 可连接类(connectable)
    • 高阶 Observable(higher order observable)处理类

# 静态和实例分类

所有的操作符都是函数,不过有的操作符是 Observable 类的静态函数,也就是不需要 Observable 实例就可以执行的函数,所以称为“静态操作符”;另一类操作符是 Observable 的实例函数,前提是要有一个创建好的 Observable 对象,这一类称为“实例操作符”。

无论是静态操作符还是实例操作符,它们都会返回一个 Observable 对象。

一个操作符应该是静态的形式还是实例的形式,完全由其功能决定。有意思的是,有些功能既可以作为 Observable 对象的静态方法,也可以作为 Observable 对象的实例方法。

# 如何实现操作符

# 操作符函数的实现

每个操作符都是一个函数,不管实现什么功能,都必须考虑下面这些功能要点:

  • 返回一个全新的 Observable 对象
  • 对上游和下游的订阅及退订处理
  • 处理异常情况
  • 及时释放资源
代码语言:javascript复制
function map (project) {
  // 返回新的 Observable 对象,可以用于链式调用
  return new Observable(observer => {
    // this 代表上游的 Observable 对象
    const sub = this.subscribe({
      next: value => {
        // 异常捕获,并传递给下游
        try {
          observer.next(project(value))
        } catch (error) {
          observer.error(error)
        }
      },
      // error 和 complete 直接交给下游处理
      error: error => observer.error(error),
      complete: () => observer.complete()
    });

    // 当下游退订时,需要对上游做退订的动作
    return {
      unsubscribe: () => {
        sub.unsubscribe();
      }
    };
  });
}

# 关联 Observable

  1. Observable 打补丁
代码语言:javascript复制
// 实例操作符
Observable.prototype.map = map;

如果是静态操作符,则是直接赋值给 Observable 类的某个属性。

  1. 使用 bind 绑定特定 Observable 对象
代码语言:javascript复制
const result$ = map.bind(source$)(project);

代码语言:javascript复制
const operator = map.bind(source$);
const result$ = operator(project);

  1. 使用 lift

RxJS v5 版本对架构有很大的调整,很多操作符都使用一个神奇的 lift 函数实现,lift 的含义就是“提升”,功能是把 Observable 对象提升一个层次,赋予更多功能。liftObservable 的实例函数,它会返回一个新的 Observable 对象,通过传递给 lift 的函数参数可以赋予这个新的 Observable 对象特殊功能。

代码语言:javascript复制
function map (project) {
  return this.lift(function (source$) {
    return source$.subscribe({
      next: value => {
        try {
          this.next(project(value))
        } catch (error) {
          this.error(error)
        }
      },
      error: error => this.error(error),
      complete: () => this.complete()
    });
  });
}

Observable.prototype.map = map;

虽然 RxJS v5 的操作符都架构在 lift 上,应用层开发者并不经常使用 lift ,这个 lift 更多的是给 RxJS 库开发者使用。

# 改进的操作符定义

如果严格遵照函数式编程的思想,应该尽量使用纯函数,纯函数的执行结果应该完全由输入参数决定,如果函数中需要使用 this ,那就多了一个改变函数行为的因素,也就算不上真正的纯函数了。定义操作符的函数中访问 this ,实际上违背了面向函数式编程的原则。

  1. 操作符和 Observable 关联的缺陷

无论是静态操作符还是实例操作符,通常在代码中只有用到了某个操作符才导入(import)对应的文件,目的就是为了减少最终的打包文件大小。

用给 Observable “打补丁”的方式导入操作符,每一个文件模块影响的都是全局唯一的那个 Observable

  1. 使用 call 来创建库

对于实例操作符,可以使用前面介绍过的 bind/call 方法,让一个操作符函数只对一个具体的 Observable 对象生效;对于静态操作符,就直接使用产生 Observable 对象的函数,而不是依附于 Observable 的静态函数。

静态操作符不能包含对 this 的访问,所以其实不需要和 Observable 类有任何关系,以前把它们挂在 Observable 类上,纯粹就是为了表示两者有些语义联系而已。

对于实例操作符,因为函数实现要访问 this ,所以需要用 bind 或者 call 的方式来绑定 this

0 人点赞