问题
I'm still a bit new to rxjs in Angular 5 and it a bit hard to formulate my question. Still I hope for some tips.
I often end up with the same setup:
- Multiple Components to display the same data
- A single Service to access the data
Now I have 2 options when receiving data via Observables:
a) Subscribe to an observable to get data once, and subscribe again to get updates
b) Subscribe to an observable and always get updates when data changes
a) ist straight forward, but with b) I often have troubles and wondering if this is the right way to use Observables.
One issue is, that unsubscribe gets important in some cases, and missing the unsubscribe leads to serious garbage being executed with each update of the observable.
On the other hand, with option a) I might miss some updates in one component when another component is updating the underlying data.
Is there any best practice to avoid all these pitfalls?
回答1:
It sounds like the concept you are trying to figure out is how to regulate subscription management for RxJS when using Angular. There are three main options that come to mind for this:
- Automatically create and delete subscriptions using the
async
pipe. If you want to make UI changes based strictly on data emitted from an observable, then theasync
pipe handily creates a subscription to the given observable when the component is created and removes those subscription when the component is destroyed. This is arguably the cleanest way to use subscriptions.
As an example:
@Component({
selector: 'my-component',
template: `
<div *ngFor="let value of value$ | async">
{{value}}
</div>
`
})
export class MyComponent {
public value$: Observable<String> = this.myValueService
.getValues()
.map(value => `Value: $value`);
constructor(myValueService: MyValueService) {}
}
- Manage subscriptions in components by creating class-level
Subscription
objects in thengOnInit
method and then unsubscribing in thengOnDestroy
method. This is the convention that I tend toward when I need access to the subscriptions within the component code. HavingngOnInit
andngOnDestroy
methods in every component that uses subscriptions adds boilerplate but is generally necessary if you need subscriptions in your component code.
For example:
@Component({
selector: 'my-component',
template: `
<div #myDiv></div>
`
})
export class MyComponent implements OnInit, OnDestroy {
private mySub: Subscription;
constructor(myValueService: MyValueService) {}
public ngOnInit() {
this.mySub = this.myValueService.getValue().subscribe((value) => {
console.log(value);
// Do something with value
});
}
public ngOnDestroy() {
this.mySub.unsubscribe();
}
}
- Limit subscription life by using a limiting operation, such as
first()
. This is what is done by default when you initiate a subscription toHttpClient
observables. This has the benefit of requiring little code, but it can also lead to cases where the subscription is not cleaned up (e.g., if the observable never emits).
If everything that I want to do with an observable can be done in the view, then I virtually always use option 1. This covers most cases in my experience. You can always use intermediate observables to produce an observable that you can subscribe to in the view if you need to. Intermediate observables don't introduce memory leak concerns.
回答2:
Another option is to use the observable to retrieve the data, then let Angular's change detection handle the rest. With Angular's change detection, it will update the UI as the data changes ... no need to subscribe again to get updates.
For example, I have this type of UI:
I retrieve the data using Http and an observable. But then I leverage Angular's change detection to handle any updates.
Here is a piece of my service:
@Injectable()
export class MovieService {
private moviesUrl = 'api/movies';
private movies: IMovie[];
currentMovie: IMovie | null;
constructor(private http: HttpClient) { }
getMovies(): Observable<IMovie[]> {
if (this.movies) {
return of(this.movies);
}
return this.http.get<IMovie[]>(this.moviesUrl)
.pipe(
tap(data => console.log(JSON.stringify(data))),
tap(data => this.movies = data),
catchError(this.handleError)
);
}
// more stuff here
}
And here is the full code (except the imports) for the detail component shown on the right above:
export class MovieDetailComponent implements OnInit {
pageTitle: string = 'Movie Detail';
errorMessage: string;
get movie(): IMovie | null {
return this.movieService.currentMovie;
}
constructor(private movieService: MovieService) {
}
ngOnInit(): void {
}
}
You can see the complete example (with editing) here: https://github.com/DeborahK/MovieHunter-communication/tree/master/MH-Take5
回答3:
When passing data between components, I find the RxJS BehaviorSubject
very useful.
You can also use a regular RxJS Subject
for sharing data via a service, but here’s why I prefer a BehaviorSubject.
- It will always return the current value on subscription - there is no need to call onnext().
- It has a getValue() function to extract the last value as raw data.
- It ensures that the component always receives the most recent data.
- you can get an observable from behavior subject using the
asobservable()
method on behavior subject. - Refer this for more
Example
In a service, we will create a private BehaviorSubject that will hold the current value of the message. We define a currentMessage variable to handle this data stream as an observable that will be used by other components. Lastly, we create the function that calls next on the BehaviorSubject
to change its value.
The parent, child, and sibling components all receive the same treatment. We inject the DataService in the components, then subscribe to the currentMessage observable and set its value equal to the message variable.
Now if we create a function in any one of these components that changes the value of the message. The updated value is automatically broadcasted to all other components.
shared.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@Injectable()
export class SharedService {
private messageSource = new BehaviorSubject<string>("default message");
currentMessage = this.messageSource.asObservable();
constructor() { }
changeMessage(message: string) {
this.messageSource.next(message)
}
}
parent.component.ts
import { Component, OnInit } from '@angular/core';
import { SharedService } from "../shared.service";
@Component({
selector: 'app-sibling',
template: `
{{message}}
<button (click)="newMessage()">New Message</button>
`,
styleUrls: ['./sibling.component.css']
})
export class SiblingComponent implements OnInit {
message: string;
constructor(private service: sharedService) { }
ngOnInit() {
this.service.currentMessage.subscribe(message => this.message = message)
}
newMessage() {
this.service.changeMessage("Hello from Sibling")
}
}
sibling.component.ts
import { Component, OnInit } from '@angular/core';
import { SharedService } from "../shared.service";
@Component({
selector: 'app-sibling',
template: `
{{message}}
<button (click)="newMessage()">New Message</button>
`,
styleUrls: ['./sibling.component.css']
})
export class SiblingComponent implements OnInit {
message: string;
constructor(private service: SharedService) { }
ngOnInit() {
this.service.currentMessage.subscribe(message => this.message = message)
}
newMessage() {
this.service.changeMessage("Hello from Sibling");
}
}
来源:https://stackoverflow.com/questions/49307098/persistent-subscriptions-with-rxjs-in-angular-5