在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()
答案 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)
这是可接受的还是预期的行为将取决于您的用例。尝试使用纯函数发挥其所有优点是不错的选择,但是有时对于您的用例而言,这是不切实际的或不合适的。