首頁  >  文章  >  web前端  >  快速了解Angular中的onPush變更檢測策略

快速了解Angular中的onPush變更檢測策略

青灯夜游
青灯夜游轉載
2021-09-09 19:44:362191瀏覽

這篇文章帶大家深入了解Angular中的onPush變更偵測策略,希望對大家有幫助!

快速了解Angular中的onPush變更檢測策略

預設的變更偵測策略

#預設情況下,Angular使用ChangeDetectionStrategy.Default策略來進行變更檢測。

預設策略並未事先對應用程式做出任何假設,因此,每當使用者事件、記時器、XHR、promise等事件使應用程式中的資料將發生了改變時,所有的元件中都會執行變更檢測。

這表示從點擊事件到從ajax呼叫接收到的資料之類的任何事件都會觸發變更偵測。

透過在元件中定義一個getter並且在模板中使用它,我們可以很容易的看出這一點:

@Component({
  template: `
    <h1>Hello {{name}}!</h1>
    {{runChangeDetection}}
  `
})
export class HelloComponent {
  @Input() name: string;

  get runChangeDetection() {
    console.log(&#39;Checking the view&#39;);
    return true;
  }
}
@Component({
  template: `
    <hello></hello>
    <button (click)="onClick()">Trigger change detection</button>
  `
})
export class AppComponent  {
  onClick() {}
}

#執行以上程式碼後,每當我們點擊按鈕時。 Angular將會執行一遍變更偵測循環,在console裡我們可以看到兩行「Checking the view」的日誌。

這種技術被稱為髒檢查。為了知道視圖是否需要更新,Angular需要存取新值並和舊值比較來判斷是否需要更新視圖。

現在想像一下,如果有一個有成千上萬個表達式的大應用,Angular去檢查每一個表達式,我們可能會遇到效能上的問題。

那麼有沒有辦法讓我們主動告訴Angular什麼時候去檢查我們的元件呢?

OnPush變更偵測策略

我們可以將元件的ChangeDetectionStrategy設定成ChangeDetectionStrategy.OnPush

這將告訴Angular該元件僅依賴它的@inputs(),只有在以下幾種情況才需要檢查:

1. Input 引用發生改變

透過設定onPush變更偵測測策略,我們與Angular約定強制使用不可變物件(或稍後將要介紹的observables)。

在變更偵測的上下文中使用不可變物件的好處是,Angular可以透過檢查引用是否發生了變化來判斷視圖是否需要檢查。這將會比深度檢查容易得多。

讓我們試試看來修改一個物件然後看看結果。

@Component({
  selector: &#39;tooltip&#39;,
  template: `
    <h1>{{config.position}}</h1>
    {{runChangeDetection}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TooltipComponent  {

  @Input() config;

  get runChangeDetection() {
    console.log(&#39;Checking the view&#39;);
    return true;
  }
}
@Component({
  template: `
    <tooltip [config]="config"></tooltip>
  `
})
export class AppComponent  {
  config = {
    position: &#39;top&#39;
  };

  onClick() {
    this.config.position = &#39;bottom&#39;;
  }
}

這時候去點擊按鈕時看不到任何日誌了,這是因為Angular將舊值和新值的引用進行比較,類似於:

/** Returns false in our case */
if( oldValue !== newValue ) { 
  runChangeDetection();
}

值得一提的是numbers, booleans, strings, null 、undefined都是原始型別。所有的原始類型都是按值傳遞的. Objects, arrays, 還有 functions 也是按值傳遞的,只不過值是引用地址的副本

所以為了觸發對該元件的變更偵測,我們需要更改這個object的參考。

@Component({
  template: `
    <tooltip [config]="config"></tooltip>
  `
})
export class AppComponent  {
  config = {
    position: &#39;top&#39;
  };

  onClick() {
    this.config = {
      position: &#39;bottom&#39;
    }
  }
}

將物件參考改變後,我們將看到視圖已被檢查,新值被顯示出來。

2.源自於該元件或其子元件的事件

當在一個元件或其子元件中觸發了某一個事件時,這個元件的內部狀態會更新。 例如:

@Component({
  template: `
    <button (click)="add()">Add</button>
    {{count}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  count = 0;

  add() {
    this.count++;
  }

}

當我們點擊按鈕時,Angular執行變更偵測循環並更新視圖。

你可能會想,按照我們開頭講述的那樣,每一次異步的API都會觸發變更檢測,但是並不是這樣。

你會發現這個規則只適用於DOM事件,以下這些API並不會觸發變更偵測:

@Component({
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  count = 0;

  constructor() {
    setTimeout(() => this.count = 5, 0);

    setInterval(() => this.count = 5, 100);

    Promise.resolve().then(() => this.count = 5); 
    
    this.http.get(&#39;https://count.com&#39;).subscribe(res => {
      this.count = res;
    });
  }

  add() {
    this.count++;
  }

注意你仍然是更新了該屬性的,所以在下一個變更偵測流程中,例如去點選按鈕,count值將會變成6(5 1)。

3. 顯示的去執行變更偵測

Angular給我們提供了3種方法來觸發變更偵測。

第一個是detectChanges()來告訴Angular在該元件和它的子元件中去執行變更偵測。

@Component({
  selector: &#39;counter&#39;,
  template: `{{count}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent { 
  count = 0;

  constructor(private cdr: ChangeDetectorRef) {

    setTimeout(() => {
      this.count = 5;
      this.cdr.detectChanges();
    }, 1000);

  }

}

第二個是ApplicationRef.tick(),它告訴Angular來對整個應用程式執行變更偵測。

tick() {
 
  try {
    this._views.forEach((view) => view.detectChanges());
    ...
  } catch (e) {
    ...
  }
}

第三是markForCheck(),它不會觸發變更偵測。相反,它會將所有設置了onPush的祖先標記,在當前或者下一次變更檢測循環中檢測。

markForCheck(): void { 
  markParentViewsForCheck(this._view); 
}

export function markParentViewsForCheck(view: ViewData) {
  let currView: ViewData|null = view;
  while (currView) {
    if (currView.def.flags & ViewFlags.OnPush) {
      currView.state |= ViewState.ChecksEnabled;
    }
    currView = currView.viewContainerParent || currView.parent;
  }
}

需要注意的是,手動執行變更檢測並不是一種“hack”,這是Angular有意的設計並且是非常合理的行為(當然,是在合理的場景下)。

Angular Async  pipe

async pipe會訂閱一個 Observable 或 Promise,並傳回它發出的最近一個值。

讓我們來看一個input()是observable的onPush元件。

@Component({
  template: `
    <button (click)="add()">Add</button>
    <app-list [items$]="items$"></app-list>
  `
})
export class AppComponent {
  items = [];
  items$ = new BehaviorSubject(this.items);

  add() {
    this.items.push({ title: Math.random() })
    this.items$.next(this.items);
  }
}
@Component({
  template: `
     <div *ngFor="let item of _items ; ">{{item.title}}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
  @Input() items: Observable<Item>;
  _items: Item[];
  
  ngOnInit() {
    this.items.subscribe(items => {
      this._items = items;
    });
  }

}

當我們點擊按鈕並不能看到視圖更新。這是因為上述提到的幾種情況都沒有發生,所以Angular在目前變更偵測循環並不會檢車該組件。

现在,让我们加上async pipe试试。

@Component({
  template: `
    <div *ngFor="let item of items | async">{{item.title}}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
  @Input() items;
}

现在可以看到当我们点击按钮时,视图也更新了。原因是当新的值被发射出来时,async pipe将该组件标记为发生了更改需要检查。我们可以在源码中看到:

private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    this._ref.markForCheck();
  }
}

Angular为我们调用markForCheck(),所以我们能看到视图更新了即使input的引用没有发生改变。

如果一个组件仅仅依赖于它的input属性,并且input属性是observable,那么这个组件只有在它的input属性发射一个事件的时候才会发生改变。

Quick tip:对外部暴露你的subject是不值得提倡的,总是使用asObservable()方法来暴露该observable。

onPush和视图查询

@Component({
  selector: &#39;app-tabs&#39;,
  template: `<ng-content></ng-content>`
})
export class TabsComponent implements OnInit {
  @ContentChild(TabComponent) tab: TabComponent;

  ngAfterContentInit() {
    setTimeout(() => {
      this.tab.content = &#39;Content&#39;; 
    }, 3000);
  }
}
@Component({
  selector: &#39;app-tab&#39;,
  template: `{{content}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
  @Input() content;
}
<app-tabs>
  <app-tab></app-tab>
</app-tabs>

也许你会以为3秒后Angular将会使用新的内容更新tab组件。

毕竟,我们更新来onPush组件的input引用,这将会触发变更检测不是吗?

然而,在这种情况下,它并不生效。Angular不知道我们正在更新tab组件的input属性,在模板中定义input()是让Angular知道应在变更检测循环中检查此属性的唯一途径。

例如:

<app-tabs>
  <app-tab [content]="content"></app-tab>
</app-tabs>

因为当我们明确的在模板中定义了input(),Angular会创建一个叫updateRenderer()的方法,它会在每个变更检测循环中都对content的值进行追踪。

快速了解Angular中的onPush變更檢測策略

在这种情况下简单的解决办法使用setter然后调用markForCheck()

@Component({
  selector: &#39;app-tab&#39;,
  template: `
    {{_content}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
  _content;

  @Input() set content(value) {
    this._content = value;
    this.cdr.markForCheck();
  }

  constructor(private cdr: ChangeDetectorRef) {}

}

=== onPush++

在理解了onPush的强大之后,我们来利用它创造一个更高性能的应用。onPush组件越多,Angular需要执行的检查就越少。让我们看看你一个真是的例子:

我们又一个todos组件,它有一个todos作为input()。

@Component({
  selector: &#39;app-todos&#39;,
  template: `
     <div *ngFor="let todo of todos">
       {{todo.title}} - {{runChangeDetection}}
     </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
  @Input() todos;

  get runChangeDetection() {
    console.log(&#39;TodosComponent - Checking the view&#39;);
    return true;
  }

}
@Component({
  template: `
    <button (click)="add()">Add</button>
    <app-todos [todos]="todos"></app-todos>
  `
})
export class AppComponent {
  todos = [{ title: &#39;One&#39; }, { title: &#39;Two&#39; }];

  add() {
    this.todos = [...this.todos, { title: &#39;Three&#39; }];
  }
}

上述方法的缺点是,当我们单击添加按钮时,即使之前的数据没有任何更改,Angular也需要检查每个todo。因此第一次单击后,控制台中将显示三个日志。

在上面的示例中,只有一个表达式需要检查,但是想象一下如果是一个有多个绑定(ngIf,ngClass,表达式等)的真实组件,这将会非常耗性能。

我们白白的执行了变更检测!

更高效的方法是创建一个todo组件并将其变更检测策略定义为onPush。例如:

@Component({
  selector: &#39;app-todos&#39;,
  template: `
    <app-todo [todo]="todo" *ngFor="let todo of todos"></app-todo>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
  @Input() todos;
}

@Component({
  selector: &#39;app-todo&#39;,
  template: `{{todo.title}} {{runChangeDetection}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoComponent {
  @Input() todo;

  get runChangeDetection() {
    console.log(&#39;TodoComponent - Checking the view&#39;);
    return true;
  }

}

现在,当我们单击添加按钮时,控制台中只会看到一个日志,因为其他的todo组件的input均未更改,因此不会去检查其视图。

并且,通过创建更小粒度的组件,我们的代码变得更具可读性和可重用性。

原文链接: https://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4

原文作者:Netanel Basal

译者:淼淼

更多编程相关知识,请访问:编程视频!!

以上是快速了解Angular中的onPush變更檢測策略的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除