angular的生命周期

277 阅读8分钟

变更检测

变更检测:就是通过检测视图与状态之间的变化,在状态发生了变化之后,帮助我们更新视图。这种将视图与数据同步的机制叫做变更检测。

变更检测的触发时机:

Demo01:个计数器组件,点击按钮Count会一直加 1

@Component({
  selector: 'app-father',
  template: `
·····-*    Count:{{ num }}
  <button (click)="clickMethod()">Modify</button>
  `,
  styleUrls: ['./father.component.less'],

})
export class FatherComponent implements OnInit, DoCheck, OnChanges, AfterContentInit, AfterViewInit, AfterContentChecked, AfterViewChecked, OnDestroy {
  num:number = 0;
  ngOnInit(): void {
    console.log('Father ngOnInit');
  }
  clickMethod(){
    this.num++;
  }
}

Demo02:一个Todo List的组件,通过Http获取数据后渲染到页面

 @Component({
    selector: "app-todos",
    template: ` <li *ngFor="let item of todos">{{ item.titme }}</li> `,
  })
  export class TodosComponent implements OnInit {
    public todos: TodoItem[] = [];

    constructor(private http: HttpClient) {}

    ngOnInit() {
      this.http.get<TodoItem[]>("/api/todos").subscribe((todos: TodoItem[]) => {
        this.todos = todos;
      });
    }
  }

从上述Demo可以看出,在两种情况下触发了变更检测

  • 点击事件发生时
  • 通过http请求远程数据时

由此可以得出:只要发生了异步操作,Angular就会认为有状态可能发生了变化,然后进行变更检测。 简而言之发生以下事件之一,Angular将会触发变更检测

  • 任何浏览器事件(click、keydown等)
  • setInterval()和setTimeout()
  • Http通过XMLHttpRequest进行请求

Angular如何订阅异步事件执行变更检测?

Angular从订阅异步事件状态到触发变更检测,这中间就要看Zone.js了

Zone.j

Zone.js提供了一种称为区域(Zone)的机制,用于封装和拦截浏览器的异步活动,还提供异步生命周期的钩子统一的异步错误处理机制

Zone.js是一种信号机制,Angular 用它来检测应用程序状态何时可能已更改。它捕获异步操作,比如 setTimeout、网络请求和事件侦听器。Angular 会根据来自 Zone.js 的信号安排变更检测

简单理解就是:Angular通过Zone.js创建了一个自己的区域并称之为NgZone,Angular应用中所有的异步操作都运行在这个区域中。

变更检测是如何工作的?

Angular的核心就是组件化,组件的嵌套会使得最终形成一棵组件树。

image.png

Angular在生成组件的同时,还会为每一个组件生成一个变更检测器changeDetector,用来记录组件的数据变化状态,由于一个Component会对应一个changeDetector,所以changeDetector同样也是一个树状结构的组织。

image.png

在组件中我们可以注入changeDetectorRef来获取组件的changeDetector

@Component({
 selector: "app-todos",
 ...
})
export class TodosComponent{
 constructor(cdr: ChangeDetectorRef) {}
}

在创建一个Angular应用后,Angular会同时创建一个ApplicationRef的实例,这个实例代表的就是当前创建的这个Angular应用的实例。ApplicationRef创建的同时,会订阅NgZone中的OnMicrotaskEmpty事件,在所有的微任务完成后调用所有的视图的detectChanges()来执行变更检测。

class ApplicationRef {
  // ViewRef 是继承于 ChangeDetectorRef 的
  _views: ViewRef[] = [];
  constructor(private _zone: NgZone) {
    this._zone.onMicrotaskEmpty.subscribe({
      next: () => {
        this._zone.run(() => {
          this.tick();
        });
      },
    });
  }

  // 执行变化检测
  tick() {
    for (let view of this._views) {
      view.detectChanges();
    }
  }
}

单向数据流

什么是单项数据流? 变更检测都是从根组件开始,沿着整颗组件树从上到下的执行每个组件的变更检测,在默认情况下,直到最后一个叶子Component组件完成变更检测达到稳定状态。在这个过程中,一旦组件完成变更检测以后,在每一次事件触发变更检测之前,它的子孙组件都不允许去更改父组件的变更检测相关属性状态。

栗子:

@Component({
  selector: "app-parent",
  template: `
    {{ title }}
    <app-child></app-child>
  `, 
})
export class ParentComponent {
  title = "我的父组件";
}

@Component({
  selector: "app-child",
  template: ``, 
})
export class ChildComponent implements AfterViewInit {
  constructor(private parent: ParentComponent) {}

  ngAfterViewInit(): void {
    this.parent.title = "被修改的标题";
  }
}

image.png

出现这个错误是因为违反了单向数据流,ParentComponent完成变更检测达到稳定状态后,ChildComponent又改变了ParentComponent的数据使得ParentComponent需要再次被检查,这是不被推荐的数据处理方式。在开发模式下,Angular会进行二次检查,如果出现上述情况,二次检查会报错:ExpressionChangedAfterItHasBeenCheckedError,在生产环境中则只会执行一次。

并不是所有的生命周期去调用都会报错

@Component({
  selector: "app-child",
  template: ``, 
})
export class ChildComponent implements OnInit {
  constructor(private parent: ParentComponent) {}

  ngOnInit(): void {
    this.parent.title = "被修改的标题";
  }
}

此时代码运行正常,Angular检测执行的顺序:

  1. 更新所有子组件绑定的属性
  2. 调用所有子组件生命周期的钩子 OnChanges, OnInit, DoCheck ,AfterContentInit
  3. 更新当前组件的DOM
  4. 调用子组件的变换检测
  5. 调用所有子组件的生命周期钩子 ngAfterViewInit

ngAfterViewInit是变更检测之后执行的,在变更检测后我们更改了父组件的数据,在Angular执行开发模式下的第二次检查时,发现与上一次的值不一致,所以报错,而ngOninit的执行在变更检测之前,所以一切正常。

变更检测的性能

默认情况下,当我们的组件中的某个值发生变化触发了变更检测,那么Angular会从上往下检查所有的组件。 不过Angular对每个组件进行变更检测的速度非常快,因为使用内联缓存,在几毫秒内执行数千次检查,其中内联缓存可生成对VM友好代码。

尽管Angular进行了大量优化,在遇到大型应用,变更检测的性能仍会下降,所以需要用一些其他的方式来优化应用。

变更检测的策略

Angular提供了两种运行变更检测的策略:

  • Default
  • OnPush

Default

默认情况下,Angular使用ChangeDetectionStrategy.Default变更检测策略,每次事件触发变更检测(如用户事件、计时器、XHR、Promise等)时,此默认策略会从上到下检查组件树中的每个组件。这种对组建的依赖关系不做任何假设的保守检查方式称为脏检查,这种策略在应用组件过多时会对我们的应用产生性能的影响。

v2-3d5376bd674cab4412968175c6966fe6_b.webp

OnPush

Angular还提供了一种OnPush策略,可以修改组件装饰器的changeDetection来更改变更检测的策略。

@Component({
    selector: 'app-demo',
    // 设置变化检测的策略
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: ...
})
export class DemoComponent {
    ...
}

设置为 OnPush 策略后,Angular 每次触发变化检测后会跳过该组件和该组件的所有子组件变化检测

OnPush策略下,只有以下这几种情况才会触发组件的变更检测:

  • 输入值(@Input)更改
  • 当前组件或子组件之一触发了事件
  • 手动触发变更检测
  • 使用async管道后,observable值发生了变化

v2-a29b6219dfe43941d7668fd823afc26a_b.webp

1、输入值(@Input)更改

在默认的变更检测中,Angular将在@Input()数据发生更改或修改时执行变更检测,使用该OnPush时,传入@Input的值必须是一个新的引用才会触发变更检测。

JavaScript有两种数据类型,值类型和引用类型,值类型包括:number、string、boolean、null、undefined,引用类型包括:Object、Arrary、Function,值类型每次赋值都会分配新的空间,而引用类型比如Object,直接修改属性是引用是不会发生变化的,只有赋一个新的对象才会改变引用。

2、当前组件或子组件之一触发了事件

如果OnPush组件或其子组件之一触发事件,例如 click,则将触发变化检测(针对组件树中的所有组件)。

需要注意的是在OnPush策略中,以下操作不会触发变化检测:

  • setTimeout()
  • setInterval()
  • Promise.resolve().then()
  • this.http.get('...').subscribe()
3、手动触发变更检测

三种手动触发变更检测的方法:

  • detectChanges() : 它会触发当前组件和子组件的变化检测
  • markForCheck() :它不会触发变化检测,但是会把当前的OnPush组件和所以的父组件为OnPush的组件 标记为需要检测状态,在当前或者下一个变化检测周期进行检测
  • ApplicationRef.tick() : 它会根据组件的变化检测策略,触发整个应用程序的更改检测
4、使用async管道

内置的 AsyncPipe 订阅一个 observable 并返回它发出的最新值。 每次发出新值时的内部AsyncPipe调用 markForCheck

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

组件的生命周期

生命周期钩子分类

  • 指令与组件共有的钩子

    • ngOnChanges
    • ngOnInit
    • ngDoCheck
    • ngOnDestroy
  • 组件特有的钩子

    • ngAfterContentInit
    • ngAfterContentChecked
    • ngAfterViewInit
    • ngAfterViewChecked

image.png

Constructor: 使用简单的值对局部变量进行初始化

ngOnChanges(): 当被绑定的输入属性的值发生变化时调用(父子组件传值的时候会触发)

ngOnInit(): Angular第一次显示数据绑定和设置组件/指令的输入属性后,初始化组件或指令,对该组件进行准备

ngDoCheck(): 该函数在ngOnInit函数之后触发可以做一些自定义的操作,比如查看数据是否改变,在发生Angular无法或不愿意自己检测的变化时作出反应。

ngAfterContentInit(): 把内容投影进组件之后调用,组件渲染完成后触发,第一次ngDoCheck()之后调用,只调用一次。

ngAfterContentChecked(): 每次完成被投影组件内容的变更检测之后调用,组件初始化渲染完成后,做一些自定义操作ngAfterContentInit() 和每次ngDoCheck()之后调用。

ngAfterViewInit(): 初始化完组件视图及其子视图之后调用(dom操作放在这里),组件视图及子视图初始化完成后调用,该函数一般进行dom操作,第一次ngAfterContentChecked()之后调用,只调用一次。

ngAfterViewChecked(): 每次做完组件视图和子视图的变更检测之后调用,ngAfterViewInit函数之后做一些自定义的操作,ngAfterViewInit()和每次ngAfterContentChecked() 之后调用

ngOnDestroy(): 组件销毁时触发

参考文档: