今天我们来聊点轻松(?)的主题 - 「如何设计出自己的 RxJS Operators」吧!
RxJS 提供了超过 100 个 operators,其实已经可以应用非常非常多的情境了,还需要自己设计 operator 吗?其实我们确实是不一定需要设计 operator 的,但以下几种状况,可能很适合自己设计 operator。
pipe
串起来时,多多少少会需要加上一些 side effect 的程序码,而这样的行为会让我们撰写单元测试时变得更不容易,此时我们可以把 side effect 前和後的 operators 各自建立成新的 operators 独立测试。pipe
里面一口气写数十个 operators 的!这时候反而可能会造成阅读上更加不易,维护上亦然。那麽将不同组的动作抽成独立的 operators,不仅可读性会更高,也能让关注点再次分离。map
、filter
和 reduce
几乎就可以完成各种变化,其他都只是让语意更明确、使用更方便一样;我们其实也可以透过 map
、filter
和 reduce
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 更佳符合握们的需求啦。
在之前介绍 map 的文章中,我们举了个例子,「将学生分数调整成开根号後乘以 10,并指显示及格的学生」,我们就来尝试看看如何将这样的逻辑抽成自订的 operator 吧!
既然 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
另外一种自订 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()
事件时,依照我们自定义的逻辑来处理
另外要注意的是,虽然我们只专注在 next()
,但 error()
和 complete()
也需要处理,在来源 Observable 发生错误或完成时,後续的 operators 或实际订阅的 Observer 才会知道有事情发生了!
程序码:https://stackblitz.com/edit/mastering-rxjs-custom-operator-by-new-observable
这种从新的 Observable 开始处理的方式,也是许多 RxJS operators 底层实际处理的方式。
今天我们学会了如何建立出属於自己的 RxJS operators,各自有好有坏:
学会自订 operators,就能写出更加漂亮的 RxJS 程序码罗!
>>: 延长赛:码农最後的哄擡价格,高级操作:说出一口聚合分析(下)
这几年手机、电脑有黑暗模式,很多网页也加上像电灯一样的开关啦! 黑暗模式可以降低亮度,减少对眼睛的压...
今年仍然一如以往的疯狂赶稿 XD,几乎每天下班就开始准备隔天的内容,最後经过了三十多天的铁人赛今天告...
前言 在学程序之前当然就是要先选择好适合自己的编译器啦~ 有许许多多的网页开发工具中如何选择呢? 我...
点击进入React源码调试仓库。 React在构建用户界面整体遵循函数式的编程理念,即固定的输入有固...
import shioaji as sj api = sj.Shioaji() accounts =...