从 constructor() 调用的 setTimeout(fn, 0) 中的 fn 的执行是否确保在 Angular 生命周期钩子 ngOnInit 和 ngAfterViewInit 之后发生?

问题描述 投票:0回答:1

我有以下 Angular2“伪”代码,我想让用户类尽可能简单:

// ###### LIB CLASSES ######

@Component()
export abstract class Parent {
  abstract childProvidedFactory;
  factory_config;
  
  ngOnInit() {
    this.dataSource = new FactoryUser(this.childProvidedFactory, this.factory_config);
  }

  // this.dataSource initialized and may be used further
}

export class FactoryUser {
  constructor (childProvidedFactory, factory_config) {
  }

  // ...
}



// ###### USER CLASS ######

@Component()
export class Child extends Parent {
  @Input() config;

  childProvidedFactory = () => {
    // implementation
  }

  ngOnInit() {
    this.factory_config = fnc(this.config);
    super.ngOnInit();
  }
}

我希望编写

Child
的用户不需要调用
super.ngOnInit()
。所以想法是将
Parent
ngOnInit
内容移动到构造函数。然而,在
constructor
执行时,所需的
childProvidedFactory
factory_config
尚不可用。因此我用
setTimeout
:

包装代码
export abstract class Parent {
  abstract childProvidedFactory;
  factory_config;
  
  constructor() {
    setTimeout(() => {
      this.dataSource = new FactoryUser(this.childProvidedFactory, this.factory_config);
    }, 0)
  }

  // this.dataSource initialized and may be used further
}

现在我可以从

super.ngOnInit()
类中删除
Child

在 Chromium 上,当使用

console.log
s 进行检测时,我得到:

Inside child's ngOnInit()
Inside child's ngAfterViewInit()
Inside parent's setTimeout() within constructor()

是否确保

setTimeout
的构造函数中的
Parent
将在
Child
ngOnInit
之后分别在
Child
ngAfterViewInit
生命周期挂钩之后执行?
如果是这样,您能解释一下原因吗?生命周期钩子是否在事件循环中与 setTimeout 相关“安排”?

代码将仅在主要 Web 引擎(WebKit、Blink、Gecko)和 Angular >= 17 上运行。

您还发现其他潜在问题吗?


重点是避免在 Child 中调用 super.ngOnInit() 。所以还有其他潜在的解决方案:

this 解决方案中,我不喜欢 Child 需要使用特殊名称而不是 ngOnInit。

最接近我的问题是this,但是构造函数没有添加到混合中。

我考虑使用一次性初始化,并在构造函数中调用 afterNextRender() 。从技术上讲它是有效的,但是由于在回调 fn 中初始化的变量在 Childs 模板中使用,因此出现运行时错误

Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError
。因此,它实际上获得了一个值,而不是
undefined
。使用此选项,需要将
ChangeDetectorRef
注入到抽象类的构造函数中,因此,如果我没有记错的话,
Child
实际上需要将其传递给该构造函数。这又会不必要地使
Child
的代码变得复杂。

angular typescript constructor settimeout angular-lifecycle-hooks
1个回答
0
投票

你尝试的方法确实不可靠,会导致难以调试的时序问题。

要解决这个问题,您可以使用装饰器,它将调用父级

ngOnInit
,它必须是不同的名称,因为我们无法在构造函数内访问 super。

装饰器

export function CallParentHook() {
  return function (constructor: any) {
    const original = constructor.prototype.ngOnInit;

    constructor.prototype.ngOnInit = function () {
      this.parentNgOnInit();
      if (original && typeof original === 'function') {
        original.apply(this, arguments);
      }
    };
  };
}

孩子

@Component({
  selector: 'app-child',
  standalone: true,
  imports: [JsonPipe],
  template: `{{ dataSource | json }}`,
})
@CallParentHook()
export class Child extends Parent {
  @Input() override config: any;

  childProvidedFactory = () => {};

  ngOnInit() {
    console.log(this.dataSource);
  }
}

完整代码:

import { JsonPipe } from '@angular/common';
import { Component, Directive, Input } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';
export const fnc = (data: any) => data;
export class FactoryUser {
  config: any;
  constructor(childProvidedFactory: any, factory_config: any) {
    this.config = factory_config;
  }

  // ...
}

export function CallParentHook() {
  return function (constructor: any) {
    const original = constructor.prototype.ngOnInit;

    constructor.prototype.ngOnInit = function () {
      this.parentNgOnInit();
      if (original && typeof original === 'function') {
        original.apply(this, arguments);
      }
    };
  };
}

@Directive()
export abstract class Parent {
  @Input() config: any;
  abstract childProvidedFactory: any;
  dataSource: any;

  parentNgOnInit() {
    this.dataSource = new FactoryUser(this.childProvidedFactory, this.config);
  }
}

@Component({
  selector: 'app-child',
  standalone: true,
  imports: [JsonPipe],
  template: `{{ dataSource | json }}`,
})
@CallParentHook()
export class Child extends Parent {
  @Input() override config: any;

  childProvidedFactory = () => {};

  ngOnInit() {
    console.log(this.dataSource);
  }
}

@Component({
  selector: 'app-root',
  imports: [Child],
  standalone: true,
  template: `
  <app-child [config]="config"/>
  asd
  `,
})
export class App {
  config = { name: 'Angular' };
}

bootstrapApplication(App);

Stackblitz 演示

© www.soinside.com 2019 - 2024. All rights reserved.