简体   繁体   English

RxJS share vs shareReplay 差异

[英]RxJS share vs shareReplay differences

There seems to be an odd discrepancy with how share and shareReplay (with refcount:true) unsubscribe. share 和 shareReplay(使用 refcount:true)取消订阅的方式似乎存在奇怪的差异。

Consider the following (can paste into rxviz.com):考虑以下内容(可以粘贴到 rxviz.com):

const { interval } = Rx;
const { take,  shareReplay, share, timeoutWith, startWith , finalize} = RxOperators;

const shareReplay$ = interval(2000).pipe(
  finalize(() => console.log('[finalize] Called shareReplay$')), 
  take(1), 
  shareReplay({refcount:true, bufferSize: 0}));

shareReplay$.pipe(
  timeoutWith(1000, shareReplay$.pipe(startWith('X'))), 
)



const share$ = interval(2000).pipe(
  finalize(() => console.log('[finalize] Called on share$')),
  take(1), 
  share());

share$.pipe(
  timeoutWith(1000, share$.pipe(startWith('X'))), 
)

The output from streamReplay$ will be -X-0- while the output from shareStream$ will be -X--0.来自 streamReplay$ 的 output 将为 -X-0-,而来自 shareStream$ 的 output 将为 -X--0。 It appears as though share unsubscribes from the source before timeoutWith can re-subscribe, while shareReplay manages to keep the shared subscription around for long enough for it to be re-used.似乎 share 在 timeoutWith 可以重新订阅之前从源取消订阅,而 shareReplay 设法将共享订阅保持足够长的时间以便重新使用。

I want to use this to add a local timeout on an RPC (while keeping the call open), and any re-subscribe would be disastrous, so want to avoid the risk one of these behaviors is a mistake and gets changed in the future.我想用它在 RPC 上添加一个本地超时(同时保持调用打开),任何重新订阅都将是灾难性的,所以我想避免这些行为中的一个是错误的并且在未来被改变的风险。

I could use race(), and merge the rpc call with a delayed startsWith (so they both subscribe at the same time) but it would be more code and operators.我可以使用 race(),并将 rpc 调用与延迟的 startsWith 合并(因此它们同时订阅),但它会包含更多代码和运算符。

EDIT: One possible solution is to merge two subscriptions one to a shared request and one to a delayed stream that takes until the shared stream emits:编辑:一种可能的解决方案是将两个订阅合并到一个共享请求,一个合并到延迟的 stream,直到共享 stream 发出:

merge(share$, of('still working...').pipe( delay(1000), takeUntil(share$)));

This way the shated stream is subscribed to at the same time, so there is no "grey area" when one operator unsubscribes as a child subscribes.这样 shated stream 是同时订阅的,所以当一个操作员取消订阅时没有“灰色区域”作为孩子订阅。 (Will turn this into an answer unless someone comes up with a better suggestion) or can explain the intentions/differences between share and shareReplay (除非有人提出更好的建议,否则会将其转化为答案)或者可以解释 share 和 shareReplay 之间的意图/差异

Timing Guarantees时间保证

Javascript's runtime doesn't come with baked in timing guarantees of any sort. Javascript 的运行时不附带任何类型的时间保证。 In a single threaded environment, that makes sense (and you can start to do a bit better with web workers and such).在单线程环境中,这是有道理的(并且您可以开始使用 web workers 等做得更好)。 If something compute-heavy happens, everything covered in that timing window just waits.如果发生了计算量大的事情,时间 window 中涵盖的所有内容都会等待。 Mostly it happens in the expected order though.不过,大多数情况下它会以预期的顺序发生。

Regardless,不管,

const hello = (name: string) => () => console.log(`Hello ${name}`);

setTimeout(hello("first"), 2000);
setTimeout(hello("second"), 2000);

In Node, V8, or SpiderMonkey you do preserve the order here.在 Node、V8 或 SpiderMonkey 中,您确实会保留此处的顺序。 But what about this?但是这个呢?

const hello = (name: string) => () => console.log(`Hello ${name}`);

setTimeout(hello("first"), 1);
setTimeout(hello("second"), 0);

Here you would assume second always comes first because it's supposed to happen a millisecond earlier.在这里你会假设第二总是第一,因为它应该早一毫秒发生。 If you run this using SpiderMonkey, the order depends on how busy the event loop is.如果您使用 SpiderMonkey 运行它,则顺序取决于事件循环的繁忙程度。 They bucket shorter timeouts since a 0ms timeout takes about 8ms on average anyway.他们使用更短的超时,因为 0 毫秒的超时平均需要大约 8 毫秒。

Always make asynchronous dependencies explicit始终使异步依赖项显式

In JavaScript, it's best practice to never ever make any timing dependencies implicit.在 JavaScript 中,最好的做法是永远不要隐含任何时序依赖性。

In the code below, we can reasonably know that that data will not be undefined when we call data.value .在下面的代码中,我们可以合理地知道当我们调用data.value时, data不会是未定义的。 This implicitly relies on asynchronous interleaving:这隐含地依赖于异步交错:

let data;
setTimeout(() => {data = {value: 5};}, 1000);
setTimeout(() => console.log(data.value), 2000);

We really should make this dependency explicit.我们真的应该明确说明这种依赖关系。 Either by checking if data is undefined or by restructuring our calls通过检查数据是否未定义或通过重构我们的调用

setTimeout(() => {
  const data = {value: 5};

  setTimeout(() => console.log(data.value), 1000);
}, 1000);

Share vs ShareReplay分享与分享重播

want to avoid the risk one of these behaviors is a mistake and gets changed in the future.想要避免其中一种行为是错误的并在未来得到改变的风险。

The real risk here isn't even in how the library implements the difference.这里真正的风险甚至不在于图书馆如何实现差异。 It's a language-level risk as well.这也是一种语言级别的风险。 You're depending on asynchronous interleaving either way.无论哪种方式,您都依赖于异步交错。 You run the risk of having a bug that only appears once in a blue moon and can't be easily re-created/tested etc.您冒着出现一个只在蓝色月亮中出现一次并且无法轻易重新创建/测试等的错误的风险。


The share operator has a ShareConfig ( source )共享运营商有一个ShareConfig来源

export interface ShareConfig<T> {
  connector?: () => SubjectLike<T>;
  resetOnError?: boolean | ((error: any) => Observable<any>);
  resetOnComplete?: boolean | (() => Observable<any>);
  resetOnRefCountZero?: boolean | (() => Observable<any>);
}

If you use vanilla shareReplay(1) or replay({resetOnRefCountZero: false}) then you aren't relying on how events are ordered in the JS Event Loop.如果你使用 vanilla shareReplay(1)replay({resetOnRefCountZero: false})那么你不依赖于事件在 JS 事件循环中的排序方式。

Quick Aside:快速旁白:

I suspect you're not going to get many answers until you describe the behavior you're after.我怀疑在描述您所追求的行为之前,您不会得到很多答案。

use this to add a local timeout on an RPC (while keeping the call open)使用它在 RPC 上添加本地超时(同时保持调用打开)

I'm not sure what keeping an RPC call opens entails.我不确定保持 RPC 调用打开需要什么。 It's sounds like you want something that cancels the request within RxJS but doesn't cancel the inflight RPC.听起来您想要取消 RxJS 内的请求但不取消飞行中的 RPC 的东西。 I feel like that's a domain mismatch.我觉得那是域不匹配。 Why not keep your observables aligned with the semantics of the calls they represent?为什么不让您的可观察对象与它们所代表的调用的语义保持一致?


Toward a solution寻求解决方案

Having seen your update, I suspect you don't need a timeout.看过你的更新后,我怀疑你不需要超时。 It looks like the behavior you're after can be achieved without unsubscribing to the observable representing your RPC.看起来您可以在不取消订阅代表您的 RPC 的可观察对象的情况下实现您所追求的行为。 I'd argue that doing so simplifies your logic and makes it easier to maintain/extend in the future.我认为这样做可以简化您的逻辑,并使将来更容易维护/扩展。

It also sidesteps all the async interleaving concerns you had earlier.它还回避了您之前遇到的所有异步交错问题。

Here I'll take an educated guess at the behaviour you're after.在这里,我将对您所追求的行为进行有根据的猜测。 It appears like you want to following:看起来你想跟随:

  1. Take one value from the source (happens to be an RPC in this case).从源中获取一个值(在本例中恰好是一个 RPC)。
  2. If that value takes more than 1s to arrive, emit `"still working..." and continue to wait for one value from the source.如果该值需要超过 1 秒才能到达,则发出“仍在工作...”并继续等待来自源的一个值。

If that's the case, you don't need share at all.如果是这样的话,你根本不需要share Normally a timeout means to cancel an inflight request if it takes too long.通常,超时意味着如果花费的时间太长则取消飞行中的请求。 Maybe you don't want a timeout at all, you want to stay subscribed to the source...也许你根本不想超时,你想继续订阅源...

If that's the case, here's a way to do this:如果是这样的话,这里有一种方法可以做到这一点:

const source$ = rpc(arg1 arg2);
// create a unique token (its memory address).
// We'll embed a message inside so it's doing double duty
const token = {a: "still working..."};

merge(
  source$,
  timer(1000).pipe(mapTo(token))
).pipe(
  // take(1), but ignoring the token
  takeWhile(v => v === token, true),
  // Unwrap the token, emit the contained message
  map(v => v === token ? v.a : v)
);

On the other hand, if you know that observable wrapping your RPC will never emit "still working..." then you don't need a unique token and you can simplify this to check the value directly.另一方面,如果您知道observable 包装您的 RPC 永远不会发出“仍在工作......”,那么您不需要唯一的令牌,您可以简化它以直接检查值。

merge(
  rpc(arg1 arg2),
  timer(1000).pipe(mapTo("still working..."))
).pipe(
  takeWhile(v => v === "still working...", true)
);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM