Multiple observables working together

情到浓时终转凉″ 提交于 2019-12-07 08:20:27

问题


I have an input that as the user types it performs a real-time search. For example, let's say he has searched for the following:

car

The result would be:

[
  {
    id: "1"
    name: "Ferrari"
  },
  {
    id: "2"
    name: "Porsche"
  }
]

This I was able to do successfully, here is how:

class WordComponent {
  word: Subject<string> = new Subject<string>();
  result: any[] = [];

  constructor(private http: Http) {
    this.subscribe();
  }

  subscribe(): void {
    this.word.debounceTime(400)
      .distinctUntilChanged()
      .switchMap((word: string): Observable<any[]> => this.http.get(word))
      .subscribe((result: any[]): any[] => this.result = result);
  }

  search(event: any): void {
    this.word.next(event.target.value);
  }

}

And the view:

<input type="text" placeholder="Word" (keyup)="search($event)">

I want the user to be able to type multiple words at the same time and perform a real-time search for each word separately. For example, let's say he has searched for the following:

car food sun

The result for car would be:

[
  {
    id: "1"
    name: "Ferrari"
  },
  {
    id: "2"
    name: "Porsche"
  }
]

The result for food would be:

[
  {
    id: "3"
    name: "egg"
  },
  {
    id: "4"
    name: "cheese"
  }
]

The result for sun would be:

[
  {
    id: "5"
    name: "star"
  },
  {
    id: "6"
    name: "sky"
  }
]

And also merge the results of each word, in this case it would look like this:

[
  [{
      id: "1"
      name: "Ferrari"
    },
    {
      id: "2"
      name: "Porsche"
    }
  ],
  [{
      id: "3"
      name: "egg"
    },
    {
      id: "4"
      name: "cheese"
    }
  ],
  [{
      id: "5"
      name: "star"
    },
    {
      id: "6"
      name: "sky"
    }
  ]
]

But let's say that the user, after typing all the words and performing the search, wishes to change one of them. Only the search for the word that was changed needs to be redone, and the merge of the final result would also have to be redone.

I still do not know all the features of rxjs and I do not know what would be the ideal way to achieve this. If you want a reference, the Display Purposes site has a very similar search engine.


回答1:


I think you need something like this:

subscribe(): void {
  this.word.debounceTime(400)
  .distinctUntilChanged()
  .switchMap((words: string): Observable<any[]> => 
    Observable.forkJoin(words.split(' ').map(word => this.http.get(word))) 
  )
  .map(arrayWithArrays => [].concat(arrayWithArrays)
  .subscribe((result: any[]): any[] => this.result = result);
 }



回答2:


I've come up with this huge but partial solution:

Fiddle here: https://jsfiddle.net/mtawrhLs/1/

Rx.Observable.prototype.combineAllCont = function () {
    let lastValues = [];

    return Rx.Observable.create(obs => {
        let i = 0;

        let subscription = this.subscribe(stream => {
            const streamIndex = i;
            subscription.add(stream.subscribe(res => {
                lastValues[streamIndex] = res;
                obs.next(lastValues);
            }));
            i++;
        });

        return subscription;
    });
}

/** STUFF TO DEMO **/
let searchBox = [
    '',
    'car',
    'car ',
    'car food',
    'car food sun',
    'cat food sun',
    'cat sun'
]

function performSearch(word) {
    return Rx.Observable.defer(() => {
        console.log('sending search for ' + word);
        return Rx.Observable.of({
            word: word,
            results: word.split('')
        });
    })
}

let searchBoxStream = Rx.Observable.interval(500)
    .take(searchBox.length * 2)
    .map((i) => searchBox[i % searchBox.length]);

/** GOOD STUFF STARTS HERE **/
let wordsStream = searchBoxStream
    .map((str) => str.trim().split(' ').filter((w) => w.trim() != ''));

let wordSearchSubjects = [];
let wordSearchStreamSubject = new Rx.ReplaySubject(1);

wordsStream.subscribe((words) => {
    const nWords = words.length;
    const nSubjects = wordSearchSubjects.length;
    // Update streams
    for (i = 0; i < nWords && i < nSubjects; i++) {
        wordSearchSubjects[i].next(words[i]);
    }

    // Create streams
    for (i = nSubjects; i < nWords; i++) {
        const wordSearchSubject = new Rx.ReplaySubject(1);
        wordSearchSubjects.push(wordSearchSubject);
        wordSearchStreamSubject.next(
            wordSearchSubject.asObservable()
                .distinctUntilChanged()
                .flatMap((w) => performSearch(w))
                .concat(Rx.Observable.of(false)) // Ending signal
        )
        wordSearchSubjects[i].next(words[i]);
    }

    // Delete streams
    for (i = nWords; i < nSubjects; i++) {
        wordSearchSubjects[i].complete();
    }
    wordSearchSubjects.length = nWords;
});

let wordSearchStream = wordSearchStreamSubject
    .combineAllCont()
    .map((arr) => arr.filter((r) => r !== false));

resultingStream = wordSearchStream
    .map((arr) => {
        let ret = [];
        arr.forEach(search => {
            ret.push(search.results);
        });
        return ret;
    })
    .subscribe((arr) => console.log(arr));

Things to improve:

  1. I had to use a custom combineAll implementation that doesn't wait the original stream to complete. This can provide memory leaks if it's not correctly implemented, and it should remove the inner subscriptions that complete (it doesn't now)
  2. Word change detection has to be improved: Removing a word by the middle makes half of the words to be searched again.

Probably theres a better, more straight-forward solution, but I just could come up with this mess. Feels very hacky/dangerous.




回答3:


I think the solution is very clear. First, we have to perform a diff algorithm on the words. Stale words need to be removed, only new words will be requested.

The only real problem is how we implement it in the cleanest way without any data race. So i come up with one implementation:

searchTags$.pipe(
  scan((pair, tags) => [pair[1] || pair[0], tags], [[]]),
  concatMap(([prevTags, nextTags]) =>
    from(createActions(prevTags, nextTags))
  ),
  reduce((collection$, action) => collection$.pipe(action), of([]))),
  switchMap(stream => stream)
).subscribe(collection => { /*...*/ })

function createActions(prevTags, nextTags) {
  nextTags.reduce((actions, tag) => {
    if (isOld(prevTags, tag)) {
      actions.push(removeAction(tag));
    }
    if (isNew(prevTags, tag)) {
      actions.push(addAction(tag));
    }
    return actions;
  }, []);
}

function removeAction(tag) {
  return map(collection => { collection[tag] = undefined; return collection; });
}

function addAction(tag) {
  return switchMap(collection => requestTag(tag).pipe(map(
    res => { collection[tag] = res; return collection; }
  )));
}

That's the shortest that i can do!



来源:https://stackoverflow.com/questions/42957703/multiple-observables-working-together

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!