【问题标题】:RxJS Custom Operator Internal VariablesRxJS 自定义操作符内部变量
【发布时间】:2019-01-31 20:56:53
【问题描述】:

在 RxJS 中使用/改变自定义运算符闭包中的变量是否有缺点?我意识到它违反了“纯”功能原则,您可以在这个简单的示例中使用scan,但我特别要求以下基本模式的有形技术问题:

const custom = () => {

  let state = 0; 

  return pipe(
    map(next => state * next),
    tap(_ => state += 1),
    share()
  )
}

// Usage
const obs = interval(1000).pipe(custom())

obs.subscribe()

【问题讨论】:

  • 在什么情况下你需要它?在你的情况下,看起来只是做 next * (next + 1) 会工作
  • 我把这个例子做的很简单,只是为了演示这个模式。一个真正的用例是当您有需要在操作员链中的多个步骤中使用的内部状态时。
  • 扫描不行吗?
  • 不完全是,我几乎把它放在问题中,因为我知道它会出现。上面的“状态”是运营商内部的(从不发送给订阅者)。扫描将需要可观察的 afaik 发出数据。

标签: javascript functional-programming rxjs reactivex


【解决方案1】:

您在custom 运算符中存储状态的方式至少存在两个问题。

第一个问题是您这样做意味着操作员不再具有引用透明性。即如果将操作符的调用替换为操作符的返回值,则行为不同:

const { pipe, range } = rxjs;
const { map, share, tap } = rxjs.operators;

const custom = () => {
  let state = 0; 
  return pipe(
    map(next => state * next),
    tap(_ => state += 1),
    share()
  );
};

const op = custom();
console.log("first use:");
range(1, 2).pipe(op).subscribe(n => console.log(n));
console.log("second use:");
range(1, 2).pipe(op).subscribe(n => console.log(n));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs@6/bundles/rxjs.umd.min.js"></script>

第二个问题 - 正如另一个答案中提到的 - 是不同的订阅将在其next 通知中收到不同的值,因为运营商内的状态是共享的。

例如,如果源 observable 是同步的,连续订阅将看到不同的值:

const { pipe, range } = rxjs;
const { map, share, tap } = rxjs.operators;

const custom = () => {
  let state = 0; 
  return pipe(
    map(next => state * next),
    tap(_ => state += 1),
    share()
  );
};

const source = range(1, 2).pipe(custom());
console.log("first subscription:");
source.subscribe(n  => console.log(n));
console.log("second subscription:");
source.subscribe(n => console.log(n));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs@6/bundles/rxjs.umd.min.js"></script>

但是,可以编写一个与您的custom 运算符非常相似的运算符,并让它在所有情况下都正确运行。为此,有必要确保运营商中的任何状态都是按订阅

可管道操作符只是一个接受可观察对象并返回可观察对象的函数,因此您可以使用defer 来确保您的状态是按订阅进行的,如下所示:

const { defer, pipe, range } = rxjs;
const { map, share, tap } = rxjs.operators;

const custom = () => {
  return source => defer(() => {
    let state = 0; 
    return source.pipe(
      map(next => state * next),
      tap(_ => state += 1)
    );
  }).pipe(share());
};

const op = custom();
console.log("first use:");
range(1, 2).pipe(op).subscribe(n => console.log(n));
console.log("second use:");
range(1, 2).pipe(op).subscribe(n => console.log(n));

const source = range(1, 2).pipe(op);
console.log("first subscription:");
source.subscribe(n => console.log(n));
console.log("second subscription:");
source.subscribe(n => console.log(n));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs@6/bundles/rxjs.umd.min.js"></script>

【讨论】:

  • 太棒了。我祈祷你会回答 :) 在尝试了更多之后,我开始意识到同步 Observables 的问题,但是延迟解释非常有帮助。谢谢!
  • 不用担心。你的问题很及时;碰巧我已经写完一篇关于这个主题的短文了。我很可能会在下周发布。
【解决方案2】:

正如你已经说过的,你失去了纯函数的一些优点。在这种特殊情况下,您可能会面临延迟订阅者获得与您预期不同的数据流的风险(取决于您在真实案例中所做的事情与在此构建的案例中所做的事情)。

例如,通过添加延迟订阅者,流“A”会看到 0 和 1。流“B”只会看到“1”(它会跳过 0,因为 obs 仍然从“A”订阅者处处于活动状态。流“C” ' 的行为类似于流 'A'。

const { interval, pipe, subscribe } = Rx;
const { take, map, tap, share  } = RxOperators;

const custom = () => {
  let state = 0; 
  return pipe(
    map(next => state * next),
    tap(_ => state += 1),
    share()
  )
}

// Late subscribers can get different streams
const obs = interval(500).pipe(custom())
const sub1 = obs.pipe(take(2)).subscribe((x) => console.log('A', x))
setTimeout(() => obs.pipe(take(1)).subscribe((x) => console.log('B', x)), 500)
setTimeout(() => obs.pipe(take(3)).subscribe((x) => console.log('C', x)), 3000)

这是可接受的还是预期的行为将取决于您的用例。虽然尝试使用纯函数以发挥其所有优势是件好事,但有时它并不实用或不适合您的用例。

【讨论】:

  • 谢谢,这促使我尝试更多同步和异步示例,这澄清了您指出的一些问题:)
猜你喜欢
  • 1970-01-01
  • 2019-09-07
  • 2023-02-22
  • 1970-01-01
  • 1970-01-01
  • 2013-03-01
  • 2022-01-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多