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

通常,当需要大量管道运算符时,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 链接: