揭秘 Angular 生命周期函数

544 阅读4分钟

当 Angular 实例化组件类并渲染组件视图及其子视图时,组件实例的生命周期就开始了。我们的应用可以使用生命周期钩子方法来触发组件生命周期中的关键事件,Angular 会按以下顺序执行钩子方法:

  1. ngOnChanges()

  2. ngOnInit()

  3. ngDoCheck()

  4. ngAfterContentInit()

  5. ngAfterContentChecked()

  6. ngAfterViewInit()

  7. ngAfterViewChecked()

  8. ngOnDestroy()

首先我们编写一个简单的 Angular 应用,看看实际执行的效果是怎样的。

export class AppComponent implements xxx {  title = 'zuckjet';  ngOnChanges() {    console.log('onChanges');  }  ngOnInit() {    console.log('onInit');  }  ngDoCheck() {    console.log('doCheck');  }  ngAfterContentInit() {    console.log('afterContentInit');  }  ngAfterContentChecked() {    console.log('afterContentChecked');  }  ngAfterViewInit() {    console.log('afterViewInit');  }  ngAfterViewChecked() {    console.log('afterViewChecked');  }  ngOnDestroy() {    console.log('onDestroy')  }}

开发模式下,控制台输出如下:

onInit
doCheck
afterContentInit
afterContentChecked
afterViewInit
afterViewChecked
Angular is running in development mode...
doCheckafter
ContentChecked
afterViewChecked

从上面的输出结果中,我们提出几个疑问:

  1. 这些生命周期函数是如何被执行的

  2. 为什么一些函数被执行了两次

  3. ngOnChanges 和 ngOnDestroy 函数为什么没有被执行

弄清楚了这些问题,也就弄懂了 Angular 生命周期函数的背后的原理,现在我们来逐个揭晓问题的答案吧。

Angular 生命周期函数主要是由 refreshView 函数触发执行,其部分代码如下:

function refreshView(tView, lView, templateFn, context) {
  ...
  if (!isInCheckNoChangesPass) {
    const preOrderHooks = tView.preOrderHooks;
    if (preOrderHooks !== null) {
        // 关键代码1
        executeInitAndCheckHooks(lView, preOrderHooks, 0, null);
    }
  }
  ...
  if (!isInCheckNoChangesPass) {
    const contentHooks = tView.contentHooks;
    if (contentHooks !== null) {
      // 关键代码2
      executeInitAndCheckHooks(lView, contentHooks, 1);
    }
    incrementInitPhaseFlags(lView, 1);
  }
  ...
  if (!isInCheckNoChangesPass) {
  const viewHooks = tView.viewHooks;
  if (viewHooks !== null) {
      // 关键代码3
      executeInitAndCheckHooks(lView, viewHooks, 2);
  }
}
}

关键代码1标记的函数 executeInitAndCheckHooks,通过名字不难看出它是执行初始化和检查的钩子,具体执行哪些生命周期函数由参数 preOrderHooks 决定。而 preOrderHooks 又是从 tView.preOrderHooks 得到,那么在接口 TView 中这个属性的含义是什么呢?

Array of ngOnInit, ngOnChanges and ngDoCheck hooks that should be executed for this view in creation mode.

现在我们可以解释控制台输出结果中,前两项为何是 onInit 和 doCheck 了。继续往下看,在关键代码2处,依旧是执行 executeInitAndCheckHooks 函数,不同的是传入的关键参数是 contentHooks,这个参数在接口 TView 中的含义如下:

Array of ngAfterContentInit and ngAfterContentChecked hooks that should be executed for this view in creation mode.

因此控制台的输出结果中,紧接着便是 afterContentInit 和 afterContentChecked。接着往下分析,在关键代码3处,执行的还是 executeInitAndCheckHooks 函数,传入的关键参数是 viewHooks,这个参数在接口 TView 中的含义是:

_Array of ngAfterViewInit and ngAfterViewChecked hooks that should be executed for this view in creation mod_e.

分析到这里,第一个问题,即”这些生命周期函数是如何被执行的“便解决了。那么第二个问题,doCheck 之类的生命周期函数为什么会被执行两次了呢?我们来继续追根溯源:

上文中重点讨论的函数 refreshView,它是由类 ApplicationRef 的 tick 方法调用:

tick() {
  for (let view of this._views) {
    view.detectChanges();
  }
  if (typeof ngDevMode === 'undefined' || ngDevMode) {
    for (let view of this._views) {
      view.checkNoChanges();
    }
  }
}

我们可以先给出结论:部分生命周期函数被执行两次,这种情况仅仅在开发模式下才会发生,在生产模式下只会执行一次。无论是生产模式还是开发模式,tick 函数都会被执行两次,只不过仅仅在开发模式下,tick 函数体中的 checkNoChanges 函数才会被执行,为何只有部分函数被执行两次,这在 refreshView 函数中有说明:

 if (!isInCheckNoChangesPass) {
    if (hooksInitPhaseCompleted) {
      // execute lifecycle hooks
    }
 }

在 refreshView 函数中,很多生命周期函数钩子都会在上述条件语句内执行。通过 isInCheckNoChangesPass 和 hooksInitPhaseCompleted 变量的控制,就能实现在后续执行 refreshView 函数时只触发部分钩子函数。

此外 tick 函数中的 checkNoChanges 函数值得关注一下,因为在 Angular 应用开发开发中,一个极其常见的错误便是因为它的执行而检测出来的。我们稍微修改一下 AppComponent 组件代码,来重现这个经典错误:

ngAfterViewInit() {
    console.log('afterViewInit');
    this.title = 'ZhuYuJie';
}

我们在生命周期函数 afterViewInit 中重新给变量赋值,此时开发模式下控制台会报错: Expression has changed after it was checked. Previous value: 'zuckjet'. Current value: 'ZhuYuJie'..

具体原因参见:官网指南,因篇幅限制不在此文展开。

最后一个问题, ngOnChanges 和 ngOnDestroy 函数为什么没有被执行呢?这个看官网指南就知道,ngOnDestroy 在组件销毁时被执行,而 ngOnChanges 则是在 input 值有变化时执行。

至此 Angular 生命周期函数的讲解就结束了,欢迎关注我的个人微信公众号【朱玉洁的博客】,后续将带来更多前端知识分享。