我如何对 RxJs 进行逆向工程并学习反应式编程?

2025-06-07

我如何对 RxJs 进行逆向工程并学习反应式编程?

是的,标题没有打错。我们实际上要对 RxJs 进行逆向工程(接下来会有大量代码 ;) )。但在继续之前,让我先告诉你我为什么要开始这项疯狂的尝试。

作为程序员,我们天生好奇。我每天都在使用 RxJs 和 React.js 等响应式库。然而,在一个阳光明媚的早晨,我突然好奇这些框架是如何在底层利用响应式编程的。

我真懂响应式编程是什么吗?它究竟是怎么工作的?我问自己。

经过一个周末的钻研,我浏览了各种博客文章和书籍,大概明白了这个概念。然而,我觉得逆向工程才是真正搞清楚这些概念的好方法,所以我决定对 RxJs 进行逆向工程。

简介:

反应式编程是使用异步数据流进行的编程。

数据流可以是任何东西,可以是用户输入(例如鼠标位置、点击事件),也可以是来自服务器的数据流(例如 Twitter 消息,或者来自套接字服务器的实时数据)。我们的应用程序将根据这些数据流做出反应。

例如,当你实时接收 Twitter 信息时,你的应用程序状态会发生变化。你可能想把最受欢迎的推文放在最顶部。因此,你的应用程序会订阅传入的数据流,并根据数据做出反应,将最受欢迎的推文放在最顶部。简而言之,这种订阅数据流并相应地更改应用程序的概念就是响应式编程。

你是不是觉得无聊?相信我,这篇文章不会是那种充斥着各种概念的博文。我们现在就开始深入代码吧。

让我们构建一个名为的类,Observable因为它是 RxJs 最基本的构建块。

class Observable {
  constructor() {
    this.fnArray = [];
  }

  subscribe() {}

  emit() {}
}

const o = new Observable();
Enter fullscreen mode Exit fullscreen mode

好了,我们刚刚创建了一个名为 Observable 的基础类,它包含两个方法。我们初始化了一个名为 fnArray 的空列表。这个数组将保存我们所有订阅的对象。
我们subscribe先来实现这个方法。这个方法将接受一个函数作为参数,并将其推送到 fnArray 中。

subscribe(fn) {
    this.fnArray.push(fn);
}
Enter fullscreen mode Exit fullscreen mode

现在让我们emit也实现这个函数。emit 函数的作用是循环fnArray并逐个执行这些函数。

emit(v) {
  for (let fun of this.fnArray) {
    fun(v);
  }
}
Enter fullscreen mode Exit fullscreen mode

我们也可以用 map 代替 for 循环。但为什么呢?因为 JS 圈里的那些酷小子们现在显然在做这件事。而且 curry 函数也挺酷的!所以现在就来试试吧。

emit(v) {
-  for (let fun of this.fnArray) {
-    fun(v);
-  }
+  this.fnArray.map(fun => fun(v))
}
Enter fullscreen mode Exit fullscreen mode

好的,现在让我们使用我们新创建的类。

function printFunction(thing) {
 console.log(`I will print the ${thing}`)
}

const o = new Observable();
o.subscribe(printFunction);
Enter fullscreen mode Exit fullscreen mode

首先,我们创建了一个函数printFunction来打印传入的任何变量。我们初始化了一个新的可观察实例并subscribe在其上调用该方法并传入我们的printFunctionas 参数。

记住,printFunction将被存储在 中fnArray。现在,如果我们调用 emit 方法,你认为会发生什么?让我们尝试一下

o.emit("Apple");
o.emit("Orange");
o.emit("Pear");
Enter fullscreen mode Exit fullscreen mode

这给了我们以下输出

I will print the Apple
I will print the Orange
I will print the Pear
Enter fullscreen mode Exit fullscreen mode

好了,现在我们可以订阅一个函数或事件,并根据该函数发出一些内容了。目前为止,完整的代码如下。

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");
Enter fullscreen mode Exit fullscreen mode

现在让我们进入有趣的部分。我们可以订阅多个函数。例如,我们可以这样做

o.subscribe(x => console.log(x * 2));
o.subscribe(x => console.log(x + 2));

o.emit(4)
Enter fullscreen mode Exit fullscreen mode

返回

