为什么我的BehaviorSubject在我没有传递新值给next()方法时更新?

huangapple go评论75阅读模式
英文:

Why does my BehaviourSubject update when I don't pass a new value to the next() method?

问题

你的TasksService中有一个BehaviorSubject,用于发射任务数组的变化。当你删除了this._tasks$.next(this._tasks);这行代码时,虽然你的任务数组被添加了新的任务,但BehaviorSubject没有得到更新,因此Angular并不知道数组已经改变了。

当你重新添加this._tasks$.next(this._tasks);后,每次调用addTask方法时,都会更新BehaviorSubject的值,Angular能够检测到变化并重新渲染视图。这就是为什么在加入this._tasks$.next(this._tasks);后,你会看到每次点击"Add Task"按钮都会触发日志,而没有它时,只有初始几次点击才会触发。

BehaviorSubject的next方法用于将新值传递给订阅者,这在Angular的应用中通常用于通知组件数据的变化,以便及时更新UI。

英文:

I am trying to make a simple TODO app in angular using RXJS. I have a JSON server mock database with my 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 task I don't want to post them to the server right away.
So when I get my tasks from the server, I save them to the _tasks property and then pass them to the next() method for my _tasks$: BehaviorSubject.
Because later I want to post my tasks to the server in bulk and now I just want them to display properly in Angular.

In my AppComponent I get my tasks and assign them to my tasks 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 for my tasks property and display my tasks:

<ng-container *ngFor="let task of tasks | async">
    {{task.text}}
    {{task.id}}
</ng-container>
<button type="button" (click)="addTask()">Add Task</button>

But later 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 still works! Angular displays newly added tasks even though I don't pass new task array for my BehaviorSubject.

So I decided to log values from my tasks! : BehaviorSubject<ITask[]> property in my AppComponent class:

public addTask()
  {
    this._tasksService.addTask(
      {
        id: crypto.randomUUID(),
        isImportant: true,
        text: &#39;Added task&#39;
      }
    );

    this.tasks.pipe(tap((value) =&gt; console.log(value)
    )).subscribe();
  }

And task is added as expected - every time a get an array with one more task:

Array(3) [ {…}, {…}, {…} ] &lt;- Add task button is clicked 
Array(4) [ {…}, {…}, {…}, {…} ] &lt;- Add task button is clicked 
Array(5) [ {…}, {…}, {…}, {…}, {…} ] &lt;- Add task button is clicked 

But when I return this line to my addTask method in TaskService:
this._tasks$.next(this._tasks);

I get these logs:

Array(3) [ {…}, {…}, {…} ] &lt;- Add task button is clicked -&gt; one task is added
Array(4) [ {…}, {…}, {…}, {…} ] &lt;- Add task button is clicked -&gt; one task is added
Array(4) [ {…}, {…}, {…}, {…} ] &lt;- I get the same array 
Array(5) [ {…}, {…}, {…}, {…}, {…} ] &lt;- Add task button is clicked -&gt; one task is added
Array(5) [ {…}, {…}, {…}, {…}, {…} ] &lt;- I get the same array 
Array(5) [ {…}, {…}, {…}, {…}, {…} ] &lt;- I get the same array once again

So I am kinda lost why the observable behaves like this... Maybe I don't fully understand the next() method?

答案1

得分: 2

I understand your request. Here's the translated content:

根据我的理解,您对这段代码有两个问题。首先,您不知道为什么会有重复的控制台日志。其次,您不知道为什么即使没有在行为主题上调用".next()",您的视图也会更新。

让我们从第一个问题开始。

您需要了解rxjs可观察对象和行为主题的工作原理。
正常的可观察对象,在订阅时会等待某个值被发出,然后每次发生时都会调用您附加到它的操作。例如:

exampleSubject = new Subject<string>();

ngOnInit(): void {
    this.exampleSubject.pipe(
        tap(console.log)
    ).subscribe();
}

emitValue() {
    this.exampleSubject.next("text");
}

现在请注意,在此代码中,我们在ngOnInit中只订阅了一次。尽管如此,每当调用emitValue()方法(例如从按钮中调用)时,console.log都将被调用。这是因为订阅会一直持续到取消订阅。这意味着每次在主题上调用next()时,都会调用操作。

那么当您多次订阅时会发生什么呢?让我们试一试:

exampleSubject = new Subject<string>();

ngOnInit(): void {
    subscribeToSubject(); // 第1次
    subscribeToSubject(); // 第2次
    subscribeToSubject(); // 第3次
    this.emitValue();
}

emitValue() {
    this.exampleSubject.next("text");
}

subscribeToSubject() {
    this.exampleSubject.pipe(
        tap(console.log)
    ).subscribe();
}

我们订阅了一个主题3次,现在每当值被发出时,console.log将被调用3次。这是对您创建的每个订阅都会调用的操作。

