构造函数和ngOnInit之间的区别

1477

Angular默认提供生命周期钩子ngOnInit

如果我们已经有了constructor,为什么还应该使用ngOnInit

27个回答

1503
构造函数是类的默认方法,在实例化类时执行,并确保类及其子类中的字段得到正确初始化。Angular,或更好的依赖注入器(DI),分析构造函数参数,并在通过调用new MyClass()创建新实例时尝试查找与构造函数参数类型匹配的提供程序,解决它们并将它们传递给构造函数。
new MyClass(someArg);

ngOnInit 是 Angular 调用的生命周期钩子,用于指示 Angular 完成了组件的创建。

我们必须像这样导入 OnInit 才能使用它(实际上实现 OnInit 不是强制性的,但被认为是良好的实践):

import { Component, OnInit } from '@angular/core';

然后要使用OnInit方法,我们需要像这样实现类:

export class App implements OnInit {
  constructor() {
     // Called first time before the ngOnInit()
  }

  ngOnInit() {
     // Called after the constructor and called  after the first ngOnChanges() 
  }
}

实现此接口以在指令的数据绑定属性初始化后执行自定义初始化逻辑。 ngOnInit在指令的数据绑定属性首次检查后立即调用,而在其任何子级被检查之前。 它仅在实例化指令时调用一次。
大多数情况下,我们使用ngOnInit进行所有的初始化/声明,并避免在构造函数中进行操作。构造函数仅应用于初始化类成员,但不应执行实际的“工作”。
因此,在设置依赖注入方面应该使用constructor(),不要做太多其他事情。ngOnInit()更适合“开始”——这是组件绑定解析的位置和时间。
更多信息请参考: 需要注意的是,在构造函数中无法访问@Input值(感谢@tim在评论中的建议)。

81
大多数(甚至全部)基于类的编程语言都有构造函数,以确保正确的初始化顺序,特别是在扩展其他类的类中可能会出现一些相当棘手的问题,比如final字段(不知道TS是否有此功能)等。构造函数与Angular2无关,它们是TypeScript的一个特性。生命周期钩子由Angular在某些初始化完成或发生某些事件后调用,以允许组件对特定情况做出反应,并为其提供在适当时间执行某些任务的机会。 - Günter Zöchbauer
25
在 https://angular.io/docs/ts/latest/guide/server-communication.html 中有一段引用来解释这个问题:"当组件构造函数简单且所有真正的工作(尤其是调用远程服务器)都在一个单独的方法中处理时,组件更易于测试和调试。" - 在这种情况下,该方法就是 ngOnInit()。 - yoonjesung
35
与所有“最佳实践”一样,我认为解释为什么不应该在构造函数中进行“工作”也是个好主意。Angular团队领导的这篇文章可能会有所帮助: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/此外,我们应该将更少的重要性放在实现OnInit所需的咒语上(这很容易找到),而更多地关注这个关键事实,即数据绑定在构造函数中不可用。 - Reikim
10
如果 tsconfig.json 文件中设置了 "strict": true 严格模式,那么你必须在构造函数中初始化类成员,而不是在 ngOnit(如 FormGroup)中。 - Rohit Sharma
9
请注意,@Input 值在构造函数中是无法访问的。 - Tim
显示剩余10条评论

271
本文Angular中构造函数和ngOnInit之间的本质区别从多个角度探讨了它们的区别。本答案提供了与组件初始化过程相关的最重要的区别解释,同时展示了它们在使用上的不同之处。
Angular的引导过程包括两个主要阶段:
1. 构建组件树 2. 运行变更检测
当Angular构建组件树时,会调用组件的构造函数。所有的生命周期钩子都是作为运行变更检测的一部分而被调用的。
当Angular构建组件树时,根模块注入器已经配置好了,所以你可以注入任何全局依赖。此外,当Angular实例化一个子组件类时,父组件的注入器也已经设置好了,所以你可以注入在父组件上定义的提供者,包括父组件本身。组件的构造函数是在注入器的上下文中调用的唯一方法,所以如果你需要任何依赖项,那就只能在那里获取这些依赖项。
当Angular开始进行变更检测时,组件树被构建,并且树中所有组件的构造函数已被调用。此外,每个组件的模板节点都被添加到DOM中。在变更检测期间,会处理@Input通信机制,因此您不能期望在构造函数中使用这些属性。它们将在ngOnInit之后可用。
让我们看一个快速示例。假设您有以下模板:
<my-app>
   <child-comp [i]='prop'>

所以Angular开始引导应用程序。正如我所说的,它首先为每个组件创建类。因此,它调用MyAppComponent构造函数。它还创建了一个DOM节点,该节点是my-app组件的宿主元素。然后,它继续为child-comp创建一个宿主元素,并调用ChildComponent构造函数。在这个阶段,它并不真正关心i输入绑定和任何生命周期钩子。因此,当这个过程完成时,Angular得到了以下组件视图树:
MyAppView
  - MyApp component instance
  - my-app host element data
       ChildComponentView
         - ChildComponent component instance
         - child-comp host element data  

只有在这之后,变更检测才会运行并更新my-app的绑定,并在MyAppComponent类上调用ngOnInit。然后,它继续更新child-comp的绑定,并在ChildComponent类上调用ngOnInit
您可以根据需要在构造函数或ngOnInit中执行初始化逻辑。例如,文章Here is how to get ViewContainerRef before @ViewChild query is evaluated展示了在构造函数中可能需要执行的初始化逻辑类型。
以下是一些文章,可以帮助您更好地理解这个主题:

63
应该接受这个答案。它实际上解释了为什么,而不是重复口头禅并声明“构造函数只应用于注入依赖项”。 - Stavm
1
@yannick1976,谢谢!请查看引用的文章。 - Max Koretskyi
请纠正我如果我错了。我理解组件树是先构建,然后再进行更改检测过程。你写道首先调用AppComponent构造函数(以及已解决的依赖项),然后调用ChildComponent构造函数(以及依赖项),然后是AppComponent的输入绑定,最后调用OnInit。但我的疑虑是,如果我在两个组件中添加生命周期钩子,流程是AppComponentConstructor->AppComponentOnInit->ChildComponentConstructor->ChildComponentOnInit。为什么AppComponentOnInit在ChildComponentConstructor之前被调用? - user2485435
@user2485435,我理解组件树首先被构建,然后才进行变更检测过程 - 是的,没错。为什么AppComponentOnInit在ChildComponentConstructor之前被调用了呢?这不应该是这样的,你有演示吗? - Max Koretskyi
1
@MaxKoretskyiakaWizard,你是对的。我在我的应用程序设置中犯了一些错误,现在已经按你所描述的工作了。 https://angular-c7zjsx.stackblitz.io/ - user2485435
显示剩余4条评论

133

好的,首先ngOnInitAngular生命周期的一部分,而constructorES6JavaScript类的一部分,因此主要区别从这里开始!...

看一下我创建的下面的图表,它展示了Angular的生命周期。

ngOnInit vs constructor

在Angular2+中,我们使用constructor来为我们执行DI(Dependency Injection),而在Angular 1中,通过调用String方法并检查注入了哪个依赖项来完成。

如您在上面的图表中看到的,ngOnInit发生在构造函数准备就绪之后,ngOnChnages在组件准备就绪后触发。所有初始化都可以在此阶段进行,一个简单的示例是注入服务并在init上对其进行初始化。

好的,我还为您分享了一个样本代码,看看我们如何在下面的代码中使用ngOnInitconstructor

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';


@Component({
 selector: 'my-app',
 template: `<h1>App is running!</h1>
  <my-app-main [data]=data></<my-app-main>`,
  styles: ['h1 { font-weight: normal; }']
})
class ExampleComponent implements OnInit {
  constructor(private router: Router) {} //Dependency injection in the constructor
  
  // ngOnInit, get called after Component initialised! 
  ngOnInit() {
    console.log('Component initialised!');
  }
}

1
谢谢。这应该是最好的答案。 - Don Dilanga
非常感谢您提供这个出色的图表。它对我们帮助很大!! - Dan Ortega
@Alireza,我们可以在同一组件中定义的styles:[]中添加数组吗? - cracker

109

我认为最好的例子就是使用服务。比如说,当我的组件被“激活”时,我想从服务器上获取数据。假设我还希望在从服务器获取数据后对其进行一些其他操作,比如遇到错误时以不同方式记录日志。

