RxJS中的管道是用来做什么的?

182

我认为我掌握了基本概念,但还存在一些模糊点。

总的来说,这是我如何使用 Observable

observable.subscribe(x => {

})

如果我想筛选数据,可以使用这个:

import { first, last, map, reduce, find, skipWhile } from 'rxjs/operators';
observable.pipe(
    map(x => {return x}),
    first()
    ).subscribe(x => {

})

我也可以这样做:

import 'rxjs/add/operator/map';
import 'rxjs/add/operator/first';

observable.map(x => {return x}).first().subscribe(x => {

})

所以我的问题是:

  1. 它们有何不同之处?
  2. 如果没有区别,为什么要存在pipe函数?
  3. 为什么这些函数需要不同的导入?

1
我本来想说这是为自定义的非本地操作符而设计的,但我甚至不知道这是否正确。pipe()函数能让你传递自己创建的操作符吗? - zero298
5个回答

106

"Pipeable"(之前称为“lettable”)操作符是自 RxJS 5.5 以来当前和推荐使用的方式

我强烈建议您阅读官方文档中关于可管道化操作符的部分

主要区别在于它更容易制作自定义操作符,并且更好地支持树摇晃,而不会修改某些全局的Observable对象。如果两个不同的方面想要创建同名的操作符,则可能会发生冲突。

对每个操作符使用单独的import语句'rxjs/add/operator/first'是减小应用程序包的一种方法。通过仅导入所需的操作符而不是整个RxJS库,可以显着减少总包大小。然而编译器无法知道您是否导入'rxjs/add/operator/first'因为你真的需要它在你的代码中,还是只是在重构代码时忘记了将其删除。这是使用可管道化操作符的优势之一,在其中未使用的导入会自动被忽略。


2
关于您的肯定性语句“未使用的导入将自动被忽略”,目前的集成开发环境(IDE)已经有插件可以删除未使用的导入。 - silvanasono
2
并不是所有人都在使用这些集成开发环境(IDE)或插件,很多人使用基本文本编辑器。可能大部分时间我们不能仅依赖于这样的说法,即团队中每个人都和我们一样使用相同的IDE / 插件集/文本编辑器。 - Navidot
5
@AdamFaryna 当然,有些团队可能会在纸上写代码,但如果他们有现代化的工具可用,为什么要这样做呢?使用文本编辑器,特别是没有重要插件的情况下,就像在纸上写代码一样。虽然你可以这样做,但任何优秀的团队/开发者都不会这样做。 - Denes Papp
2
@DenesPapp 代码编辑器并不重要,只要人们能以有效的方式使用它即可。除此之外,这只是个人偏好。关于在纸上编写代码的比喻是不准确的,你无法在纸上执行代码,但任何文本编辑器中编写的代码都可以执行。 - Navidot
2
@perymimon,你可以这样做,但是你必须安装 rxjs-compat 包。https://github.com/ReactiveX/rxjs/blob/master/docs_app/content/guide/v6/migration.md#rxjs-v5x-to-v6-update-guide - martin
显示剩余5条评论

26

管道方法

根据原始文档

可管道的操作符是一个接收 observable 作为输入并返回另一个 observable 的函数。前一个 observable 保持不变。

pipe(...fns: UnaryFunction<any, any>[]): UnaryFunction<any, any>

原文链接

管道是什么意思?

这意味着你之前在可观察对象实例上使用的任何操作符都可以作为纯函数在 rxjs/operators 中使用。这使得构建操作符的组合或重用变得非常容易,而无需诸如创建扩展 Observable 的自定义可观察对象、然后覆盖 lift 等各种编程技巧来创建自定义的东西。

const { Observable } = require('rxjs/Rx')
const { filter, map, reduce,  } = require('rxjs/operators')
const { pipe } = require('rxjs/Rx')

const filterOutWithEvens = filter(x => x % 2)
const doubleByValue = x => map(value => value * x);
const sumValue = reduce((acc, next) => acc + next, 0);
const source$ = Observable.range(0, 10)

source$.pipe(
  filterOutWithEvens, 
  doubleByValue(2), 
  sumValue)
  .subscribe(console.log); // 50

@VladKuts 更改代码和给定属性。对不起给您带来的不便。 - Chanaka Weerasinghe
1
谢谢,我甚至没有意识到可以将可管道操作符存储为函数引用,并在pipe()调用中使用它们。这比始终内联执行要清晰得多。 - Alex. A

22

什么是不同之处? 从您的示例中可以看出,主要区别在于提高源代码的可读性。您的示例中只有两个函数,但是想象一下如果有十几个函数呢?那么它将变成

function1().function2().function3().function4()

这真的很丑陋,阅读起来也很困难,特别是当您填写函数内部时。此外,某些编辑器(如Visual Studio code)不允许超过140行长度。但如果像以下方式那样处理,则:

Observable.pipe(
function1(),
function2(),
function3(),
function4()
)

这显著提高了可读性。

如果没有区别,为什么需要pipe()函数?PIPE()函数的目的是将所有接受和返回observable的函数合并在一起。它最初接受一个observable,然后在其内部使用每个函数使用的observable来执行pipe()函数。第一个函数接收observable,处理它、修改它的值,并将其传递给下一个函数,接着下一个函数接收第一个函数的输出observable,处理它,并传递到下一个函数,以此类推,直到pipe()函数中的所有函数都使用该observable,最终你会得到已处理的observable。最后,你可以使用subscribe()函数执行observable,从中提取出值。请记住,原始observable中的值不会被更改!!

为什么这些函数需要不同的导入?导入取决于函数在rxjs包中的指定位置。它是这样的:所有模块都存储在Angular的node_modules文件夹中。import { class } from "module";

