在 useEffect 中反应取消订阅 RxJS 订阅

React unsubscribe from RxJS subscription in useEffect

一个关于 useEffect hooks 中 RxJS 的快速问题

我注意到 React 社区在 RxJS 中使用了这种取消订阅模式:

useEffect(()=>{
  const sub = interval(10).subscribe()
  return ()=>sub.unsubscribe()
})

我很好奇这是否是由于 convention/code 清晰,或者我是否忽略了什么。我想以下会更简单:

useEffect(()=> interval(10).subscribe().unsubscribe)

但是,我可能忽略了一些东西。

编辑:查看所选答案。 “This”绑定在方法调用上,而不是订阅实例化上。结果,取消订阅失败,因为“this”对象没有引用间隔订阅,而是引用了 useEffect 回调环境。感谢两位贡献者。这是 useEffect 挂钩失败的示例:codesandbox.io/s/morning-bush-7b3m6h?file=/src/App.js

这里是:

useEffect(()=>{
  const sub = interval(10).subscribe();
  return () => sub.unsubscribe();
});

可能是 re-written 作为:

useEffect(()=>{
  const sub = interval(10).subscribe();

  function unsub(){
    sub.unsubscribe();
  }

  return unsub;
});

要注意的关键是您正在 return 将函数返回到 React。 unsub 不会立即调用,稍后会在组件卸载时调用。

其实你可以return任意代码待运行以后:

useEffect(() => {
  /****** 
   * Code that gets called when 
   * effect is run
   ******/ 
  return () => { // <- start of cleanup function 
    /****** 
     * Code that gets called to 
     * clean up the effect later
     ******/
  } // <- end of cleanup function
});

问题

我将重写您的解决方案,以便更清楚地讨论问题。这在语义上是等价的,我只是引入了一个中间变量。

useEffect(() => 
  const sub = interval(10).subscribe();
  return sub.unsubscribe;
);

问题最清楚地归结为:这些值之间有什么区别?在什么情况下(如果有的话)一个会失败而另一个不会。

sub.unsubscribe
() => sub.unsubscribe()

如果 unsubscribe 是一个函数(未绑定到 class/object 的实例,因为它不包含 this 关键字),那么两者在语义上是等价的。

问题是取消订阅实际上不是一个函数。它是订阅对象上的一个方法。因此,上面的第一个值是未定义 this 的未绑定方法。该方法尝试使用其上下文 (this) 时,JavaScript 将引发错误。

要确保 unsubscribe 作为一种方法被调用,您可以这样做:

useEffect(() => {
  const sub = interval(10).subscribe();
  return sub.unsubscribe.bind(sub);
});

虽然看起来大致相同,但您通过这种方式减少了一层间接寻址。

此外,我建议在大多数情况下不要使用 bind。方法、函数、匿名 lambda 函数和包含这三个值中的任何一个的属性在各种边缘情况下的行为都不同。

据我所知,() => a.b() 可能会不必要地包装一个函数,但不会失败。再加上 JIT 可以很好地优化 99.9% 的情况。

其中 a.b.bind(a) 将在先前绑定的方法上失败,但 100% 的时间会被优化。我不会使用 bind 除非有必要(而且很少有必要)

更新:

顺便提一句:我在这里使用函数来表示不依赖上下文的可调用代码块(没有使用 this 关键字引用的对象)和表示依赖于某些上下文的可调用代码块的方法。

如果您更喜欢其他术语,那很好。边看边换字,我不会生气的,保证:)

提防this

简短的回答是 unsubscribe 函数使用 this 关键字,并被设计为作为订阅对象的 属性 调用(例如,作为 subscription.unsubscribe()) rather than "on its own" (e.g., as just 退订()`).

什么是 this

在 JavaScript 中(除了传递给它的参数和范围内的 closed-over 值)函数可以使用 this 关键字来访问特殊的上下文值。最初,this 意味着等于“接收者”,或者函数被调用的对象 from。例如,如果您使用任何函数 f 设置 a.f = fb.f = f,您可以调用 a.f() 并且 this 将等于 a 或调用 b.f() 并且 this 将等于 b。如果你单独调用 f()this 将是 undefined

这有什么关系?

在这种情况下,interval(10).subscribe() returns unsubscribe 当前源代码的 Subscription object with an unsubscribe function that expects this to be that same subscription object. Particularly, this line 检查 this.closed 以避免 re-closing 订阅已经关闭。

因此,当您 允许 interval(10).subscribe().unsubscribe 拉开并在没有接收者的情况下调用它时,您将收到类似“无法读取未定义的属性(阅读 'closed')".

我该怎么办?

不幸的是,当 this 用于不是您编写的函数时,并不总是很清楚。最好避免隔离来自对象(例如 const f = obj.func;)的函数,除非您确定它是 this-free。但是如果一个函数已经“独立”,你通常不必担心 this.

但是,有以下解决方法:

  1. 将其包装在一个新函数中,该函数始终使用预期的接收者调用它。这就是您的示例所做的:() => sub.unsubscribe().

  2. 创建函数的“绑定”副本。函数也是对象,每个函数都可以使用 bind 函数用于对 hard-code this 的值进行排序。如果你写const unsub = sub.unsubscribe.bind(this);,调用unsub将总是调用unsubscribe函数,sub作为this的值。这实际上与选项 1 非常相似,因为它创建了一个新函数,但这更符合其目的。

  3. 使用“箭头”功能。这不是消费者可以做到的,但 rxjs 作者可以这样写:unsubscribe = () => { ... }。然后你可以把这个函数拉出来,自己使用它,不会出错。这是因为“箭头”函数——使用 () => ... 语法创建的函数——有特殊的行为:它们总是从 创建 的任何地方保留 this。因此,当构造一个 new Subscription() 时,它会得到一个专用的 unsubscribe 函数,其 this 值将始终等于构造的订阅对象。但是由于 unsubscribe 函数当前声明时没有箭头语法,它使用典型的 this-equals-receiver 行为。此外,由于它直接在 Subscription class 的正文中声明(而不是分配给 属性 的值)它也是 Subscription [=88 的一部分=]prototype 而不是订阅对象本身的一部分。由于它在所有实例之间共享,this 不能引用任何特定实例,但必须查看接收者。