使用ngOnInit比构造函数更容易实现这一点,而且也限制了我需要添加多少个回调层级到应用程序中。

例如:

export class Users implements OnInit{

    user_list: Array<any>;

    constructor(private _userService: UserService){
    };

    ngOnInit(){
        this.getUsers();
    };

    getUsers(){
        this._userService.getUsersFromService().subscribe(users =>  this.user_list = users);
    };


}

使用构造函数,我可以调用我的用户服务并填充用户列表,但也许我想对其进行一些额外操作。就像确保所有内容都是大写的,我不确定我的数据究竟是怎样的。

因此,使用ngOnInit会使事情变得更加容易。

export class Users implements OnInit{

    user_list: Array<any>;

    constructor(private _userService: UserService){
    };

    ngOnInit(){
        this.getUsers();
    };

    getUsers(){
        this._userService.getUsersFromService().subscribe(users =>  this.user_list = users);
        this.user_list.toUpperCase();
    };


}

这使得查看变得更加容易,因此我在初始化组件时仅调用我的函数,而不必到其他地方进行查找。这只是另一个工具,可以让您将来更轻松地阅读和使用。此外,我发现在构造函数中放置函数调用实在是一个糟糕的做法!


如果你将user_list设置为Observable,那么你的示例就可以简化了。Angular2有async管道,所以那里不会有任何问题。 - DarkNeuron
1
@Morgan,只是为了我学习一个小东西,你为什么要先创建一个函数 getUsers 然后再将其插入到 ngOnInit 中呢?直接在 ngOnInit 中编写不是更少代码吗?我只是想知道人们为什么这样做?是为了以后可以重复使用代码吗?谢谢。 - Alfa Bravo
40
如下回答所示,无论是否在构造函数中,这都没有任何区别。这并不是真正的回答。 - Jimmy Kane
10
我不明白这个回答与问题有什么关系。 为什么不能将代码放在“构造函数”中? - CodyBugstein
4
@Morgan 为什么你不能简单地这样写: constructor(private _userService: UserService){ this.getUsers(); }; - Ashley
显示剩余3条评论

82

在上面的解释中有一件重要的事情被省略了,它解释了什么时候你必须使用ngOnInit

如果您通过例如 ViewChildrenContentChildrenElementRef 操作组件的 DOM,那么在构造函数阶段期间您的原生元素将不可用。

但是,由于ngOnInit 在组件创建后并且检查(ngOnChanges)已被调用之后发生,因此您可以在此时访问 DOM。

export class App implements OnInit, AfterViewInit, AfterContentInit {
  @Input() myInput: string;
  @ViewChild() myTemplate: TemplateRef<any>;
  @ContentChild(ChildComponent) myComponent: ChildComponent; 

  constructor(private elementRef: ElementRef) {
     // this.elementRef.nativeElement is undefined here
     // this.myInput is undefined here
     // this.myTemplate is undefined here
     // this.myComponent is undefine here
  }

  ngOnInit() {
     // this.elementRef.nativeElement can be used from here on
     // value of this.myInput is passed from parent scope
     // this.myTemplate and this.myComponent are still undefined
  }
  ngAfterContentInit() {
     // this.myComponent now gets projected in and can be accessed
     // this.myTemplate is still undefined
  }

  ngAfterViewInit() {
     // this.myTemplate can be used now as well
  }
}

7
不行。特别是对于 @ViewChildren,你需要使用 ngAfterViewInit 方法。请参见: https://dev59.com/ZFYO5IYBdhLWcg3wHuIA?rq=1 - AsGoodAsItGets
1
谢谢@AsGoodAsItGets指出。我现在已经改进了答案。 - Miroslav Jonas
1
这是最美丽的回应。我们学习Angular生命周期钩子的原因主要归结于这些用例。 - N. Raj
很高兴你觉得有用,@N.Raj - Miroslav Jonas
elementRefзҡ„APIжңүж”№еҸҳеҗ—пјҹжҲ‘е·Із»ҸжөӢиҜ•иҝҮпјҢеңЁжһ„йҖ еҮҪж•°дёӯthis.elementRef.nativeElementдёҚжҳҜжңӘе®ҡд№үзҡ„гҖӮ - bytrangle
很有可能。这个答案是在4-5年前发布的,在整个常春藤重构之前。 - Miroslav Jonas