现在当我们看您的示例时,每次单击addTask按钮时都会添加订阅。这就是为什么每次添加任务时都会有一个额外的控制台日志。
但是,嘿,在您的第一个示例中,当您删除了.next()时,即使没有发出任何值,您也有一些控制台日志,为什么呢?这就是行为主题的作用。这是一种特殊类型的主题,它保存其值并在订阅时发出它。并且每次调用.next()时也会发出,但您没有这样做,所以每次订阅时它只调用1次控制台日志。

您应该只在例如ngOnInit()中调用一次:

this.tasks.pipe(tap((value) => console.log(value))).subscribe();

好了,现在让我们来解决第二个问题。

这相当简单,但需要一些关于引用如何工作的知识。

在您的服务中,您将整个任务数组放入了行为主题中。实际上,此主题持有对此数组的引用。这意味着每当您向数组中添加新值时,行为主题也会具有该值。这就是发生的情况,每当您调用addTask方法时,您都会将新值推送到任务数组中,而您的行为主题也具有它。

英文:

As I understand, you have two issues with this code. First, you don't know why you have duplicated console logs. Second, you don't know why your view is updated even though you didn't call ".next()" on your behaviour subject.

Let's start with the first one.

You need to understand how rxjs observables and behaviourSubjects work.
Normal observable, when you subscribe to it, will wait for some value to be emitted, and then every time it happens, it will invoke an action that you attached to it. For example:

exampleSubject = new Subject&lt;string&gt;();

ngOnInit(): void {
    this.exampleSubject.pipe(
        tap(console.log)
    ).subscribe();
}

emitValue() {
    this.exampleSubject.next(&quot;text&quot;);
}

Now notice that in this code, we subscribed only once in ngOnInit. Despite this, console.log will be invoked every time you call emitValue() method (for example from a button). This is because subscriptions lasts until they're unsubscribed. This means, that action will be invoked every time there's next() called on a subject.

So what happens when you subscribe multiple times? Let's try it:

exampleSubject = new Subject&lt;string&gt;();

ngOnInit(): void {
    subscribeToSubject(); // 1st
    subscribeToSubject(); // 2nd
    subscribeToSubject(); // 3rd
    this.emitValue();
}

emitValue() {
    this.exampleSubject.next(&quot;text&quot;);
}

subscribeToSubject() {
    this.exampleSubject.pipe(
        tap(console.log)
    ).subscribe();
}

We subsribed to a subject 3 times, and now whenever value is emitted, console log will be called 3 times. It's called for every subscription you created.

Now when we have a look on your example, you add subscription every time you click on addTask button. That's why every time you add task, there's one more console log.
But hey, in your first example, when you removed .next(), you had some console logs, even though you didn't emit any values, why is that? And here we come to BehaviourSubject. This is a special kind of subject that holds its value and will emit it the moment it's subscribed to. AND it also emits every time you call .next(), but you don't do it, so that's why it calls 1 console log every time you subscribe.

What you should have done was to call

this.tasks.pipe(tap((value) =&gt; console.log(value)
)).subscribe();

only once, for example in ngOnInit()

Alright, now let's get to the second issue

This is quite simple, but requires some knowledge on how references work.

In your service you put in BehaviourSubject the whole array of tasks. In fact, this subject holds reference to this array. This means, that whenever you push new value in array, BehaviourSubject will also have it. And that's what happens, whenever you call addTask method, tou push new value to tasks array, and your BehaviourSubject also has it.

答案2

得分: 0

你在控制台中获得相同的值,因为每次单击按钮时都会添加一个新的订阅。将订阅移到构造函数或OnInit中。

修复后,您会问为什么屏幕上会出现新消息,但控制台中没有。这是因为当您在getTasksObservableFromDb中使用this._tasks$.next(tasks)时,您发送了内存链接到同一任务数组,这足以在数组更改时在屏幕上显示消息。但这对于behaviorsubject知道对象已更改来说还不够。为了避免这种情况,您应该使用this._tasks$.next([...tasks])(在getTasksObservableFromDb中)指向一个新数组。之后,您的代码将无法正常工作,如果没有下一个。

完整示例 https://stackblitz.com/edit/angular-vsrfhx?file=src/tasks.service.ts

英文:

You get the same values in console because you add a new subscription each time when you click button. Move the subscription to a constructor or OnInit.

When you fix it you will ask why you get the new message to screen but there is not in console. This is because when you this._tasks$.next(tasks) in getTasksObservableFromDb you send memory link to the same tasks array and this is enough to display the messages on the screen when the array is changed. but it's not enough to the behaviorsubject know that the object was changed. To avoid it you should point a new array with help this._tasks$.next([...tasks]) (in getTasksObservableFromDb).
After this you code will not work without the next.

Full example https://stackblitz.com/edit/angular-vsrfhx?file=src/tasks.service.ts

huangapple
  • 本文由 发表于 2023年5月14日 02:56:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/76244421.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定