我如何对 RxJs 进行逆向工程并学习反应式编程?
是的,标题没有打错。我们实际上要对 RxJs 进行逆向工程(接下来会有大量代码 ;) )。但在继续之前,让我先告诉你我为什么要开始这项疯狂的尝试。
作为程序员,我们天生好奇。我每天都在使用 RxJs 和 React.js 等响应式库。然而,在一个阳光明媚的早晨,我突然好奇这些框架是如何在底层利用响应式编程的。
我真懂响应式编程是什么吗?它究竟是怎么工作的?我问自己。
经过一个周末的钻研,我浏览了各种博客文章和书籍,大概明白了这个概念。然而,我觉得逆向工程才是真正搞清楚这些概念的好方法,所以我决定对 RxJs 进行逆向工程。
简介:
反应式编程是使用异步数据流进行的编程。
数据流可以是任何东西,可以是用户输入(例如鼠标位置、点击事件),也可以是来自服务器的数据流(例如 Twitter 消息,或者来自套接字服务器的实时数据)。我们的应用程序将根据这些数据流做出反应。
例如,当你实时接收 Twitter 信息时,你的应用程序状态会发生变化。你可能想把最受欢迎的推文放在最顶部。因此,你的应用程序会订阅传入的数据流,并根据数据做出反应,将最受欢迎的推文放在最顶部。简而言之,这种订阅数据流并相应地更改应用程序的概念就是响应式编程。
你是不是觉得无聊?相信我,这篇文章不会是那种充斥着各种概念的博文。我们现在就开始深入代码吧。
让我们构建一个名为的类,Observable
因为它是 RxJs 最基本的构建块。
class Observable {
constructor() {
this.fnArray = [];
}
subscribe() {}
emit() {}
}
const o = new Observable();
好了,我们刚刚创建了一个名为 Observable 的基础类,它包含两个方法。我们初始化了一个名为 fnArray 的空列表。这个数组将保存我们所有订阅的对象。
我们subscribe
先来实现这个方法。这个方法将接受一个函数作为参数,并将其推送到 fnArray 中。
subscribe(fn) {
this.fnArray.push(fn);
}
现在让我们emit
也实现这个函数。emit 函数的作用是循环fnArray
并逐个执行这些函数。
emit(v) {
for (let fun of this.fnArray) {
fun(v);
}
}
我们也可以用 map 代替 for 循环。但为什么呢?因为 JS 圈里的那些酷小子们现在显然在做这件事。而且 curry 函数也挺酷的!所以现在就来试试吧。
emit(v) {
- for (let fun of this.fnArray) {
- fun(v);
- }
+ this.fnArray.map(fun => fun(v))
}
好的,现在让我们使用我们新创建的类。
function printFunction(thing) {
console.log(`I will print the ${thing}`)
}
const o = new Observable();
o.subscribe(printFunction);
首先,我们创建了一个函数printFunction
来打印传入的任何变量。我们初始化了一个新的可观察实例并subscribe
在其上调用该方法并传入我们的printFunction
as 参数。
记住,printFunction
将被存储在 中fnArray
。现在,如果我们调用 emit 方法,你认为会发生什么?让我们尝试一下
o.emit("Apple");
o.emit("Orange");
o.emit("Pear");
这给了我们以下输出
I will print the Apple
I will print the Orange
I will print the Pear
好了,现在我们可以订阅一个函数或事件,并根据该函数发出一些内容了。目前为止,完整的代码如下。
class Observable {
constructor() {
this.fnArray = [];
}
subscribe(fn) {
this.fnArray.push(fn);
}
emit(v) {
this.fnArray.map(fun => fun(v));
}
}
function printFunction(thing) {
console.log(`I will print the ${thing}`);
}
const o = new Observable();
o.subscribe(printFunction);
o.emit("Apple");
o.emit("Orange");
o.emit("Pear");
现在让我们进入有趣的部分。我们可以订阅多个函数。例如,我们可以这样做
o.subscribe(x => console.log(x * 2));
o.subscribe(x => console.log(x + 2));
o.emit(4)
返回
// 8
// 6
因为我们的 emit 调用循环遍历了在类构造函数上初始化的函数数组中的所有函数。
注意,我现在用的是箭头函数。我们也可以任意组合我们的函数。
const square = num => num * num;
o.subscribe(x => printFunction(x * 2));
o.subscribe(x => printFunction(square(x)));
o.emit(4);
// outputs
// I will print the 8
// I will print the 16
在第一个场景中,我们用 组合了我们的函数printFunction
。在第二个场景中,我们创建了一个square
函数,并用 组合了它printFunction
。
这很酷吧?
好吧,我们可以组合函数,但我们需要一种更好的组合方式。比如pipe
RxJS 那样更全面的机制。所以,让我们来构建这个机制。
const pipe = (f, g) => x => g(f(x));
我们定义了一个名为 pipe 的新函数,它接受两个函数作为参数,并返回一个接受一个参数的函数,该函数返回由 f 和 g 组成的函数。
刚才发生了什么?
我们接受了两个函数作为参数。然后,我们将另一个值作为参数,并将第一个函数的f
值应用到它x
上面。然后,我们获取了它的返回值f(x)
,并应用了函数g
。
这可能有点令人困惑,如果你是,我强烈建议你阅读currying function
JavaScript 相关的内容。
现在,使用 pipe 函数,我们可以执行以下操作
o.subscribe(
pipe(
square,
printFunction,
)
)
o.emit(4);
// outputs
// I will print the 16
但这里有一个问题。我们希望能够传入任意数量的函数,然后能够将它们组合起来。因此,假设有 f,g,h,k ⇒ k(h(g(f)))。
所以我们将像这样修改管道
const pipe = (...funcs) => x => funcs.reduce((effects, f) => f(effects), x);
这是什么函数式魔法?首先,我们用展开运算符传入多个函数。(...funcs)
部分指定我们可以按顺序传入任意数量的函数。然后,我们传入一个值x
作为参数进行操作。funcs.reduce
它会遍历每个函数,返回更新后的值x
,并将其传递给序列中的下一个函数。可以将其视为一系列执行。执行结束时结果x
仍然相同,因为我们不会改变函数中的值。pure functions.
关于纯函数的更多信息请见我的下一篇文章,敬请期待😊
现在让我告诉你为什么我们这样做。我们来看看下面的代码
o.subscribe(
pipe(
square,
double,
square,
printFunction
)
);
o.emit(2);
// outputs
// I will print the 64
现在,您可以组合函数而不必过多关注它们的执行顺序,并且还可以保持数据不可变。然而,我们的实现缺少一点。我们无法在管道之间收集数据。我的意思是,我们无法在第二个管道应用
后中断并收集值。RxJs 有一个方法可以实现这一点。所以,让我们来实现一个方法。double
tap
tap
const tap = fun => x => {
fun(x);
return x;
};
对于此方法,我们接受一个函数和一个值,然后将该值应用于该函数并
返回原始值。这样,现在我们可以在管道流的特定位置获取并提取值。
o.subscribe(
pipe(
square,
double,
tap(printFunction),
square,
printFunction
)
);
o.emit(2);
// outputs
// I will print the 8
// I will print the 64
差不多就是这样了。技术上来说,我们已经拥有了像 RxJS 这样的响应式库的基本功能。*** 现在我想向你展示我们响应式库的实际实现***。
假设我们有一些异步数据传入(例如鼠标指针位置),并且我希望根据这些数据在应用程序中执行一些状态更改。那么我们将使用我们的响应式库来处理这个问题。
o.subscribe(pipe(
filter(x => {
if(x > 0) {
console.log('In Range')
return x;
}
console.log('Out of Range')
return 0
}),
square,
tap(printFunction),
));
o.emit(2);
o.emit(-4);
o.emit(8);
o.emit(4);
// outputs
// In Range
// I will print the 4
// Out of Range
// I will print the 0
// In Range
// I will print the 64
// In Range
// I will print the 16
所以我们可以funnel like data filtering
像 RxJS 一样用我们的库来实现这一点。希望本文能让你对 RxJS 的幕后运作方式有所了解。
在第二部分中,我们将进一步剖析 RxJS 的所有操作。敬请关注,如有任何反馈,欢迎留言。关注我,获取更多类似文章。
⚡️⚡️⚡️⚡️⚡️
到目前为止,您还享受这段旅程吗?前往第 2 部分 🕟 🕔 🕠 🕕。