更新于2021.11.17
version:12+


Angular变更检测分为默认的变更检测策略(CheckAlways)和OnPush变更检测策略(CheckOnce),对于变更检测,网上的资源只是片面的对其进行解释,下面对变更策略进行详细的分析(Angular的变更检测是以深度优先为基础,遇到兄弟组件优先兄弟组件,然后对组件树进行单向的检测并刷新页面数据).

对于Angular而言每个组件都有一个对应的视图View,视图与组件有直接的关联,而每个视图都有一个通过nodes属性链接到其子视图的链接,因此可以对子视图执行操作


视图状态

对于每个视图而言都有一个状态State,当视图状态ChecksEnabledfalse或处于Errored/Destroyed状态下,将跳过更改检测.默认视图的状态ChecksEnabledtrue,但当处于OnPush策略下时,视图状态将ChecksEnabled将变为false,OnPus策略会影响当前组件及其所有子组件;只有当input属性发生变化时,才会重新对视图进行变更检测

//OnPush策略下更改视图状态
if (view.def.flags & ViewFlags.OnPush) {
view.state &= ~ViewState.ChecksEnabled;
}

//视图状态
export const enum ViewState {
BeforeFirstCheck = 1 << 0,
FirstCheck = 1 << 1,
Attached = 1 << 2,
ChecksEnabled = 1 << 3,
IsProjectedView = 1 << 4,
CheckProjectedView = 1 << 5,
CheckProjectedViews = 1 << 6,
Destroyed = 1 << 7,
}

而Angulr将这一概念称为ViewRef,它封装了基础组件视图,并且有一个我们所熟知的名称 detectChanges ,通过构造函数我们可以了解到detectChanges下有以下几个方法

export class AppComponent {
constructor(cd: ChangeDetectorRef) { ... }


export declare abstract class ChangeDetectorRef {
abstract checkNoChanges(): void;
abstract detach(): void;
abstract detectChanges(): void;
abstract markForCheck(): void;
abstract reattach(): void;
}

变更检测操作

下面就是我们整个检测操作流程

  1. sets ViewState.firstCheck to true if a view is checked for the first time and to false if it was already checked before
  2. checks and updates input properties on a child component/directive instance
  3. updates child view change detection state (part of change detection strategy implementation)
  4. runs change detection for the embedded views (repeats the steps in the list)
  5. calls OnChanges lifecycle hook on a child component if bindings changed
  6. calls OnInit and ngDoCheck on a child component (OnInit is called only during first check)
  7. updates ContentChildren query list on a child view component instance
  8. calls AfterContentInit and AfterContentChecked lifecycle hooks on child component instance (AfterContentInit is called only during first check)
  9. updates DOM interpolations for the current view if properties on current view component instance changed
  10. runs change detection for a child view (repeats the steps in this list)
  11. updates ViewChildren query list on the current view component instance
  12. calls AfterViewInit and AfterViewChecked lifecycle hooks on child component instance (AfterViewInit is called only during first check)
  13. disables checks for the current view (part of change detection strategy implementation)

通过对流程的梳理,我们知道视图的状态在我们整个变更检测中起着重要的所有.

假如有A>>B>>C三个组件按照这个层级结构,他们钩子的调用顺序如下

A: AfterContentInit
A: AfterContentChecked
A: Update bindings
B: AfterContentInit
B: AfterContentChecked
B: Update bindings
C: AfterContentInit
C: AfterContentChecked
C: Update bindings
C: AfterViewInit
C: AfterViewChecked
B: AfterViewInit
B: AfterViewChecked
A: AfterViewInit
A: AfterViewChecked

探索

回头我们再来看我们所熟知的一些知识点,就发现所有这些方法的调用都只是在更改我们视图的状态,

  1. detach()

    this._view.state &= ~ViewState.Attached;
  2. reattach()

    this._view.state |= ViewState.Attached;
  3. markForCheck():

    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;
    }
    }
  4. detectChanges():这里与detach()可以局部刷新的原理就是首先将组件脱离视图,避免脏检查,同时在需要的地方调用detectChanges(),翻看源码发现detectChanges调用了视图的基础方法checkAndUpdateView()来更新数据

  5. checkNoChanges()开发时使用,不过多解释

默认检测策略下的触发时机

  • 事件:页面内的一些列事件click、submit、mouse、down等等
  • 组件@Input()参数的变化(值引用)
  • setTimeout()、setInterval()
  • Observable


OnPush策略下的触发时机

  • 事件:页面内的一些列事件click、submit、mouse、down等等
  • Observable 但是需要设置 Async pipe
  • 组件@Input()参数的变化(值引用)
  • 手动使用ChangeDetectorRef.detectChanges()、ChangeDetectorRef.markForCheck()、ApplicationRef.tick()方法


在Angular源码中看到变化检测对象ChangeDetectorStatus有如下几种状态:

CheckOnce:表示只检查一次,调用detectChanges之后状态将会变为Checked
Checked:表示在状态变为CheckOnce之前会跳过所有检测
CheckAlways:表示总是接受变化检测,每次调用detectChanges后状态还是CheckAlways
Detached:代表变化检测对象脱离了变化检测对象树,不再进行变化检测
Errored:表述变化测试对象发生错误,变化检测实效
Destroyed:表示变化检测对象已经被销毁


应用

  1. 将组件设置为OnPush模式、markForCheck()和非纯管道async组合的形式优化性能
  2. 将组件脱离文档流detach(),并调用detectChanges()来进行局部的变更检测
  3. 局部代码通过zone.js来实现在合适的时机进行变更检测

    this.ngZone.runOutsideAngular(() => {
    this._sub = Observable.timer(1000, 1000)
    .subscribe(i => this.ngZone.run(() => {
    this.content = "Loaded! " + i;
    }));
    });