为什么 RxJS 订阅允许省略箭头函数和以下方法参数?

Why does RxJS subscribe allow omitting the arrow function and the following method argument?

最近需要用到RxJS。我试图设计一个错误处理流程,但我发现了一些奇怪的语法传递方法参数:

.subscribe(
    x => {
    },
    console.warn // <- Why does this compile, and warn 'is not 7' in debug console?
);

Link 最小复制:

https://stackblitz.com/edit/rxjs-6-5-error-handle-no-arrow-issue

重现步骤:

  1. 使用 RxJS 6.5
  2. 创建一个函数 return observable
  3. 订阅 observable
  4. 将参数传递给订阅
  5. 就用,console.warn,不像,error => { console.warn(error); }

没有箭头函数,它仍然将错误传递给console.warn。为什么?

代码:

import { throwError, concat, of } from "rxjs";
import { map } from "rxjs/operators";

const result = concat(of(7), of(8));

getData(result).subscribe(
  x => {
    console.log("typeof(x)", typeof(x));
    if (typeof(x) === 'string') {
      console.log("x  Error", x);
      return;
    }
    console.log("no error", x);
  },
  console.warn // <- Why does this compile, and warn 'is not 7' in debug console?
);

// pretend service method
function getData(result) {
  return result.pipe(
    map(data => {
      if (data !== 7) {
        throw "is not 7";
      }
      return data;
    })
  );
}

我尝试google一些关键字,js,rxjs,angular,省略箭头函数,缺少参数,...但我找不到这里使用的技术。

谁能提供解释此机制的链接?

下面两个问题相关但不解释行为,只说“等价”:

The line

map(this.extractTreeData)

is the equivalent of

map(tree => this.extractTreeData(tree))

Why is argument missing in chained Map operator

首先,您需要了解实际传递给 .subscribe 函数的内容。本质上它接受三个可选参数 nexterrorcomplete。每一个都是一个回调,当相应的通知被源可观察对象发出时执行。

所以当你使用箭头函数时,你定义了一个就地回调函数。

sourceObservable.subscribe({
  next: (value) => { },
  error: (error) => { },
  complete: () => { }
});

相反,您可以单独定义函数并将其用作回调。

onNext(value) {
}

onError(error) {
}

onComplete() {
}

sourceObservable.subscribe({
  next: this.onNext,
  error: this.onError,
  complete: this.onComplete
});

现在这就是您所看到的。但是您传递的不是用户定义的函数,而是内置的 console.warn() 函数。反过来,通知中的值将作为参数传递给回调函数。因此,您的错误 is not 7 中的值作为参数发送给 console.warn() 然后执行它的工作(即打印到控制台)。

但是有一个问题。如果您希望在回调中使用 this 关键字引用任何 class 成员变量,它会抛出一个错误,指出该变量未定义。那是因为 this 指的是回调函数的作用域,而不是 class。克服这个问题的一种方法是使用箭头函数(我们已经看到了)。或者使用bind()函数将this关键字的含义绑定到class.

sourceObservable.subscribe({
  next: this.onNext.bind(this),
  error: this.onError.bind(this),
  complete: this.onComplete.bind(this)
});

因此,如果您希望仅包含错误回调,您可以明确声明并忽略其他回调。

sourceObservable.subscribe({ error: console.warn });

关于你的问题“为什么函数调用中没有括号”,已经讨论过here and here。参数需要对函数的引用,函数名称表示它们的引用。

您的订阅需要 2 个参数。第一个参数是一个将使用“下一个”值调用的函数,第二个参数也是一个函数,如果发生错误将调用该函数。因此,由于“console.warn”是一个函数,您可以将它用作第二个参数。

(error) => console.warn(error) 等同于 console.warn(error)

但是要小心,因为 console.warn 不依赖于上下文“this”,你不会给出任何问题。但是如果你想使用一个使用上下文“this”的函数,你将需要使用箭头函数。

有关 JS 作用域的更多信息:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

这是由于 Observable 能够发出的三种可能类型的值,

  1. 下一个
  2. 错误
  3. 完成

这些逻辑在订阅函数的参数中进行了翻译,因此第一个函数回调将触发通过 next 发出的值,第二个回调将触发错误发出的值,第三个函数将触发完整值。

在你的例子中,console.warn 作为函数传递给第二个函数,每次发出错误时都会调用该函数。

第二个问题可以参考箭头函数文档,https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions

console.log 是函数

可以用括号中的参数调用函数

console.log("123") 表示使用参数 "123"

调用函数 console.log

tree => console.log(tree)也是函数

它也可以用括号中的参数调用,例如。 (tree => console.log(tree))(tree)

因此以 回调作为其参数的函数 可以使用括号中的参数调用其回调

function example(callback) {
callback();
}

所以如果我们将 console.log 传递给它,example(console.log),它基本上运行为

function example(callback) {
console.log();
}

如果我们将 tree => console.log(tree) 传递给它,example(tree => console.log(tree)),它基本上运行为

function example(callback) {
(tree => console.log(tree))();
}

如果你理解上面的代码。很容易理解现在订阅。

function subscribe(nextCb, errorCb, completeCb) {
// ... got next data
nextCb(data);
//... got error
errorCb(error);
// completed observe
completeCb();
} 

所以你的错误回调 console.log 基本上被称为 console.log(error);

error=> console.log(error) 基本上被称为 (error=> console.log(error))(error);

在这种情况下结果相同。

在 JS 中 functions are first class objects。当您的代码 console.warn 没有括号时,您有一个对该对象的引用,但您没有调用该对象,这将需要大括号 console.warn()。例如你可以这样做:

let x = console.warn;
console.log('not happened yet');
x('test');

所以您的代码很简单,将 console.warn 函数传递给 Subscribe 失败的参数,其方式与传递任何其他函数的方式完全相同,例如

Subscribe(() => {}, () => {});

[为什么]显示警告'is not 7'

另一部分是您抛出错误 throw "is not 7";。因此,订阅错误调用的签名是:

subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): Subscription;

所以error的参数是any类型的。因此 throw 将 Error 传递给错误函数处理程序。这被设置为 console.warn,其签名为:

console.warn(obj1 [, obj2, ..., objN]);

console.warn 本质上是将它传递的任何参数转换为字符串,JS 不是强类型的,这本质上归结为 type coercion,并将其记录下来。 throw "is not 7"; 的字符串是 is not 7。所以它记录 is not 7.

总而言之,我想说的是这一切都有些神秘,可能难以理解。这里在技术上没有任何错误,但我认为执行以下操作更有意义:

.subscribe(
    x => {
    },
    x => {console.warn(x);} 
);

基于"Any fool can write code that a computer can understand. Good programmers write code that humans can understand."

的原则