【问题标题】:Observable concat method on array returns the same array数组上的可观察 concat 方法返回相同的数组
【发布时间】:2017-06-20 14:59:13
【问题描述】:

在我的服务中,我尝试解决下一个问题。我有一个包含其他 json 文件(卡片)名称的 json 文件:

{
  "filename1" : ... ,
  "filename2" : ... ,
  ...
  "filenameN" : ...
}

我需要加载所有这些文件。 “filenameX”文件有一些卡片数据:

{
  dataX
}

我需要在对象中合并加载的数据:

{
  "filename1" : { data1 },
  "filename2" : { data2 },
...
  "filenameN" : { dataN }
}

我为任何文件加载创建观察者,并尝试将它们组合成一个高级单个观察者,当所有相应的观察者都存在时,该观察者被解析。这是我的代码:

import { Observable } from "rxjs/Observable";
import "rxjs/add/observable/concat";
import "rxjs/add/operator/map";
...
    _loadCards(dir, cardsListFile) {
        var http = ...
        var url = ...
        return Observable.create(function (observer) {
            http.get(url + dir + "/" + cardsListFile + ".json").map(res => { return res.json(); }).subscribe(list => {
                let data = {};
                let observers = [];
                for(var card in list) {
                    if(list.hasOwnProperty(card)) {
                        let obs = http.get(url + dir + "/cards/" + card + ".json").map(res => { return res.json(); });
                        observers.push(obs);
                        let getData = function(card) {
                            obs.subscribe(cardData => {
                                data[card] = cardData;
                            });
                        };
                        getData(card);
                    }
                }

                let concatResult = Observable.concat(observers);
                console.log(concatResult);
                concatResult.subscribe(result => {
                    observer.onNext(data);
                    observer.onCompleted();
                });         
            });
        });
    };

但是,Observer 的 concat 运算符并不能像描述的那样工作 - 它返回相同的 Observers 数组作为其输入。问题出在哪里,我可以使用哪些其他运算符来使我的解决方案更直接(因为现在它肯定很难看)?

