RxJS自定义运算符内部变量

时间:2018-08-25 18:56:35

标签: javascript functional-programming rxjs reactivex

在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()

2 个答案:

答案 0 :(得分:6)

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通知中收到不同的值。

例如,如果可观察的源是同步的,则连续的订阅将看到不同的值:

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>

答案 1 :(得分:2)

您已经说过,您失去了纯函数的一些优点。在这种特殊情况下,您可能会面临后期订阅者获得的数据流与预期不同的风险(取决于您在实际情况下与在此构造的情况下所进行的操作)。

例如,通过添加较晚的订阅者,流“ A”将看到0和1。流“ B”将仅看到“ 1”(由于“ obs”仍在“ A”订阅者中处于活动状态,因此跳过0。的行为类似于流“ 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)

这是可接受的还是预期的行为将取决于您的用例。尝试使用纯函数发挥其所有优点是不错的选择,但是有时对于您的用例而言,这是不切实际的或不合适的。