RXJS中scan仅返回单个元素,期望返回多个的问题排查
嘿,我来帮你理清楚这个问题!你遇到的核心问题其实是RxJS里switchMap操作符的特性在“搞鬼”,导致你最终只拿到了包含最后一个元素的数组。
为什么会这样?
Observable.range(0, 10)会快速连续吐出10个值,但你的put方法是异步的IndexedDB请求,需要一点时间才能完成。而switchMap的行为是:每当源Observable发射新值时,它会立刻取消之前还在运行的Observable订阅,转而订阅新的那个。
也就是说,当range吐出第2个值时,第1个put请求可能还没跑完,switchMap就直接把它的订阅取消了——这意味着第1个请求的结果永远不会传到后面的scan里。以此类推,前面9个put请求全被中途取消了,只有最后一个请求顺利完成,结果被scan收集到,所以数组里自然只有1个元素。
看你的日志也能验证这点
从你贴的日志片段里能明显看到:
2 Jean Gibbs 3 Jean Gibbs put Jean Gibbs put.finished Jean Gibbs 2 Chris Garrett 3 Chris Garrett put Chris Garrett put.finished Chris Garrett 2 Todd Moss 3 Todd Moss put Todd Moss put.finished Todd Moss val Todd Moss acc.count 0 4 1 5 Todd Moss length 1
前面的Jean Gibbs和Chris Garrett的put虽然执行完了,但它们的结果根本没进入scan环节,只有最后一个Todd Moss的结果被scan接收到了。
怎么解决?
根据你的需求(要收集所有put完成后的结果到数组),把switchMap换成**concatMap**就好:
concatMap的特性是必须等前一个Observable完全完成,才会订阅下一个,这样所有put请求会按顺序依次执行,每个请求的结果都会被传递到scan里,最终就能得到包含10个元素的数组了。
修改后的核心代码片段:
return Observable.range(0, numberOfOwners) .do(counter => console.log('1 ' + counter)) // 这里你原来的do没打日志,建议加上方便调试 .map(counter => NameService.GeneratePersonName()) .do(val => console.log('2 ' + val)) .map(personName => OwnerModel.GenerateModel(personName, NumberService.GenerateRandomNumber(60), isUser, game)) .do(owner => console.log('3 ' + (owner as OwnerModel).name)) .concatMap(owner => this.put(owner)) // 把switchMap换成concatMap .scan((acc, val) => { console.log('val ' + val.name); console.log('acc.count ' + acc.length); acc.push(val); return acc; } , []) .do(owners => console.log('4 ' + owners.length)) .do(owners => console.log('5 ' + owners[0].name));
另外还有个可选方案
如果你不需要按顺序执行请求,想让所有put并行跑,最后一次性拿到所有结果,可以用mergeMap配合toArray:
return Observable.range(0, numberOfOwners) .map(counter => NameService.GeneratePersonName()) .map(personName => OwnerModel.GenerateModel(personName, NumberService.GenerateRandomNumber(60), isUser, game)) .mergeMap(owner => this.put(owner)) // 并行执行所有put请求 .toArray(); // 等所有请求都完成后,把结果打包成数组
不过toArray()要等源Observable完全结束才会吐出数组,而scan是每收到一个结果就吐出一次更新后的数组,你可以根据自己的调试需求或者业务场景来选。
最后提个小细节
你原来的.do(counter => '1 ' + counter)其实没输出任何日志,应该改成.do(counter => console.log('1 ' + counter)),这样才能看到每个counter的调试信息哦。
内容的提问来源于stack exchange,提问作者Simon Houlton