// 8
// 6
Enter fullscreen mode Exit fullscreen mode

因为我们的 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
Enter fullscreen mode Exit fullscreen mode

在第一个场景中,我们用 组合了我们的函数printFunction。在第二个场景中,我们创建了一个square函数,并用 组合了它printFunction

这很酷吧?
好吧,我们可以组合函数,但我们需要一种更好的组合方式。比如pipeRxJS 那样更全面的机制。所以,让我们来构建这个机制。

const pipe = (f, g) => x => g(f(x));
Enter fullscreen mode Exit fullscreen mode

我们定义了一个名为 pipe 的新函数,它接受两个函数作为参数,并返回一个接受一个参数的函数,该函数返回由 f 和 g 组成的函数。
刚才发生了什么?
我们接受了两个函数作为参数。然后,我们将另一个值作为参数,并将第一个函数的f值应用到它x上面。然后,我们获取了它的返回值f(x),并应用了函数g
这可能有点令人困惑,如果你是,我强烈建议你阅读currying functionJavaScript 相关的内容。
现在,使用 pipe 函数,我们可以执行以下操作

o.subscribe(
 pipe(
   square,
   printFunction,
 )
)
o.emit(4);

// outputs
// I will print the 16
Enter fullscreen mode Exit fullscreen mode

但这里有一个问题。我们希望能够传入任意数量的函数,然后能够将它们组合起来。因此,假设有 f,g,h,k ⇒ k(h(g(f)))。

所以我们将像这样修改管道

const pipe = (...funcs) => x => funcs.reduce((effects, f) => f(effects), x);
Enter fullscreen mode Exit fullscreen mode

这是什么函数式魔法?首先,我们用展开运算符传入多个函数。(...funcs)部分指定我们可以按顺序传入任意数量的函数。然后,我们传入一个值x作为参数进行操作。funcs.reduce它会遍历每个函数,返回更新后的值x,并将其传递给序列中的下一个函数。可以将其视为一系列执行。执行结束时结果x仍然相同,因为我们不会改变函数中的值。pure functions.

关于纯函数的更多信息请见我的下一篇文章,敬请期待😊

现在让我告诉你为什么我们这样做。我们来看看下面的代码

o.subscribe(
 pipe(
   square,
   double,
   square,
   printFunction
 )
);
o.emit(2);

// outputs
// I will print the 64
Enter fullscreen mode Exit fullscreen mode

现在,您可以组合函数而不必过多关注它们的执行顺序,并且还可以保持数据不可变。然而,我们的实现缺少一点。我们无法在管道之间收集数据。我的意思是,我们无法在第二个管道应用
后中断并收集值。RxJs 有一个方法可以实现这一点。所以,让我们来实现一个方法。doubletaptap

const tap = fun => x => {
 fun(x);
 return x;
};
Enter fullscreen mode Exit fullscreen mode

对于此方法,我们接受一个函数和一个值,然后将该值应用于该函数并
返回原始值。这样,现在我们可以在管道流的特定位置获取并提取值。

o.subscribe(
 pipe(
   square,
   double,
   tap(printFunction),
   square,
   printFunction
 )
);
o.emit(2);

// outputs
// I will print the 8
// I will print the 64
Enter fullscreen mode Exit fullscreen mode

差不多就是这样了。技术上来说,我们已经拥有了像 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
Enter fullscreen mode Exit fullscreen mode

所以我们可以funnel like data filtering像 RxJS 一样用我们的库来实现这一点。希望本文能让你对 RxJS 的幕后运作方式有所了解。

在第二部分中,我们将进一步剖析 RxJS 的所有操作。敬请关注,如有任何反馈,欢迎留言。关注我,获取更多类似文章。

⚡️⚡️⚡️⚡️⚡️

到目前为止,您还享受这段旅程吗?前往第 2 部分 🕟 🕔 🕠 🕕。

文章来源:https://dev.to/shadid12/how-i-reverse-engineered-rxjs-and-learned-reactive-programming-83k
PREV
如何从头开始构建 Node.Js 项目?
NEXT
通过创建模板设置你的 ReactJS + Tailwind CSS 项目🔥 创建一个 React 应用程序 设置 Tailwind CSS 最后一部分 - 创建模板仓库以供以后使用 创建模板或