angular 检测更新

字数 2054阅读 361

视图View

1.视图与组件的关系(View && Component)

  • 一个视图对应一个组件
  • 一个组件对应一个视图
  • 视图引用组件实例
  • 所有操作(属性检测,DOM更新)都发生在视图上,所以与其说angular是组件树,不如说其是视图树
  • 组件可以用来描述视图的高级概念

2.视图与视图容器即子视图

  • 一个视图上的元素的属性可以改变,但是在视图中的元素的结构(数量和顺序)不能够改变。改变元素结构的唯一方式是,通过 ViewContainerRef 进行 插入,移动或者移除嵌套的Views
  • 一个视图能够包含多个视图容器(View Containers)
  • 每个视图都能够通过 nodes属性与其子视图建立联系,因此能够对其子视图进行操作

3.视图状态(View State)

每个视图都有一个 state,这个状态很重要,因为angular会根据它的值来决定对视图及其子视图是否进行检测还是直接跳过检测。

与状态变化检测相关的可能的状态flags有:

  • FirstCheck
  • ChecksEnabled
  • Errored
  • Destroyed
    这些状态是可以混合设置的,比如同时设置 FirstCheckChecksEnabled flags

默认情形下,所有的视图都是开启 ChecksEnabled flag的,除非使用 ChangeDetectionStrategy.OnPush 策略。

跳过检测的情形有:

  1. ChecksEnabled 设置为了 false
  2. 视图进入 Errored 状态
  3. 视图进入 Destroyed 状态

4.ViewRef

angular有很多操作视图的高级概念,其中一个就是 ViewRef,它封装了底层的组件视图,并且有个直观的方法叫 detectChanges(检测变化)。

当发生异步事件时,angular将从最上层的ViewRef触发变化检测,检测完后再检测其子视图

ViewRef 可以通过 ChangeDetectorRef token 注入到组件构造器中

export class MyComponent {
  constructor(cd: ChangeDetectorRef) {}
}

ViewRef 的定义

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

export abstract class ViewRef extends ChangeDetectorRef {
  // ...
}

上面几个概念:

1.底层的组件视图

2.detectChange方法

class ViewRef_ implements EmbeddedViewRef<any>, InternalViewRef {
  // 内部属性
  // 1.底层的组件视图
  _view: ViewData;
  
  // ...
  
  // 2.检测变化方法
  detectChanges(): void { Services.checkAndUpdateView(this._view); }
}

3.触发检测

class ApplicationRef_ extends ApplicationRef {
  // ...
  
  // 3.触发检测
  tick(): void {
    // ...
    
    try {
        this._views.forEach((view) => view.detectChanges());
    }
  }
}

变化检测操作

运行视图变化检测的主要逻辑都在 checkAndUpdateView这个函数中。

这个函数将从宿主组件开始调用,完成之后再到其子组件中调用,依次递归,直到组件树调用完成。

当这个方法发生在某个特定视图时,会按顺序出现以下步骤:

  1. 如果视图是第一次被检测,将会设置 ViewState.firstChecktrue, 如果不是第一次检测,这个flag将设置为 false
  2. 检测和更新子组件或者子指令的输入属性input properties
  3. 更新子视图变化检测状态(这是变化检测策略实现的一部分)
  4. 对插入的视图运行变化检测 (重复list中的步骤)
  5. 如果子组件的绑定值发生变化,将调用 OnChanges 生命周期函数
  6. 调用 OnInitngDoCheck 生命周期函数 (OnInit 只在第一次检测的时候的被调用)
  7. 更新子视图组件实例中的 ContentChildren query list, 调用 checkAndUpdateQuery 函数
  8. 子组件实例调用 AfterContentInitAfterContentChecked 生命周期函数 (AfterContentInit 只在第一次检测的时候的被调用), 调用 callProviderLifecycles 函数
  9. 如果当前视图组件实例的属性发生改变,则会更新DOM插值, 可参考 The mechanics of DOM updates in Angular
  10. 对子视图运行变化检测 (重复下面步骤)
  11. 对当前组件实例更新 ViewChildren query list
  12. 对子组件实例调用 AfterViewInitAfterViewChecked 生命周期函数 (AfterViewInit 只在第一次检测的时候的被调用)
  13. 对当前视图禁用检测 (这是变化检测策略实现的一部分)

上面步骤中值得注意的地方

  1. 子组件的 OnChanges 生命周期函数 在 子视图被检测之前调用,即使子视图跳过变化检测步骤,这个钩子也会触发,这个比较重要的点
  2. 当视图检测更新时,DOM将作为变化检测机制的一部分来更新。这也意味着,如果组件没有被检测,即使存在于模板中的组件属性发生了变化,DOM也不会被更新。模板在first check之前被渲染。DOM更新实际上是插值更新。比如 <span>some {{name}}</span>, DOM元素 span在first check之前就已经渲染,在检测时, {{name}} 插值部分将被渲染。
  3. 子组件视图的状态能够在检测变化时发生改变。默认情况下, 所有组件视图的 checksEnabled 是开启的,但是如果组件检测更新策略设置为了 OnPush, 在first check之后检测更新将被禁用.(上面的步骤9)
```
# 对使用了 ChangeDetectionStrategy.OnPush 检测更新策略的组件
# 只有父组件视图绑定值发生改变,子组件才会更新
if (compView.def.flags & ViewFlags.OnPush) {
  compView.state |= ViewState.ChecksEnabled;
}
``` 
  1. 当前视图的变化检测负责开启子视图的变化检测(步骤8)
  2. 一些生命周期钩子在DOM更新(步骤3, 4, 5为DOM更新) 之前被调用, 一些生命周期钩子在DOM更新之后被调用(步骤9)

