I'm trying to make a simple TODO application using RXJS. I have a JSON server mock database containing TODO tasks.
So I ended up with this TasksService:
@Injectable({ providedIn: 'root' }) export class TasksService { private _tasks : ITask[] = []; private _tasks$: BehaviorSubject<ITask[]> = new BehaviorSubject<ITask[]>([]); constructor (private _http: HttpClient) { } public getTasks() { this.getTasksObservableFromDb().pipe( tap( (tasks) => { this._tasks = tasks; this._tasks$.next(tasks); } ) ).subscribe(); return this._tasks$; } public addTask(task: ITask) { this._tasks.push(task); this._tasks$.next(this._tasks); } private getTasksObservableFromDb(): Observable<any> { return this._http.get<any>('http://127.0.0.1:3000/tasks'); }
When I add tasks, I don't want to publish them to the server immediately. So when I get tasks from the server, I save them to the _tasks property and then pass them to the next() method of my _tasks$:BehaviorSubject . Because later I want to batch publish my tasks to the server and now I just want them to display correctly in Angular.
In my AppComponent I get the tasks and assign them to my task property.
export class AppComponent implements OnInit { public tasks!:BehaviorSubject<ITask[]>; constructor (private _tasksService: TasksService) {} ngOnInit(): void { console.log('OnInit'); this.tasks = this._tasksService.getTasks(); } public addTask() { this._tasksService.addTask( { id: crypto.randomUUID(), isImportant: true, text: 'Added task' } ); } }
In my HTML template, I use an async pipe as my task attribute and display my task:
<ng-container *ngFor="let task of tasks | async"> {{task.text}} {{task.id}} </ng-container> <button type="button" (click)="addTask()">Add Task</button>
But then I accidentally deleted this line in my TaskService:
this._tasks$.next(this._tasks);
So my method now looks like this:
public addTask(task: ITask) { this._tasks.push(task); }
But adding tasks is still valid! Even though I don't pass an array of new tasks to my BehaviorSubject, Angular displays the newly added tasks.
So I decided to log the value in my task! : BehaviorSubject<ITask[]> property in my AppComponent class:
public addTask() { this._tasksService.addTask( { id: crypto.randomUUID(), isImportant: true, text: 'Added task' } ); this.tasks.pipe(tap((value) => console.log(value) )).subscribe(); }
and tasks are added as expected - every time when getting an array containing one task:
Array(3) [ {…}, {…}, {…} ] <- Add task button is clicked Array(4) [ {…}, {…}, {…}, {…} ] <- Add task button is clicked Array(5) [ {…}, {…}, {…}, {…}, {…} ] <- Add task button is clicked
But when I return this line to the addTask method in TaskService:
this._tasks$.next(this._tasks);
I get these logs:
Array(3) [ {…}, {…}, {…} ] <- Add task button is clicked -> one task is added Array(4) [ {…}, {…}, {…}, {…} ] <- Add task button is clicked -> one task is added Array(4) [ {…}, {…}, {…}, {…} ] <- I get the same array Array(5) [ {…}, {…}, {…}, {…}, {…} ] <- Add task button is clicked -> one task is added Array(5) [ {…}, {…}, {…}, {…}, {…} ] <- I get the same array Array(5) [ {…}, {…}, {…}, {…}, {…} ] <- I get the same array once again
So I'm a bit lost as to why the observable behaves like this... maybe I don't fully understand the next() method?
P粉0806439752024-03-22 00:14:34
As far as I understand, there are two problems with this code. First, you don't know why the console logs are duplicated. Second, even though you didn't call ".next()" on the behavior subject, you don't know why the view updated.
Let’s start with the first one.
You need to understand how rxjs observables and BehaviorSubjects work. A normal Observable, when you subscribe to it, will wait for a certain value to be emitted, and then every time that happens, it will call the operation you attached to it. For example:
exampleSubject = new Subject(); ngOnInit(): void { this.exampleSubject.pipe( tap(console.log) ).subscribe(); } emitValue() { this.exampleSubject.next("text"); }
Now please note that in this code we only subscribe once in ngOnInit. Despite this, console.log is called every time the emitValue() method is called (e.g. from a button). This is because the subscription lasts until unsubscribed. This means that the operation will be called every time next() is called on the topic.
So what happens when you subscribe multiple times? Let’s try it:
exampleSubject = new Subject(); ngOnInit(): void { subscribeToSubject(); // 1st subscribeToSubject(); // 2nd subscribeToSubject(); // 3rd this.emitValue(); } emitValue() { this.exampleSubject.next("text"); } subscribeToSubject() { this.exampleSubject.pipe( tap(console.log) ).subscribe(); }
We subscribed to a topic 3 times and now whenever a value is emitted, the console log is called 3 times. It is called for every subscription you create.
Now when we look at your example, the subscription is added every time the addTask button is clicked. That's why every time you add a task, there is an extra console log. But hey, in your first example, when you remove .next() you have some console logs even though you don't emit any values, why is that? Now we come to BehaviorSubject. This is a special theme that holds its value and emits it as soon as you subscribe. And it also emits every time you call .next() but you don't do that, so that's why it calls 1 console log every time you subscribe.
What you should do is call
this.tasks.pipe(tap((value) => console.log(value) )).subscribe();
Only once, such as in ngOnInit()
Okay, now we enter the second period
This is very simple, but requires some knowledge of how references work.
In your service you put the entire task array into a BehaviorSubject. In fact, this subject holds a reference to this array. This means that every time you push a new value into the array, the BehaviorSubject will also own it. This is what happens, whenever you call the addTask method, a new value is pushed to the tasks array, and your BehaviorSubject also has it.