The Ultimate Answer To The Very Common Angular Question: subscribe() vs | async Pipe 對于常見...

原文來源
翻譯說明: 翻譯已取得作者授權(quán)锦溪。中英對照沈条, 意譯刃鳄, 譯文會有適當?shù)呐虐嫜嵯巍7g不到位的地方驻襟,還請看官多多指教夺艰。
免責說明: 本文僅用于學習目的, 請勿用于商業(yè)用途沉衣, 后果自負郁副。

Most of the popular Angular state management libraries like NgRx expose application state in a form of a stream of state objects.
大多數(shù)流行的 Angular 狀態(tài)管理庫, 比如 NgRx 會以狀態(tài)對象流的形式暴露應用狀態(tài)豌习。

This is usually implemented with the help of RxJS Observables.
這通常是借助于 RxJS Observables 實現(xiàn)的存谎。

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.
狀態(tài)更新被推送到組件,組件通過重新渲染其模板來顯示應用程序狀態(tài)的最新版本進行響應肥隆。

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:
組件可以有多種方式使用狀態(tài)更新的observable stream既荚,其中最流行的兩種是:

1、 Using the subscribe() method and storing the state on the component instance, todos$.subscribe(todos => this.todos = todos)...
使用 subscribe() 方法巷屿,將狀態(tài)存儲在組件實例里固以, 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 管道嘱巾, 直接在組件模板中拆解狀態(tài)對象憨琳, <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.
我思考這個問題和相關(guān)的權(quán)衡問題已經(jīng)有一段時間了。我更傾向于使用 subscribe()旬昭,但無法真正地說出恰當?shù)睦碛伞?/p>

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!
這導致我們需要在考慮所有利弊的同時對情況進行概述篙螟,從而創(chuàng)建最終的指導方針,以便在任何情況下都能做出客觀的決策!

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…
在我們繼續(xù)深入下去前问拘, 我想感謝 simply10wTim Deschryver遍略, 在我進行 Angular NgRx Material Starter項目的工作時, 他們提出了很多很棒的點子和反饋 (PR)骤坐, 該項目實現(xiàn)了本文所提到的觀點绪杏。

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)
    狀態(tài)處理邏輯的位置(組件的類中 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() 的優(yōu)勢

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.
拆解的屬性在組件中的任何地方都可使用盹愚。這意味著它可以直接在組件的方法中使用,而不需要從模板傳遞它站故。這樣皆怕,所有的狀態(tài)都可以保持在組件中。

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()”)
模板的唯一職責就是觸發(fā)此方法(比如, 使用 (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() 引入了補充需求端逼, 在組件生命周期末尾需要取消訂閱以避免內(nèi)存泄漏朗兵。

Developers usually have to unsubscribe manually.
開發(fā)者通常必須手動取消訂閱。

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…
這種解決方案非常啰嗦, 而且容易出錯礁鲁, 因為很容易就會忘記實現(xiàn) ngOnDestroy 盐欺, 而這并不會導致任何錯誤, 只是會有靜默的內(nèi)存泄漏仅醇。

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組件中可觀測數(shù)據(jù)源的最后一根稻草和交易破壞者

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 管道的優(yōu)勢

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.
只要保證你所有的業(yè)務邏輯(比如 reducer, service)是不可變的叶摄, 并且總是返回新的對象属韧。

Anyway, this is the whole purpose of using NgRx in a first place so I guess immutable data can be assumed…
無論如何,這是首先使用NgRx的全部目的蛤吓,因此我認為可以假設(shè)不可變數(shù)據(jù)...

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 管道的劣勢

  1. 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"泼疑, 集合會以直接的方式拆解德绿。

  2. 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…
    對象可能必須在單個模板中的多個不同位置多次拆解。這可以通過使用專用的包裝元素來避免王浴,但這意味著狀態(tài)管理強制對DOM結(jié)構(gòu)進行更改,這非常奇怪

  3. 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 拆解的屬性無法在組件的方法中訪問梅猿。這意味著氓辣, 我們必須將這些屬性在模板中作為方法參數(shù)傳遞給方法, 而這會將邏輯分離在模板和組件中袱蚓。

<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.
更重要的是蹬敲,我們可以說暇昂,這些特性的一致使用簡化了應用程序,因為開發(fā)人員總是可以采用單向數(shù)據(jù)流以及自動訂閱管理伴嗡。

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() 是恰當?shù)姆桨浮?/p>

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 的相關(guān)要點澄暮。 如果我們發(fā)現(xiàn)模板太復雜了, 并且充斥了很多 拆解對象的 | 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 變更檢測策略帶來的益處脐彩。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末碎乃,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子惠奸,更是在濱河造成了極大的恐慌梅誓,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佛南,死亡現(xiàn)場離奇詭異梗掰,居然都是意外死亡,警方通過查閱死者的電腦和手機嗅回,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門及穗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人绵载,你說我怎么就攤上這事埂陆】涟祝” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵焚虱,是天一觀的道長购裙。 經(jīng)常有香客問我,道長鹃栽,這世上最難降的妖魔是什么躏率? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮谍咆,結(jié)果婚禮上禾锤,老公的妹妹穿的比我還像新娘。我一直安慰自己摹察,他們只是感情好恩掷,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著供嚎,像睡著了一般黄娘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上克滴,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天逼争,我揣著相機與錄音,去河邊找鬼劝赔。 笑死誓焦,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的着帽。 我是一名探鬼主播杂伟,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼仍翰!你這毒婦竟也來了赫粥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤予借,失蹤者是張志新(化名)和其女友劉穎越平,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灵迫,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡秦叛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了瀑粥。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挣跋。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖利凑,靈堂內(nèi)的尸體忽然破棺而出浆劲,到底是詐尸還是另有隱情,我是刑警寧澤哀澈,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布牌借,位于F島的核電站,受9級特大地震影響割按,放射性物質(zhì)發(fā)生泄漏膨报。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一适荣、第九天 我趴在偏房一處隱蔽的房頂上張望现柠。 院中可真熱鬧,春花似錦弛矛、人聲如沸够吩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽周循。三九已至,卻和暖如春万俗,著一層夾襖步出監(jiān)牢的瞬間湾笛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工闰歪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嚎研,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓库倘,卻偏偏與公主長得像临扮,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子于樟,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi閱讀 7,325評論 0 10
  • [玫瑰][玫瑰][玫瑰] 現(xiàn)在好【智匯教育~林全】20180403第048天分享: 銷售準備 絕大...
    林一智匯教育閱讀 697評論 0 0
  • 我有一個進步公条,呆在廚房的時間多了,早上六點起床迂曲,在廚房里邊靶橱。我做的事總結(jié)起來就是倒垃圾,把大人孩子昨夜洗澡的臟水倒...
    天清水藍112閱讀 84評論 0 0
  • 人路捧,有很多種关霸。以前在一張圖片上看到寫的文字,大意就是看見過紋身的大漢對老人家恭恭敬敬的杰扫,也見過穿著一身西服對著人破...
    豬豬霞的秘密樂園閱讀 200評論 0 0
  • 北越皇帝看到程庾尸體時队寇,立刻從龍椅上起來要去抱起程庾,隨后的腳步聲讓他止了動作章姓。 白飛飛與歐陽明日對視了一眼佳遣,神色...
    半盞風月閱讀 1,025評論 0 2