让我们以以下代码为例。我刚刚在stackblitz中编写了它,所以没有自动生成任何内容,也没有从其他地方复制任何内容。当您可以直接阅读文档时,我认为将文档中陈述的内容复制过来没有意义。我假设您在这里提出此问题,是因为您没有理解文档。

  • 从各自的模块中导入pipe、observable、of、map类。
  • 在类的正文中,我使用了Pipe()函数,如代码所示。
  • Of()函数返回一个observable,在订阅时按顺序发出数字。

  • Observable还未被订阅。

  • 当你使用Observable.pipe()时,pipe()函数将给定的Observable作为输入使用。

  • 第一个函数map()使用该Observable进行处理,返回经过处理的Observable回到pipe()函数中,

  • 然后,如果有下一个函数,它会将已处理的Observable传递给下一个函数,并且一直这样进行,直到所有函数都处理了Observable,

  • 最后,pipe()函数将已处理的Observable返回到变量中,在以下示例中为obs。

现在Observable中的事情是,只要观察者没有订阅它,它就不会发出任何值。因此,我使用subscribe()函数订阅此Observable,一旦我订阅了它,of()函数就开始发出值,然后它们通过pipe()函数进行处理,最终你可以得到最终结果。例如,1从of()函数中获取,1在map()函数中加上1并返回。你可以将该值作为参数放入subscribe(function (argument) {})函数中获取。如果要打印它,请使用以下方式:

subscribe( function (argument) {
    console.log(argument)
   } 
)
    import { Component, OnInit } from '@angular/core';
    import { pipe } from 'rxjs';
    import { Observable, of } from 'rxjs';
    import { map } from 'rxjs/operators';
    
    @Component({
      selector: 'my-app',
      templateUrl: './app.component.html',
      styleUrls: [ './app.component.css' ]
    })
    export class AppComponent implements OnInit  {
    
      obs = of(1,2,3).pipe(
      map(x => x + 1),
      ); 
    
      constructor() { }
    
      ngOnInit(){  
        this.obs.subscribe(value => console.log(value))
      }
    }

https://stackblitz.com/edit/angular-ivy-plifkg


12
不知道。我喜欢第一种方法多一些。对我来说,它看起来更清晰、更合乎逻辑。 - monstro
第一种方法? - Don Dilanga
9
我是一样的。fn().fn().fn() 看起来不错。管道有很多好处,但我觉得可读性并没有真正提高。我更喜欢的一个论点是点表示法是用于对象属性和函数的,而在这种情况下是人为的。从数学上讲,函数应该是 fn4(fn3(fn2(fn1()))),这才真的丑陋。 - Chaos Crafter
你也可以说,美在观者的眼中。 - Don Dilanga
2
@DonDilanga 可管道化的运算符是一种函数,它以可观察对象作为输入,并返回另一个可观察对象。原始的可观察对象保持不变。这是管道运算符的一个重要点。 - Manoj
@ChaosCrafter 如果这能让你感觉更好,你可以收集和组合以下内容:1)实现:const compose = (fns) => fns.reduce((fn, composition) => (...args) => composition(fn(...args))),2)使用!const composition = compose([fn4, ... , fn1])。如果你想要反向组合,还有额外的奖励:const pipe = compose(fns.reverse()) - Nick Bull

15
我想到的一个好的概括是:
它将流操作(map、filter、reduce等)与核心功能(订阅、管道)解耦。通过使用管道操作而不是链式调用,它不会污染Observable的原型,使得树摇更容易实现。
请参见https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md#why 引入补丁操作符的问题是:
导入补丁操作符的任何库都将增强所有消费该库的Observable.prototype,从而创建盲目依赖。如果该库删除了它们的使用,他们会无意中破坏其他所有人。使用可管道操作符,您必须在每个使用它们的文件中导入所需的操作符。
直接打补丁到原型上的操作符无法被类似rollup或webpack的工具进行“树摇”。管道操作符可以,因为它们只是从模块中直接提取的函数。
应用程序中导入的未使用操作符不能被任何构建工具或lint规则可靠地检测到。这意味着您可能会导入scan,但停止使用它,它仍然会添加到您的输出包中。使用可管道操作符,如果您不使用它,lint规则可以为您捕获它。
函数组合很棒。构建自己的自定义操作符变得更加容易,现在它们的工作方式和外观与rxjs中的所有其他操作符相同。您不再需要扩展Observable或覆盖lift。

7
这是我解释observable的方式:
你需要根据天气条件制定计划,所以打开收音机并收听全天候播报天气状况的天气频道。在这种情况下,不是只得到一个单一响应,而是持续性的响应。这种响应类似于对observable的订阅。observable就像是"天气",订阅就是"保持更新的收音机信号"。只要你的收音机开着,你就能接收到所有的更新信息。直到你关闭收音机,你才会错过任何信息。
我说了天气是observable,但你收听的是收音机而不是天气。因此,收音机也是observable。天气播报员说的话是由气象学家发送给他的天气报告的函数。气象学家所写的是来自气象站的数据的函数。气象站中的数据来源于其附加的所有仪器(气压计、风向标、风速计)的函数,这些仪器又是天气本身的函数。
整个过程中至少有5个observables。在这个过程中,有两种类型的observables:源observable和输出observable。在这个例子中,天气是"源observable",收音机是"输出observable"。两者之间的所有内容都代表PIPE FUNCTION。
管道函数是将源observables进行操作以提供输出observable的函数,所有这些操作都在内部发生。这些操作都处理observables本身。

1
好像是个不错的故事,但是我感觉它对我来说有点复杂。我相信它会帮助其他人。 - Gel
这并没有解释为什么要使用管道函数而不是直接在Observable对象上调用mapfirst等构造可观察对象的方法。其他Rx的实现,例如RxJava,不需要管道函数。 - Max

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接