如果组件层级为 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

变化检测APIs

ChangeDetectorRef 接口定义

class ChangeDetectorRef {
  markForCheck() : void
  detach() : void
  reattach() : void
  
  detectChanges() : void
  checkNoChanges() : void
}

假设我们要禁用 AComponent 和 其子组件的检测更行,使用下面方法各自的不同

#1 组件树.png

detach

这个方法用来禁用当前视图的检测

detach(): void { this._view.state &= ~ViewState.ChecksEnabled; }

使用

export class AComponent {
  constructor(public cd: ChangeDetectorRef) {
    this.cd.detach();
}

这样A组件和其子组件都将跳过变化检测(橙色部分), 因为跳过了检测,组件模版中的DOM也不会更新

#2 禁用更新检测.png

示例:

@Component({
  selector: 'a-comp',
  template: `<span>See if I change: {{changed}}</span>`
})
export class AComponent {
  constructor(public cd: ChangeDetectorRef) { // 注入ChangeDetectorRef服务 
    this.changed = 'false';

    setTimeout(() => {
      this.cd.detach();            // 禁用变化检测
      this.changed = 'true';
    }, 2000);
  }

组件模版在first check之后渲染结果为 See if I change: false,2秒后, changed 值变化为 true, 但是因为跳过了变化检测,DOM中的插值并不会更新

reattach

设置 ViewState.ChecksEnabled的值

reattach(): void { this._view.state |= ViewState.ChecksEnabled; }

如果AComponent的输入值属性(假设为 name)发生了变化, OnChanges 钩子函数将调用。因此一旦输入属性发生了变化,我们可以在 OnChanges 钩子函数中重新激活变化检测,然后在下一次的时候再detach

// 子组件
@Component({
  selector: 'a-comp',
  template: `<span>See if I change: {{changed}}</span>`
})
export class AComponent {
  @Input() name;  // 输入属性
  constructor(public cd: ChangeDetectorRef) { // 注入ChangeDetectorRef服务 
    this.cd.detach();  // 禁用变化检测
  }
  
  OnChanges(values): void {
    this.cd.reattach();  // 激活变化检测
    
    setTimeout(() => { // 下一次的时候再禁用变化检测
      this.cd.detach();
    })
  }
}  

// 父组件
@Component({
  selector: 'app-comp',
  template: `<a-comp [name]="yourName"></a-comp><button (click)="changeName()">更新name</button>`
})
export class AppComponent {
  yourName = 'james';
  
  changeName() {
    this.yourName = 'kobe';
  }
}

当我们点击 '更新name' 按钮时, AComponent中的输入属性 name 将发生变化,此时会调用OnChanges 钩子函数,从而激活变化检测,因此DOM会产生更新.

上面示例的形式,等同于将组件的更新策略设置为 ChangeDetectionStrategy.OnPush: 在第一次变化检测更新运行之后,禁用检测更新,但父组件绑定属性发生变化时,再开启检测更新,检测更新完成之后再次禁用掉检测更新。

注意:

  • OnChanges只在禁用分支的最顶层组件(此处是AComponent)触发,而不是禁用分支中的所有组件。

markForCheck

reattach 只对当前组件开启检测,但是如果其父组件没有开启更新检测,则还是没有效果。

我们需要一个方法来开启所有父组件中检测更新,这个方法就是 markForCheck

# 可以看出这个方法向上迭代开启检测更新
let currView: ViewData|null = view;
while (currView) {
  if (currView.def.flags & ViewFlags.OnPush) {
    currView.state |= ViewState.ChecksEnabled;
  }
  currView = currView.viewContainerParent || currView.parent;
}

ngOnChangesngDoCheck 这2个钩子函数即使在使用 OnPush 更新策略时也会触发,同样它们只在禁用分支的最顶层触发,而不是所有组件中都会触发。我们可以实现一些自定义逻辑(个人感觉和react的 shouldComponentUpdate 方法很像)。

因为angular只检测对象引用,我们可以实现对象属性的脏值检查

import { Component, ChangeDetectorRef, Input, ChangeDetectionStrategy, OnInit, DoCheck } from '@angular/core';

@Component({
  selector: 'a-comp',
  template: `<span>数量 {{prevLength}}</span>`,
  changeDetection: ChangeDetectionStrategy.OnPush // 更新策略
})
export class AComponent implements OnInit, DoCheck {
  @Input() items;
  prevLength: number;
  constructor(public cd: ChangeDetectorRef) {}

  ngOnInit(): void {
    this.prevLength = this.items.length;
  }

  ngDoCheck() {
    if (this.items.length !== this.prevLength) { // 只有当items数量变化时才更新
      
      this.cd.markForCheck();  // 检测父组件和自身以及子组件
      this.prevLength = this.items.length;
    }
  }

}

# 父组件
import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <button (click)="addItem()">加item</button>
    <a-comp [items]="items"></a-comp>
  `,
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

  items = ['a', 'b', 'c']


  addItem() {
    this.items.push('d');
  }
}

detectChanges

对当前组件及其子组件都运行检测。即使组件是否检测状态是关闭的,也会运行检测。

感觉这个和angularjs1.x中的 $apply | $digest 方法很像。

export class AComponent {
  @Input() inputAProp;

  constructor(public cd: ChangeDetectorRef) {
    this.cd.detach();
  }

  ngOnChanges(values) {
    this.cd.detectChanges();
}

checkNoChanges

这个用于确保当前更新检测中不发生任何变化,如果绑定属性发生变化或者DOM将更新,都会抛出错误。

基本上走上面 1, 7, 8 步骤。

markForCheck 和 detectChanges 区别

参考:

本文来源:

推荐阅读更多精彩内容