问题
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:
- I had to use a custom
combineAllimplementation 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) - 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