如何设计自己的 RxJS Operators

今天我们来聊点轻松(?)的主题 - 「如何设计出自己的 RxJS Operators」吧!

为何要自己设计 opereators

RxJS 提供了超过 100 个 operators,其实已经可以应用非常非常多的情境了,还需要自己设计 operator 吗?其实我们确实是不一定需要设计 operator 的,但以下几种状况,可能很适合自己设计 operator。

  • 单元测试:当我们将一堆 operators 使用 pipe 串起来时,多多少少会需要加上一些 side effect 的程序码,而这样的行为会让我们撰写单元测试时变得更不容易,此时我们可以把 side effect 前和後的 operators 各自建立成新的 operators 独立测试。
  • 共用性:假设我们是负责撰写 library 的开发人员,在提供共用的功能时,我们不太可能跟使用 library 的人说:「你就去把某几个 operators 串起来就可以啦!」,这时候就适合把共用的部分抽出来,让其他人更容易的使用。
  • 可读性:当功能越来越复杂时,是有可能在一个 pipe 里面一口气写数十个 operators 的!这时候反而可能会造成阅读上更加不易,维护上亦然。那麽将不同组的动作抽成独立的 operators,不仅可读性会更高,也能让关注点再次分离。
  • 重构:我们会重构程序码,当然也会重构 operators,将 operators 抽出成新的 operator,就跟把一段复杂的程序码抽成一个 function 一样。
  • 真的没有适合的 operator:实际上应该是不太可能发生,就像阵列处理只要 mapfilterreduce 几乎就可以完成各种变化,其他都只是让语意更明确、使用更方便一样;我们其实也可以透过 mapfilterreduce operators 组合出任何想要的功能才对,最多就是程序写起来更丑更难维护而已。

接着让我们再认识一次 RxJs 的 Operators 定义,然後进入实作吧!

再次认识 RxJS Operators 结构

所谓的 operator,其实就是个 curry function!在之前介绍 「RxJS 的 functional programming 文章中」,我们曾经看过 map 的基本结构:

export function map<T, R>(project: (value: T, index: number) => R, thisArg?: any): OperatorFunction<T, R> {
 return function mapOperation(source: Observable<T>): Observable<R> {
  ...
 };
}

curry function 最外层是设定相关资料就不用多说了,这个 function 需要回传一个 OperatorFunction,而内层的 function mapOperation 实际上就是回传这个 OperatorFunction 且传入参数和回传值都是一个「Observable」,如果多看几个 operator 的程序码,可以发现结构都是一致的!也就是说,我们只要会定义一个「以一个 Observable 当作参数,且能够回传一个 Observable 的 function」,就等於时做出一个 RxJS 的 operator 罗!

以下定义一个「不做任何事情」的 operator:

const doNothing = (source) => {
  return source;
}

实际使用:

source$ = from(1, 2, 3, 4);
source$.pipe(
  doNothing
);

就是这麽简单,当我们产生订阅 (subscribe) 时,RxJS 就会把来源 Observable (source$),当作参数去呼叫 doNothing 这个自订的 operator,再将会传的 Observable 传入下一个 operator,直到最後。

如果需要定义「有参数」的 operator,写个 curry function 就好了:

const doSomething = (args) => {
  return (source) => {
	  return source;
  };
};

很简单吧!接着就是在 function 里面加上变化,让回传的 Observable 更佳符合握们的需求啦。

两种自订 Operator 的方法

在之前介绍 map 的文章中,我们举了个例子,「将学生分数调整成开根号後乘以 10,并指显示及格的学生」,我们就来尝试看看如何将这样的逻辑抽成自订的 operator 吧!

直接串现有的 operators

既然 operator 的逻辑是将现有的 Observable 参数转换成一个新的 Observable,那麽最简单的方式当然是将传入的 Observable 参数搭配现有的 operators,产生一个新的 Observable 回传啦!

const adjustAndFilterPassScore = () => {
  return (source$: Observable<number>) => {
    return source$.pipe(
      map(score => Math.sqrt(score) * 10),
      filter(score => score >= 60)
    )
  }
};

如果单纯使用 function 时,可以写成:

const scores$ = of(0, 16, 36, 49, 100);
adjustAndFilterPassScore()(sources$).subscribe();

当然,有了 pipe 我们就不会这样写啦!使用 pipe 的写法:

const scores$ = of(0, 16, 36, 49, 100);
score$.pipe(
  adjustAndFilterPassScore()
);

我们也可以将「调整成绩」和「过滤成绩」两个行为拆开成两个各自的 operator,最後再组合起来:

const adjustScore = () => {
  return (source$: Observable<number>) => {
    return source$.pipe(
      map(score => Math.sqrt(score) * 10)
    )
  }
};

const filterPassScore = () => {
  return (source$: Observable<number>) => {
    return source$.pipe(
      filter(score => score >= 60)
    )
  }
};

const adjustAndFilterPassScore = () => {
  return (source$: Observable<number>) => {
    return source$.pipe(
      adjustScore(),
      filterPassScore()
    )
  }
};

of(0, 16, 36, 49, 100)
  .pipe(
    adjustAndFilterPassScore()
  ).subscribe(score => {
    console.log(`自订 operator 示范 (1): ${score}`);
  });
// 60
// 70
// 100

看起来程序码好像变多了,但其实是让 operator 要专注的事情更少了,未来维护上会更加容易喔!

如果需要加上指定及格分数呢?很简单!curry function 是个好东西!!

const filterPassScoreBy = (passScore: number) => {
  return (source$: Observable<number>) => {
    return source$.pipe(
      filter(score => score >= passScore)
    )
  };
};

const adjustAndFilterPassScoreBy = (passScore: number) => {
  return (source$: Observable<number>) => {
    return source$.pipe(
      adjustScore(),
      filterPassScoreBy(passScore)
    );
  };
};

of(0, 16, 36, 49, 100)
  .pipe(
    // 指定及格成绩
    adjustAndFilterPassScoreBy(70)
  ).subscribe(score => {
    console.log(`自订 operator 示范 (2): ${score}`);
  });
// 70
// 100

很容易吧!

程序码:https://stackblitz.com/edit/mastering-rxjs-customize-operators-by-piping-other-operators

从新的 Observable 开始

另外一种自订 operator 的方法,就是从一个新的 Observable 开始,这麽做的好处是具有更大的弹性,不过就需要更全面地进行考量罗!一样拿上述的例子来看,中间的各种观念就省略了,直接看看程序码:

const adjustAndFilterPassScoreBy = (passScore: number) => {
  return source$ => {
    // 建立新的 Observable
    return new Observable(subscriber => {
      // 订阅来源 Observable
      // 并建立观察者 Observer 来处理来源 Observable 的各种事件
      source$.subscribe({
        next: score => {
          // 成绩转换
          const newScore = Math.sqrt(score) * 10;
          // 判断成绩决定要不要产生新事件
          if (newScore >= passScore) {
            // 及格,产生新事件
            subscriber.next(newScore);
          }
        },
        // 也要处理 error 和 complete 事件
        error: error => subscriber.error(error),
        complete: () => subscriber.complete()
      });
    });
  };
};

第 4 行程序建立并回传一个新的 Observable,因此所有发生事件的时机就可以在里面的 callback function 内自行决定;由於 source$ 是我们的资料来源,因此在第 7 行程序直接订阅它,并建立一个 Observer 来处理 source$ 订阅的 next()error()complete() 事件,当来源 Observable 有新的 next() 事件时,依照我们自定义的逻辑来处理

  • 第一步进行成绩转换
  • 第二部判断是否及格,及格才让新的 Observable 产生事件

另外要注意的是,虽然我们只专注在 next(),但 error()complete() 也需要处理,在来源 Observable 发生错误或完成时,後续的 operators 或实际订阅的 Observer 才会知道有事情发生了!

程序码:https://stackblitz.com/edit/mastering-rxjs-custom-operator-by-new-observable

这种从新的 Observable 开始处理的方式,也是许多 RxJS operators 底层实际处理的方式。

本日小结

今天我们学会了如何建立出属於自己的 RxJS operators,各自有好有坏:

  • 直接转现有的 operators:简单易懂,宣告式 (declarative) 的程序码也好阅读;虽然可以满足大部分的需求了,但缺乏一点弹性
  • 使用新的 Observable:具有最大弹性,但程序码变成指令式 (imperative) 的了,需要更小心撰写出好读好维护的程序码,同时也必须自行处理 Observer 内所有的事件。

学会自订 operators,就能写出更加漂亮的 RxJS 程序码罗!


<<:  mostly:functional 谢幕与片尾曲

>>:  延长赛:码农最後的哄擡价格,高级操作:说出一口聚合分析(下)

#10-帮网页加上黑暗模式!日夜开关(CSS变数&Media Query)

这几年手机、电脑有黑暗模式,很多网页也加上像电灯一样的开关啦! 黑暗模式可以降低亮度,减少对眼睛的压...

【修正模型】4-4 完赛,但我们才正要开始

今年仍然一如以往的疯狂赶稿 XD,几乎每天下班就开始准备隔天的内容,最後经过了三十多天的铁人赛今天告...

【DAY 02】如何选择网页开发的编辑器

前言 在学程序之前当然就是要先选择好适合自己的编译器啦~ 有许许多多的网页开发工具中如何选择呢? 我...

梳理useEffect和useLayoutEffect的原理与区别

点击进入React源码调试仓库。 React在构建用户界面整体遵循函数式的编程理念,即固定的输入有固...

买菸赔菸 - 零股买卖

import shioaji as sj api = sj.Shioaji() accounts =...