🌳

自定义一个 Operators

 

带有自定义操作符的声明式 RxJS

notion image
通常,当需要大量管道运算符时,RxJS 流往往会变得有点混乱和不可读。这种不可读性的原因之一是这些 RxJS 流的命令性。因此,通过使用自定义运算符使它们具有声明性,可以使您的流更加清晰和可读。
本文最初发布于ng-journal.com

声明式与命令式编程

不一定特定于 Angular 或 Typescript,关于声明式编程与命令式编程的讨论是所有编程语言的普遍讨论。它的核心是让代码更自然地可读,但缺点是需要进行更多的代码提取。通常,声明式方法被认为更干净,因为它更具可读性。它基本上通过函数提取隐藏了许多低级细节,并定义了非常明确和可读的函数名称。
// Imperative

const arr = [1, 2, 3, 4, 5, 6];
const even = [];

for (const item of arr) {
  if (item % 2 === 0) {
    even.push(item);
  }
}

console.log(even); // 2, 4, 6
// Declarative

const arr = [1, 2, 3, 4, 5, 6];

const even = arr.filter(item => isEven(item));

console.log(even); // 2, 4, 6

function isEven(value: number): boolean {
  return value % 2 === 0;
}
这两个示例都是完全有效且可执行的代码,但后者在可读性方面更好,因为它将许多低级功能提取到具有显式命名的不同函数中。但是这个概念如何转化为 RxJS?不是所有的 RxJS 代码都是声明性的吗?嗯,是的,但也不是……
我们使用管道运算符并以任何顺序链接它们,这本身就是声明式的,但是当在运算符中添加更多逻辑时,它往往更加命令式。为了防止命令式范式,我们可以创建自定义管道运算符并将逻辑提取到其中。

自定义管道运算符

编写自定义运算符比人们想象的要容易得多。最简单的运算符就是获取可观察值并返回可观察值的函数。因此,我们可以简单地返回传递的 observable 并通过管道将其添加到那里并添加一些运算符。但更准确地说,我们应该编写一个高阶函数,该函数返回一个函数,该函数然后获取一个可观察对象并返回一个新的可观察对象。一句话包含了很多意思,但不要担心,因为下面的代码会更好地解释这一点:
export function log<T>(): (source$: Observable<T>) => Observable<T> {
  return (source$) => source$.pipe(tap(console.log));
}
我们使用泛型,这样类型就不会通过我们的自定义管道运算符改变。然后我们返回一个获取 source$ 的匿名函数,它基本上只是外部可观察对象,然后返回它的一个管道。

例子

此示例包含一个表单控件,用户可以在其中放置计算数字的命令。这样的命令可能如下所示:
  • 添加,1,2,3,4
  • 减去,1,1,1,2
  • 相乘,3,4,5
在重构之前,result$ 流非常大并且在一个地方包含了所有逻辑。因此,如果不仔细观察,最初阅读起来并不容易。重构后,result$ 流更具声明性,因为自定义管道运算符隐藏了实现细节,仅通过名称告知运算符在做什么。因此直接阅读很简单,无需处理低级实现细节。

一、之前

result$ = this.control.valueChanges.pipe(
  debounceTime(800),
  distinctUntilChanged(),
  filter((v): v is string => !!v && typeof v === 'string'),
  map((v) => v.split(',')),
  filter((v) => v.length >= 1),
  map((v) => {
    if (v[0] === 'add') {
      return v
        .slice(1)
        .map((v) => +v)
        .reduce((acc: number, current: number) => {
          return acc + current;
        }, 0);
    } else if (v[0] === 'subtract') {
      return v
        .slice(1)
        .map((v) => +v)
        .reduce((acc: number, current: number) => {
          return acc - current;
        }, 0);
    } else if (v[0] === 'multiply') {
      return v
        .slice(1)
        .map((v) => +v)
        .reduce((acc: number, current: number) => {
          return acc * current;
        }, 1);
    }
    return null;
  })
);

二。

import {
  assertNumber,
  assertString,
  command,
  lookAhead,
  split,
} from './custom-operators';

[...]

result$ = this.control.valueChanges.pipe(
  lookAhead(),
  assertString(),
  split(),
  command(),
  assertNumber()
);
export function lookAhead<T>(): (source$: Observable<T>) => Observable<T> {
    return (source$) => source$.pipe(debounceTime(800), distinctUntilChanged());
  }
  
  export function assertString(): (
    source$: Observable<unknown>
  ) => Observable<string> {
    return (source$) =>
      source$.pipe(
        filter((value): value is string => !!value && typeof value === 'string')
      );
  }
  
  export function assertNumber(): (
    source$: Observable<unknown>
  ) => Observable<number> {
    return (source$) =>
      source$.pipe(
        filter((value): value is number => !!value && typeof value === 'number')
      );
  }
  
  export function split(): (source$: Observable<string>) => Observable<string[]> {
    return (source$) =>
      source$.pipe(
        map((v) => v.split(',')),
        filter((v) => v.length >= 1)
      );
  }
  
  export function command(): (
    source$: Observable<string[]>
  ) => Observable<number | null> {
    return (source$) =>
      source$.pipe(
        map((v) => {
          if (v[0] === 'add') {
            return v
              .slice(1)
              .map((v) => +v)
              .reduce((acc: number, current: number) => {
                return acc + current;
              }, 0);
          } else if (v[0] === 'subtract') {
            return v
              .slice(1)
              .map((v) => +v)
              .reduce((acc: number, current: number) => {
                return acc - current;
              }, 0);
          } else if (v[0] === 'multiply') {
            return v
              .slice(1)
              .map((v) => +v)
              .reduce((acc: number, current: number) => {
                return acc * current;
              }, 1);
          }
          return null;
        })
      );
  }

GitHub 资料库

您可以在页面底部找到 GitHub Repo 链接:

引用