71

第一个(构造函数)与类实例化有关,与Angular2无关。我的意思是,构造函数可以用于任何类。您可以在其中放置一些新创建实例的初始化处理。

第二个对应于Angular2组件的生命周期钩子:

引自官方Angular网站:

  • ngOnChanges在输入或输出绑定值更改时调用
  • ngOnInit在第一个ngOnChanges之后调用

因此,如果初始化处理依赖于组件绑定(例如使用@Input定义的组件参数),则应使用ngOnInit,否则构造函数就足够了...


47

简短而简单的答案是:

构造函数: 构造函数 是一个默认方法,在组件被构造时(默认情况下)运行。当您创建一个类的实例时,也会调用构造函数(默认方法)。因此,换句话说,在组件被构建或/和实例化时,将调用构造函数(默认方法),并且写入其中的相关代码被称为。基本上,在Angular2中,它用于注入像服务这样的东西,以便在组件被构造时进行进一步使用。

OnInit: OnInit是组件的生命周期钩子,它在构造函数(default method)之后第一次运行,当组件正在初始化时。

因此,您的构造函数将首先被调用,然后才会调用Oninit方法。

boot.ts

import {Cmomponent, OnInit} from 'angular2/core';
import {ExternalService} from '../externalService';

export class app implements OnInit{
   constructor(myService:ExternalService)
   {
           this.myService=myService;
   }

   ngOnInit(){
     // this.myService.someMethod() 
   }
}

资源: 生命周期钩子

您可以查看这个小演示,它展示了两者的实现。


7
我认为“构造函数是组件初始化时运行或调用的内容”这一说法有误导性。构造函数是类的特征,而不是组件的特征。我会说,在构造函数被调用并且Angular完成初始化之后,该类的实例才成为组件。 - Günter Zöchbauer
是的,我修改了这个语句,你现在可以检查一下。 - micronyks
1
嗯,依我之见,“构造函数(默认方法)是在组件构建时运行或调用的东西”仍然是一样的。它不仅在组件构建时被调用,还在服务或执行类似于new MyClass()的代码时被调用。我认为说构造函数与组件有关是具有误导性的,它们与类和初始化这些类的实例有关。组件只是这样一个类。否则,我认为这是一个很好的答案。 - Günter Zöchbauer
2
是的,绝对正确。忘了提到当你创建一个类的对象时,constructor也会被调用。但是这个答案是在angular2上下文中编写的。要知道最好的答案,你必须了解面向对象编程的基础知识。我仍然会更新答案。 - micronyks
@GünterZöchbauer,我认为“是类的特征而不是组件的特征”这个说法并不正确。从编程语言的角度来看,是的,这是正确的。但是,我可以成功地使用没有任何生命周期钩子的组件。但是,如果需要 DI,我无法在没有构造函数的情况下使用组件,因为那是唯一可注入的位置。请参见我的回答 - Max Koretskyi

36

构造函数和ngOnInit的主要区别在于,ngOnInit是一个生命周期钩子,并且在构造函数之后运行。组件插值模板和输入初始值在构造函数中不可用,但它们在ngOnInit中是可用的。

实际上,ngOnInit如何影响代码结构是最���的区别。大多数初始化代码可以移动到ngOnInit中 - 只要这不会创建竞争条件

构造函数反模式

大量的初始化代码会使构造函数方法难以扩展、阅读和测试。

将初始化逻辑与类构造函数分离的通常做法是将其移动到另一个方法中,例如init

class Some {
  constructor() {
    this.init();
  }

  init() {...}
}

ngOnInit可以在组件和指令中用来实现此目的:

constructor(
  public foo: Foo,
  /* verbose list of dependencies */
) {
  // time-sensitive initialization code
  this.bar = foo.getBar();
}

ngOnInit() {
  // rest of initialization code
}

依赖注入

在Angular中,类构造函数的主要作用是依赖注入。构造函数也被用于TypeScript中的DI注释。几乎所有的依赖都被分配为类实例的属性。

由于依赖关系,平均组件/指令构造函数已经足够大了,将不必要的初始化逻辑放入构造函数主体中会导致反模式。

异步初始化

