admin管理员组

文章数量:1433496

I have two collections: persons and pets. Every pet has personId. My target is to get all persons and foreach of them to add his/her pets in single json. What I did so far is:

this.personService.getPersions().subscribe(persons => {
  const personsWithPets = persons.flatMap(person => this.petService.getPetsByPersonId(person._id)
    .subscribe(petsData => {
      person.pets = petsData;
      return person;
    }, (err) => {
     console.log(err);
    }));
  this.persons = personsWithPets; // This is fired before previous subscribe
}, (err) => {
  console.log(err);
});

What I do wrong? Why this.persons = personsWithPets; is fired before subscription finish?

I have two collections: persons and pets. Every pet has personId. My target is to get all persons and foreach of them to add his/her pets in single json. What I did so far is:

this.personService.getPersions().subscribe(persons => {
  const personsWithPets = persons.flatMap(person => this.petService.getPetsByPersonId(person._id)
    .subscribe(petsData => {
      person.pets = petsData;
      return person;
    }, (err) => {
     console.log(err);
    }));
  this.persons = personsWithPets; // This is fired before previous subscribe
}, (err) => {
  console.log(err);
});

What I do wrong? Why this.persons = personsWithPets; is fired before subscription finish?

Share Improve this question asked Mar 13, 2019 at 11:05 IntoTheDeepIntoTheDeep 4,11815 gold badges45 silver badges87 bronze badges 2
  • 1 because your getPetsByPersonId is probably asynchronus, so the code inside your call to getPetsByPersonId().subscribe will likely not return a value until after this.persons = personsWithPets. The subscribe is called, just the code inside won't run until the service returns a value. It really has nothing to do with nested observables – BlackICE Commented Mar 13, 2019 at 11:08
  • Any suggestions to make it works? – IntoTheDeep Commented Mar 13, 2019 at 11:13
Add a ment  | 

4 Answers 4

Reset to default 2

Updated added ments

Another one: stackblitz

this.service.getPersons().pipe(switchMap((per:any[])=>{
       //create an array of observables
       const obs=per.map(per=>this.service.getPet(per.id));
       //call all of them in forkjoin
       return forkJoin(obs).pipe(map(pets=>
         //pets is an array, in pets[0] is the response of getPet(1), 
         //in pets[1] is the response of getPet(2)
         pets.map((pet,i)=>{
           return {
             ...per[i], //all the properties of the person
             pets:pet   //+ in pets an array with the pets of the person
             }
         })
       ))
     })).subscribe(res=>this.res=res)

I have made an example for you

const { of } = rxjs;
const { map, switchMap, toArray, mergeMap } = rxjs.operators;

function getPeople() {
  return of([{ id: 1, name: 'Amanda' }, { id: 2, name: 'Nancy' }]);
}

function getPetsByPersonId(id) {
  switch(id) {
    case 1:
      return of(['Doggie']);
    case 2:
      return of(['Kitten']);
  }
}

const getPets = (person) => {
  return getPetsByPersonId(person.id).pipe(
    map(pets => ({ ...person, pets }))
  )
}

getPeople().pipe(
  switchMap(people => people),
  mergeMap(getPets),
  toArray()
)
  .subscribe(peopleWithPets => console.log(peopleWithPets));
<script src="https://unpkg./rxjs/bundles/rxjs.umd.min.js"></script>

Using subscribe inside subscribe is considered as a bad practice and may lead to problems as you described. I would suggest to use mergeMap bined with forkJoin operator:

this.personService.getPersions().pipe(mergeMap(persons => {
   const requests = persons.map(person => this.petService.getPetsByPersonId(person._id));
   return forkJoin(of(persons), ...requests);
}),
map(values => {
  const persons = values[0];
  const pets = values.slice(1);
  // here you need to assign correct pet to correct person

})
).subscribe(personsWithPets => {
  console.log(personsWithPets);
}, err => {
  console.log(err);
});

Observables are asynchronous by nature. The subscribe call will be run in another thread while the rest of the instructions in the method will continue to run in the main thread. What you need to do is wait until the asynchronous action pletes before running the next one. You can do this by putting the isntructions inside of the subscription, or better yet use the onCompleted parameter which es after the (err) param, like so

this.personService.getPersons().subscribe(persons => {
  const personsWithPets = persons.flatMap(person => 
        this.petService.getPetsByPersonId(person._id)
            .subscribe(petsData => {
                 person.pets = petsData;
                 return person;
             }, 
             (err) => {
                 console.log(err);
             },
             () => {
                 this.persons = personsWithPets;
             }))
        });

本文标签: javascriptAngular 7 nested observablesStack Overflow