【问题讨论】:

    标签: angular typescript rxjs observable rxjs5


    【解决方案1】:

    我不完全确定我理解您想要做什么,但 concat() 运算符将其参数解包而不是作为数组。

    这意味着 Observable.concat(observables) 将重新发送 Observable,就像它们在 observables 数组中一样。

    您想要的是传递解压缩的数组Observable.concat(...observables),它与以下内容相同:

    Observable.concat(observable1, observable2, observable3 ,...)
    

    我认为这模拟了您正在尝试做的事情。它使用 forkJoin 而不是 concat 所以它会等到所有 Observables 完成。

    var input = {
      "filename1": 'data1',
      "filename2": 'data2',
      "filenameN": 'dataN'
    };
    
    let keys = Object.keys(input);
    
    let observables = keys.map(key => {
      return Observable.of(input[key]).map(s => s.split("").reverse().join(""));
    });
    
    Observable.forkJoin(observables, (...results) => {
        let combined = {};
        keys.forEach((key, i) => {
          combined[key] = results[i];
        });
        return combined;
      })
      .subscribe(val => console.log(val));
    

    这会打印到控制台:

    { filename1: '1atad', filename2: '2atad', filenameN: 'Natad' }
    

    观看现场演示:https://jsbin.com/coruyu/4/edit?js,console

    【讨论】:

    • 为什么要使用forEachfor 循环? RxJS 已经包含了循环操作符。当您使用 RxJS 语法而不是在 RxJS 和 JS 语法之间交替时,最终的代码看起来更清晰。此外,您不需要临时变量来存储可观察对象列表 (observables),您可以从原始可观察对象开始保持链接运算符,直到达到所需的结果。 (见我的回答。)
    • 因为这就是 forkJoin 中的结果选择器功能的用途。我正在使用 observables 数组,因为 OP 就是这样做的,我不知道他的用例到底是什么,以及他为什么这样做。如果您认为使用.map().mergeMap().mergeMap().reduce(),然后在mergeMap() 中使用另一个map() 更容易理解......那么我很抱歉任何重构此代码的人。
    • 对不起,我不是故意让你难过的。只是尝试建设性的对话,这样我们都可以学到一些东西。 :)
    • 感谢您清除如何使用 Observable.concat(...observables),我真的不知道这种语言构造。我同意像 map->mergemap->mergemap->reduce 这样的模式对于那些不是每天都使用函数式编程的人来说很难理解。例如,我花了大约半个小时来了解 AngularFrance 的解决方案,仍然需要清除一些时间。但是我也明白,像 ReactiveX 这样使用 FP 的库是针对此类代码的。
    【解决方案2】:

    带有解决方案的JSBIN:http://jsbin.com/dahaqif/edit?js,console

    我假设您有两个 observable,一个用于获取文件名列表,另一个用于获取特定文件名的数据:

    const filenamesObs = Observable.of({
      "filename1" : null,
      "filename2" : null,
      "filename3" : null
    });
    // This is actually a function that returns an observable.
    const filedataObs = (filename) => {
      return Observable.of(`This is the data for ${filename}`);
    }
    

    现在,您可以通过以下方式组合两个 observable 以获得您描述的数据结构:

    const source = filenamesObs
    
      // Extract the list of filenames as an array.
      .map(obj => Object.keys(obj))
    
      // Flatten the array, i.e. emit each filename individually
      // vs a SINGLE array containing ALL filenames.
      .mergeMap(val => val)
    
      // Get the data for each filename.
      .mergeMap(filename =>
        filedataObs(filename).map(data => Object.assign({}, { filename: filename, data: data }))
      )
    
      // At this point, you have a stream of { filename, data } objects.
      // Reduce everything back to a single object.
      .reduce((acc, curr) => {
        acc[curr.filename] = curr.data;
        return acc;
      }, {});
    

    如果您订阅并将结果记录到控制台,您将看到:

    [Object] {
      filename1: "This is the data for filename1",
      filename2: "This is the data for filename2",
      filename3: "This is the data for filename3"
    }
    

    Arseniy 发表评论后的补充说明

    • 第一个mergeMap() 是一个“技巧”,可以将我们一开始拥有的单个值数组转换为多个单独的值。由于您希望能够单独处理每个文件名(以获取相应的数据),因此更方便的是逐个接收文件名而不是作为包含所有文件名的大数组。为了清楚起见,您应该转到我的 JSBIN 并添加行 .do(console.log) beforeafter 第一个 mergeMap()。您会立即明白其中的区别。
    • 第二个mergeMap() 将源 observable 的值“投影”到目标 observable 中。这听起来很花哨,但这只是意味着我们正在将文件名(第一个 observable)转换为 HTTP 请求以获取文件的数据(第二个 observable)。
    • 最后,Object.assign() 让我们将所有东西重新组合在一起。由于我们有来自两个 observable 的数据——一个发出文件名,一个发出文件数据——我们需要将所有内容合并到一个容器中,然后才能使用它(或进一步转换它)。为此,我使用Object.assign() 创建具有两个属性filenamedata 的临时对象。请注意,这是 vanilla JavaScript,它与 observables 无关。同样,您可以在第二个 mergeMap() 之后添加 .do(console.log) 行,以将此时流包含的内容打印到控制台。

    【讨论】:

    • 谢谢你,特别是每一步都提供cmets。作为几个mergeMaps,我仍然需要了解一些要点以及为什么要使用Object.assign。我认为你的解决方案是最好的。我唯一需要添加的是手动指向类型:reduce 供编译器通过。
    • 不客气。我已经修改了我的答案以澄清我的代码,并希望能回答您刚刚提出的问题。
    • 至于mergeMap,我从您的第一个评论开始就明白为什么第一次使用它。问题是要了解这个方法的真正用途,通常如何使用它以及为什么它被称为“mergeMap”而不是“divideMap”,因为它的官方文档是地狱
    • 我现在理解了使用 Object.assign() 的逻辑,正如我所期望的,我们可以像往常一样创建临时对象,但它需要额外的大括号:“data => { return { filename: filename, data: data }}" 用什么只是选择问题
    • 感谢您的进一步澄清。我仍然需要很多时间来了解 mergeMap 的工作原理以及何时使用它。
    【解决方案3】:

    规则 #1:切勿在其他订阅中使用自定义订阅 - 可能有 0.1% 的情况确实需要这样做。

    我已经稍微改写了你的流,它是如何完成的:

    _loadCards(dir, cardsListFile) {
        var http = ...
        var url = ...
        return http.get(url + dir + "/" + cardsListFile + ".json")
            .map(res => res.json())
            .switchMap(fileMap => Rx.Observable.from(Object.keys(fileMap)))
            .concatMap(card => http.get(url + dir + "/cards/" + card + ".json")
                   .map(res => ({[card]: res.json()})))
            .map(arrayOfCardResultObjects => Object.assign.apply({}, arrayOfCardResultObjects));
    };
    

    调整后的版本,可以作为 sn-p 实时运行:

    getMockedCardMap()
        .switchMap(fileMap => Rx.Observable.from(Object.keys(fileMap)))
        .concatMap(card => getMockedCardFromFile(card).map(res => ({[card]: res})))
        .toArray()
        .map(arrayOfCardResultObjects => Object.assign.apply({}, arrayOfCardResultObjects))
        .subscribe(console.log);
    
    
    // Mocked helper-methods
    function getMockedCardMap() {
        return Rx.Observable.of({
            "file1": "foo",
            "file2": "bar",
            "file3": "baz"
        });
    }
    
    function getMockedCardFromFile(file) {
        return Rx.Observable.of("Result from: " + file);
    }
    <script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>

    【讨论】:

    • 就像我对@martin 说的,为什么要使用for 循环? RxJS 已经包含了循环操作符。您可以从原始 observable 开始链接运算符,直到达到所需的结果,从而避免临时的 cards$ 变量。
    • 好点 - 我误读了提供的示例,并认为文件路径在值中,而不是键中 - 我已经调整了代码
    • 感谢您的解决方案,我将对其进行测试,作为已接受的解决方案。据我了解异步和函数式编程,确实不应该在订阅和 for 循环中使用订阅。但是,由于我非常不擅长函数式编程,而且 Observers 对我来说是一个新概念,我无法自己创建这样的解决方案,请理解我的不好。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-09-09
    • 2018-12-16
    相关资源
    最近更新 更多