异步初始化构造函数通常被认为是反模式,并且有味道,因为类实例化在异步程序完成之前就完成了,这可能会创建竞态条件。如果不是这种情况,ngOnInit和其他生命周期钩子是更好的位置,尤其是因为它们可以受益于async语法:

constructor(
  public foo: Foo,
  public errorHandler: ErrorHandler
) {}

async ngOnInit() {
  try {
    await this.foo.getBar();
    await this.foo.getBazThatDependsOnBar();
  } catch (err) {
    this.errorHandler.handleError(err);
  }
}

如果存在竞态条件(包括组件在初始化错误时不应出现的情况),异步初始化例程应该在组件实例化之前进行,并移至父组件、路由守卫等。

单元测试

ngOnInit比构造函数更灵活,并提供了一些单元测试方面的好处,这些好处在此答案中有详细解释。

考虑到在单元测试中,ngOnInit不会在组件编译时自动调用,因此在组件实例化后可以对ngOnInit中调用的方法进行监视或模拟。

在特殊情况下,ngOnInit可以完全被桩替代,以为其他组件单位(例如某些模板逻辑)提供隔离。

继承

子类只能增强构造函数,而不能替换构造函数。

由于在super()之前无法引用this,这会对初始化顺序产生限制。

考虑到Angular组件或指令使用ngOnInit进行与时间无关的初始化逻辑,子类可以选择何时调用super.ngOnInit()

ngOnInit() {
  this.someMethod();
  super.ngOnInit();
}

仅使用构造函数是不可能实现这一点的。


28

与许多其他编程语言一样,您可以在类级别、构造函数或方法中初始化变量。这取决于开发人员决定在其特定情况下什么最好。但以下是在做出决策时的最佳实践清单。

类级别变量

通常,在此处声明将在组件的其余部分中使用的所有变量。如果值不依赖于任何其他因素,则可以初始化它们,否则可以使用“const”关键字创建常数(如果它们不会更改)。

export class TestClass{
    let varA: string = "hello";
}

构造函数

通常最佳实践是不在构造函数中执行任何操作,而只是将其用于将要被注入的类。大多数情况下,您的构造函数应该如下所示:

   constructor(private http: Http, private customService: CustomService) {}

这将自动创建类级变量,因此您将可以访问customService.myMethod()而无需手动执行。

NgOnInit

NgOnit是Angular 2框架提供的生命周期钩子。您的组件必须实现OnInit才能使用它。这个生命周期钩子在构造函数调用后和所有变量初始化后被调用。大部分初始化应该放在这里。您可以确信Angular已正确初始化您的组件,可以在OnInit中开始执行任何需要的逻辑,而不是在组件加载完全之前执行。

下面是一个详细说明哪些内容会被调用的图像:

enter image description here

https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html

TLDR

如果您正在使用Angular 2框架并且需要与某些生命周期事件交互,请使用框架提供的方法来避免问题。


22

为了测试这个功能,我编写了以下代码,借鉴了NativeScript教程

user.ts

export class User {
    email: string;
    password: string;
    lastLogin: Date;

    constructor(msg:string) {        
        this.email = "";
        this.password = "";
        this.lastLogin = new Date();
        console.log("*** User class constructor " + msg + " ***");
    }

    Login() {
    }
}

login.component.ts

import {Component} from "@angular/core";
import {User} from "./../../shared/user/user"

@Component({
  selector: "login-component",
  templateUrl: "pages/login/login.html",
  styleUrls: ["pages/login/login-common.css", "pages/login/login.css"]
})
export class LoginComponent {

  user: User = new User("property");  // ONE
  isLoggingIn:boolean;

  constructor() {    
    this.user = new User("constructor");   // TWO
    console.log("*** Login Component Constructor ***");
  }

  ngOnInit() {
    this.user = new User("ngOnInit");   // THREE
    this.user.Login();
    this.isLoggingIn = true;
    console.log("*** Login Component ngOnInit ***");
  }

  submit() {
    alert("You’re using: " + this.user.email + " " + this.user.lastLogin);
  }

  toggleDisplay() {
    this.isLoggingIn = !this.isLoggingIn;
  }

}

控制台输出

JS: *** User class constructor property ***  
JS: *** User class constructor constructor ***  
JS: *** Login Component Constructor ***  
JS: *** User class constructor ngOnInit ***  
JS: *** Login Component ngOnInit ***  

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接