The Ultimate Answer To The Very
原文来源
翻译说明: 翻译已取得作者授权。中英对照, 意译, 译文会有适当的排版。翻译不到位的地方,还请看官多多指教。
免责说明: 本文仅用于学习目的, 请勿用于商业用途, 后果自负。
Most of the popular Angular state management libraries like NgRx expose application state in a form of a stream of state objects.
大多数流行的 Angular 状态管理库, 比如 NgRx 会以状态对象流的形式暴露应用状态。
This is usually implemented with the help of RxJS Observables.
这通常是借助于 RxJS Observables 实现的。
The state updates get pushed to the components which react by re-rendering their templates to display the most recent version of the application state.
状态更新被推送到组件,组件通过重新渲染其模板来显示应用程序状态的最新版本进行响应。
There are multiple ways in which it is possible to consume this observable stream of state updates by the components, the two most popular being:
组件可以有多种方式使用状态更新的observable stream,其中最流行的两种是:
1、 Using the subscribe()
method and storing the state on the component instance, todos$.subscribe(todos => this.todos = todos)
...
使用 subscribe()
方法,将状态存储在组件实例里, todos$.subscribe(todos => this.todos = todos)
...
2、 The | async
pipe unwraps the state object directly in the component’s template, <li *ngFor=”let todo of todos$ | async”></li>
...
使用 | async
管道, 直接在组件模板中拆解状态对象, <li *ngFor=”let todo of todos$ | async”></li>
...
I have been thinking about this question and related trade-offs for quite some time now. I was mostly in favor of using subscribe() but couldn’t really point out exact reasons why.
我思考这个问题和相关的权衡问题已经有一段时间了。我更倾向于使用 subscribe()
,但无法真正地说出恰当的理由。
This led to a need to come up with an overview of the situation while considering all the pros and cons to create a final guideline to be able to make objective decision in every case!
这导致我们需要在考虑所有利弊的同时对情况进行概述,从而创建最终的指导方针,以便在任何情况下都能做出客观的决策!
Before we dig deeper, I would like to thank simply10w and Tim Deschryverwho provided lots of great input and feedback (PR) while working on Angular NgRx Material Starter project which implements ideas discussed in this article…
在我们继续深入下去前, 我想感谢 simply10w 和 Tim Deschryver, 在我进行 Angular NgRx Material Starter项目的工作时, 他们提出了很多很棒的点子和反馈 (PR), 该项目实现了本文所提到的观点。
Topics 话题
Please notice following things which make a big impact on the final answer to this question…
请注意以下对此问题的最终答案有很大影响的事情
-
the way
| async
pipe works for the collection vs for singular objects
对于 集合 和单个对象来说,| async
管道的运行方式 -
possibility of using new-ish
*ngIf
“as” syntax (from Angular 4 and above)
使用稍微新的的*ngIf
“as” 语法的可能性(从 Angular 4 开始) -
location of state handling logic (component’s class vs template)
状态处理逻辑的位置(组件的类中 vs 模板中)
Case 1: Use subscribe()
in the ngOnInit
method 案例1: 在 ngOnInit
方法中使用 subscribe()
@Component({
/* ... */
template: `
<ul *ngIf="todos.length > 0">
<li *ngFor="let todo of todos">{{todo.name}}</li>
</ul>
`
})
export class TodosComponent implements OnInit, OnDestroy {
private unsubscribe$ = new Subject<void>();
todos: Todo[];
constructor(private store: Store<State>) {}
ngOnInit() {
this.store
.pipe(select(selectTodos), takeUntil(this.unsubscribe$)) // unsubscribe to prevent memory leak
.subscribe(todos => this.todos = todos); // unwrap observable
}
ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}
Simple example of consuming observable stream of todos by unwrapping it in the component ngOnInit
method and using unwrapped property in the template
很简单的一个消费 todos 的 observable stream, 在组件 ngOnInit
方法中拆解, 然后在模板中使用拆解的属性。
👍 Advantages of using subscribe()
使用 subscribe()
的优势
1、Unwrapped property can be used in multiple places in the template “as it is” without the need to rely on various workarounds as will be shown in the case 2 examples
拆解的属性可以在模板的很多地方“原样” 使用, 不用依赖各种变通方法, 如案例2 例子所示
2、Unwrapped property is available everywhere in the component. This means it can be used directly in the component’s method without the need to pass it from the template. That way, all state can be kept in the component.
拆解的属性在组件中的任何地方都可使用。这意味着它可以直接在组件的方法中使用,而不需要从模板传递它。这样,所有的状态都可以保持在组件中。
toggleAllTodosToDone() {
// 拆解的 `todos` 可以直接在方法中访问
this.todos.forEach(todo => {
this.store.dispatch(new ActionToggleTodo({id: todo.id, done: true}))
})
}
The only responsibility of the template is to trigger this method (eg using (click)=”toggleAllTodosToDone()”
)
模板的唯一职责就是触发此方法(比如, 使用 (click)=”toggleAllTodosToDone()”
)
👎 Disadvantages of using subscribe()
使用 subscribe()
的劣势
Using of the subscribe()
introduces complementary need to unsubscribe at the end of the component life-cycle to avoid memory leaks.
使用 subscribe()
引入了补充需求, 在组件生命周期末尾需要取消订阅以避免内存泄漏。
Developers usually have to unsubscribe manually.
开发者通常必须手动取消订阅。
The most RxJS (declarative) way to do this is to employ takeUntil(unsubscribe$)
operator as shown in the example above.
最 RxJS(声明式) 的方式是使用 takeUntil(unsubscribe$)
操作符, 如上例所示。
This solution is verbose and error prone because it is very easy to forget implementing ngOnDestroy
which will not lead to any errors just a silent memory leak…
这种解决方案非常啰嗦, 而且容易出错, 因为很容易就会忘记实现 ngOnDestroy
, 而这并不会导致任何错误, 只是会有静默的内存泄漏。
Subscribing to the observable manually in the ngOnInit()
doesn’t work with OnPush
change detection strategy out of the box.
在 ngOnInit()
中手动订阅 observable 并不会开箱即用地和 OnPush
变更检测策略 正常运行。
We could make it work by using this.cd.markForCheck()
inside of our subscribe handler but this is a very easy to forget, error prone solution.
我们可以通过在 订阅处理程序中使用 this.cd.markForCheck()
让其正常运行, 但是这是一种很易忘且易出错的方式。
constructor(
private store: Store<State>,
private cd: ChangeDetectorRef,
) {}
ngOnInit() {
this.store
.pipe(select(selectTodos), takeUntil(unsubscribe$))
.subscribe(todos => {
this.todos = todos;
this.cd.markForCheck();
})
}
The issue with the OnPush
change detection strategy was the final straw and the deal-breaker for my previously favorite subscribe()
approach to handling of the observable data sources in the Angular components
OnPush
变更检测策略的问题是我以前最喜欢的 subscribe()
方法处理 Angular组件中可观测数据源的最后一根稻草和交易破坏者
Case 2: Use | async
pipe in the component template 案例2: 在组件模板中使用 | async
管道
@Component({
/* ... */
template: `
<ul *ngIf="(todos$ | async).length">
<li *ngFor="let todo of todos$ | async">{{todo.name}}</li>
</ul>
`
})
export class TodosComponent implements OnInit {
todos$: Observable<Todo[]>;
constructor(private store: Store<State>) {}
ngOnInit() {
this.todos$ = this.store.pipe(select(selectTodos))
}
}
👍 Advantages of using | async
pipe 使用 | async
管道的优势
Solution works with OnPush
change detection out of the box!
此方案搭配OnPush
开箱即用。
Just make sure that all your business logic (eg reducer, service) is immutable and always returns new objects.
只要保证你所有的业务逻辑(比如 reducer, service)是不可变的, 并且总是返回新的对象。
Anyway, this is the whole purpose of using NgRx in a first place so I guess immutable data can be assumed…
无论如何,这是首先使用NgRx的全部目的,因此我认为可以假设不可变数据...
Angular handles subscriptions of | async
pipes for us automatically so there is no need to unsubscribe manually in the component using ngOnDestroy
. This leads to less verbosity and hence less possibilities for making a mistake. Yaaay 😸
Angular 自动为我们处理 | async
管道的订阅, 因此没有必要在组件中使用 ngOnDestroy
手动取消订阅。 这样的话就没那么啰嗦, 也就减少犯的地可能。
👎 Disadvantages of using | async
pipe 使用 | async
管道的劣势
-
Objects have to be unwrapped in the template using
*ngIf="something$ | async as something"
syntax. On the other hand, this is not a problem with collections which get unwrapped in a straight forward manner when using*ngFor="let something of somethings$ | async"
.
对象必须在模板中使用*ngIf="something$ | async as something"
语法拆解。另一方面, 对于集合来说, 这并不是问题。当使用*ngFor="let something of somethings$ | async"
, 集合会以直接的方式拆解。 -
Objects have to be potentially unwrapped multiple times in a single template in multiple different places. This can be avoided by using a dedicated wrapper element but that means that the state management is mandating changes to
DOM
structure which is pretty weird…
对象可能必须在单个模板中的多个不同位置多次拆解。这可以通过使用专用的包装元素来避免,但这意味着状态管理强制对DOM结构进行更改,这非常奇怪 -
Properties unwrapped in the template using
*ngIf
or*ngFor
are notaccessible in the component’s methods. This means we have to pass these properties to the methods from the template as the method parameters which further splits logic between the template and the component itself…
在模板中使用*ngIf
或者*ngFor
拆解的属性无法在组件的方法中访问。这意味着, 我们必须将这些属性在模板中作为方法参数传递给方法, 而这会将逻辑分离在模板和组件中。
<h1>{{(something | async).title}}</h1> <!-- 很多 | async pipes-->
<p>{{(something | async).description}}</p>
<ul>
<li *ngFor="let item of (something | async).items">{{item.name}}</li>
</ul>
<!-- versus -->
<div *ngIf="something$ | async as something"> <!-- 包装 元素 -->
<h1>{{something.title}}</h1>
<p>{{something.description}}</p>
<ul>
<li *ngFor="let item of something.items">{{item.name}}</li>
</ul>
</div>
We could also use <ng-container>
instead of <div>
. This way, no new wrapper element will be created in the final DOM
but we still end up with wrapped element in the template source code.
我们也可以使用 <ng-container>
替代 <div>
。 这样的话, 在最终的 DOM 中并没有新的 包装元素生成, 但是我们最终还是在模板源码中加入了包装元素。
Many thanks to Martin Javinez for suggesting solution with multiple | async
pipes resolved into one variable…
非常感谢 Martin Javinez 提出 将多个 | async
管道解析到一个变量中的方法
<ng-container *ngIf="{
something: something | async,
somethingElse: somethingElse | async
} as data">
<!-- 然后在模板中使用 -->
{{data.something}}
</ng-container>
Many thanks to Ward Bell for pointing out that besides using wrapper <ng-container></ng-container>
element there is an another way of preventing multiple subscription by multiple | async
pipes in our templates…
非常感谢 Ward Bell 指出除了使用包装元素 <ng-container></ng-container>
之外, 还有另一种方式避免在我们的模板中通过多次使用 | async
管道的方式多次订阅。
The solution is to pipe our observable stream through ReplaySubject
like this something$ = sourceOfSomething$.pipe(ReplaySubject(1));
解决方法就是通过 ReplaySubject
, 引导我们的 observable stream,
就像这样something$ = sourceOfSomething$.pipe(ReplaySubject(1));
The Verdict 🥊 裁决
The OnPush
change detection strategy is great for performance so we should be using | async
pipe as much as possible.
OnPush
变更检测策略对于性能非常有用, 所以我们应当尽可能地使用| async
管道
More so, we could argue that the consistent use of these features simplifies application because developers can always assume one way data flow and automatic subscription management.
更重要的是,我们可以说,这些特性的一致使用简化了应用程序,因为开发人员总是可以采用单向数据流以及自动订阅管理。
The | async
pipe is the clear winner
| async
管道显然是赢家
Considerations 考虑
The subscribe()
solution does provide some benefits in terms of template readability and simplicity. It can be the right solution in case we’re not using OnPush
change detection strategy and are not planing to use it in the future…
就模板的可读性和简洁性而言, subscribe()
方案确实提供了一些好处。 如果我们没用 OnPush
变更检测策略并且将来也不会使用的情况下, 使用 subscribe()
是恰当的方案。
Also, check out related great tip from Tim Deschryver. If we find ourselves in a situation when our template is getting too complex and bloated with many | async
pipes unwrapping a lot of objects we should consider breaking that component into a smart wrapper and multiple dumb view components…
看看Tim Deschryver 的相关要点。 如果我们发现模板太复杂了, 并且充斥了很多 拆解对象的 | async
管道, 我们应该考虑将组件分为小的包装组件和多个 dumb 视图组件。
After that you can simply pass unwrapped property inside your dumb component like this <dumb [prop]="value$ | async"></dumb>
so you have working OnPush
while having a benefit of working with the unwrapped objects in the potentially complex template of the dumb component.
然后, 你就可以简单地将拆解的属性传递到你的 dumb 组件里, 就像这样 <dumb [prop]="value$ | async"></dumb>
。 这样, OnPush
正常运行的同时, 也有益于 拆解的对象在可能复杂的 dumb 组件的模板
// TodosComponent.ts
@Component({// smart (container) component
/*...*/
template: `<todo-list [todos]="todos$ | async"></todo-list>` // dumb component consumes unwrapped todos
})
export class TodosComponent impelements OnInit {
todos$: Observable<Todo[]>;
constructor(private store: Store<State>) {}
ngOnInit() {
this.todo$ = this.store.pipe(select(selectTodos))
}
}
// TodoList
@Component({// dumb (view) component
/* ... */
template: `
<ul>
<li *ngFor="let todo of todos">{{todo.name}}</li>
</ul>
`
})
export class TodoList {
@Input() todos: Todo[];
}
In case of complex template it often make sense to extract parts of it into stand-alone dumb components which can work directly with unwrapped objects while benefiting from the OnPush
change detection strategy.
如果遇到复杂的模板, 将其一部分提取到独立的 dumb 组件很有意义, 它可以直接和拆解的对象搭配的同时还享有 OnPush
变更检测策略带来的益处。