组件有一个由Angular本身管理的生命周期。html
Angular建立它,渲染它,建立和渲染它的子项,在数据绑定属性发生变化时对其进行检查,并在将它从DOM中删除以前对其进行销毁。java
Angular提供生命周期挂钩,提供这些关键生命时刻的可视性以及发生时的行为能力。web
指令具备相同的生命周期挂钩集,减去特定于组件内容和视图的挂钩。api
指令和组件实例的生命周期与Angular建立,更新和摧毁它们同样。 开发人员能够经过在Angular core库中实现一个或多个Lifecycle Hook界面来挖掘该生命周期中的关键时刻。浏览器
每一个接口都有一个单一的钩子方法,其名称是以ng开头的接口名称。 例如,OnInit接口有一个名为ngOnInit的钩子方法,Angular在建立组件后当即调用:安全
lib/src/peek_a_boo_component.dart (ngOnInit)服务器
class PeekABoo implements OnInit { final LoggerService _logger; PeekABoo(this._logger); // implement OnInit's `ngOnInit` method void ngOnInit() { _logIt('OnInit'); } void _logIt(String msg) { // Don't tick or else // the AfterContentChecked and AfterViewChecked recurse. // Let parent call tick() _logger.log("#${_nextId++} $msg"); } }
没有指令或组件会实现全部的生命周期钩子,而且一些钩子只对组件有意义。 若是它被定义了,Angular只会调用一个指令/组件钩子方法。app
经过调用其构造函数建立组件/指令后,Angular在特定时刻按如下顺序调用生命周期钩子方法:ide
钩子 | 做用和时机 |
---|---|
ngOnChanges | Angular(从新)设置数据绑定输入属性时响应。 该方法接收当前和前一个属性值的SimpleChanges对象。函数 在ngOnInit以前调用而且每当有一个或多个数据绑定输入属性发生变化时调用。 |
ngOnInit | 在Angular首次显示数据绑定属性并设置指令/组件的输入属性后,初始化指令/组件。 在第一次ngOnChanges以后调用一次。 |
ngDoCheck | 检测Angular没法或没法自行检测到的更改并采起相应措施。 在每次更改检测运行期间,当即在ngOnChanges和ngOnInit以后调用。 |
ngAfterContentInit | 在Angular将外部内容投影到组件的视图以后进行响应。 在第一次NgDoCheck以后调用一次。 组件独有的钩子。 |
ngAfterContentChecked | 在Angular检查投影到组件中的内容以后做出响应。 在ngAfterContentInit和后续的每次NgDoCheck以后调用。 组件独有的钩子。 |
ngAfterViewInit | 在Angular初始化组件的视图和子视图以后进行响应,。 在第一次ngAfterContentChecked以后调用一次。 组件独有的钩子。 |
ngAfterViewChecked | 在Angular检查组件的视图和子视图以后做出响应。 在ngAfterViewInit和后续的每次ngAfterContentChecked以后调用。 组件独有的钩子。 |
ngOnDestroy | 在Angular摧毁指令/组件以前进行清理。 取消订阅observables并分离事件处理程序以免内存泄漏。 在Angular摧毁指令/组件以前调用。 |
其余Angular子系统除了这些组件钩子可能有本身的生命周期钩子。
例如,路由器也有本身的路由器生命周期挂钩,可让咱们利用路由导航中的特定时刻。 能够在ngOnInit和routerOnActivate之间绘制一个平行线。 二者的前缀都是为了不碰撞,而且在组件初始化时都运行正确。
第三方库可能也会实现它们的钩子,以便让开发人员更好地控制这些库的使用方式。
经过组件的一系列练习在根AppComponent的控制下呈现来演示生命周期挂钩。
它们遵循一种常见的模式:父组件做为一个子组件的一个或多个生命周期钩子方法的测试装备。
如下是每一个练习的简要说明:
组件 | 描述 |
---|---|
Peek-a-boo | 演示每一个生命周期的钩子。 每一个挂钩方法都会写入屏幕日志。 |
Spy | 指令也有生命周期挂钩。 SpyDirective可使用ngOnInit和ngOnDestroy挂钩建立或销毁它探测的元素。 此示例将SpyDirective应用于由父SpyComponent管理的ngFor英雄迭代器中的<div>。 |
OnChanges | 看看每次组件输入属性发生变化时,Angular如何用变动对象调用ngOnChanges钩子。 显示如何解释更改对象。 |
DoCheck | 使用自定义更改检测实现ngDoCheck方法。 看看Angular多久会调用这个钩子,并在更改日志后观察它。 |
AfterView | 经过视图显示Angular的意图。 演示ngAfterViewInit和ngAfterViewChecked挂钩。 |
AfterContent | 演示如何将外部内容投影到组件中,以及如何区分组件的视图中的投影内容和子组件。 演示ngAfterContentInit和ngAfterContentChecked挂钩。 |
Counter | 演示组件和指令的组合,每一个组件都有本身的钩子。 在此示例中,每次父组件递增其输入计数器属性时,CounterComponent都会记录更改(经过ngOnChanges)。 同时,前面例子中的SpyDirective被应用到CounterComponent日志中,它监视正在建立和销毁的日志条目。 |
本章的其他部分将进一步详细讨论选定的练习
PeekABooComponent演示了一个组件中的全部钩子。
若是有的话,你不多会实现像这样的全部接口。 peek-a-boo存在以显示Angular如何按预期顺序调用钩子。
此快照反映用户单击“建立...”按钮而后单击“销毁...”按钮后日志的状态。
日志消息的顺序遵循规定的钩子调用顺序:OnChanges,OnInit,DoCheck(3x),AfterContentInit,AfterContentChecked(3x),AfterViewInit,AfterViewChecked(3x)和OnDestroy。
构造函数自己不是一个Angular钩子。 日志确认输入属性(在这种状况下的name属性)在构造时没有分配的值。
若是用户点击Update Hero按钮,日志会显示另外一个OnChanges和两个更多的DoCheck,AfterContentChecked和AfterViewChecked三元组。 显然这三个钩子常常发射。 尽量保持这些钩子中的逻辑!
接下来的例子集中于钩子细节。
使用这两个间谍钩进行卧底探索,以发现元素什么时候被初始化或销毁。
这是指令的完美渗透工做。 英雄们永远不会知道他们正在被监视。
一边开玩笑,注意两点:
- Angular为指令和组件调用钩子方法。
- 间谍指令能够提供对不能直接更改的DOM对象的洞察。 显然,你不能触摸本地div的实现。 您也不能修改第三方组件。 可是你能够监察一个指令。
这个偷偷摸摸的间谍指令很简单,几乎彻底由ngOnInit和ngOnDestroy钩子组成,这些钩子经过注入的LoggerService将消息记录到父级。
// Spy on any element to which it is applied. // Usage: <div mySpy>...</div> @Directive(selector: '[mySpy]') class SpyDirective implements OnInit, OnDestroy { final LoggerService _logger; SpyDirective(this._logger); ngOnInit() => _logIt('onInit'); ngOnDestroy() => _logIt('onDestroy'); _logIt(String msg) => _logger.log('Spy #${_nextId++} $msg'); }
您能够将间谍应用到任何本机或组件元素,而且会与该元素的同一时间进行初始化和销毁。 在这里它被附加到重复的英雄<div>
<div *ngFor="let hero of heroes" mySpy class="heroes"> {{hero}} </div>
每一个间谍的出生和死亡标志着所附英雄<div>的出生和死亡,并在Hook Log中有一个条目,以下所示:
添加一个英雄会产生一个新的英雄<div>。 间谍的ngOnInit记录该事件。
重置按钮清除英雄列表。 Angular从DOM中移除全部英雄<div>元素并同时销毁他们的间谍指令。 间谍的ngOnDestroy方法报告其最后时刻。
ngOnInit和ngOnDestroy方法在实际应用中扮演更重要的角色。
使用ngOnInit有两个主要缘由:
有经验的开发人员赞成组件应该便于构建且安全。
Angular团队负责人Misko Hevery解释了为何您应该避免使用复杂的构造函数逻辑。
不要在组件构造函数中获取数据。您不该该担忧当在测试下建立或决定显示以前时新组件会尝试联系远程服务器。构造函数不该仅仅将初始局部变量设置为简单值。
ngOnInit是组件获取其初始数据的好地方。 教程和HTTP章节显示了如何。
还要记住,指令的数据绑定输入属性在构建以后才会设置。 若是您须要根据这些属性初始化指令,那么这是一个问题。 当ngOninit运行时,它们将被设置。
ngOnChanges方法是您第一次访问这些属性的机会。 在ngOnInit以前Angular会调用ngOnChanges ...并在此以后屡次调用。 它只调用一次ngOnInit。
您能够期待Angular在建立组件后当即调用ngOnInit方法。 这就是深度初始化逻辑所属的地方。
将清理逻辑放入ngOnDestroy中,在Angular销毁指令以前必须运行的逻辑。
这是通知应用程序的另外一部分组件将要销毁的时间。
这是释放资源的地方,不会自动收集垃圾。 取消订阅observables和DOM事件。 中止间隔定时器。 取消注册此指令在全局或应用服务中注册的全部回调。 若是你忽视这样作,你会冒内存泄漏的风险。
只要检测到组件(或指令)的输入属性发生变化,Angular就会调用它的ngOnChanges方法。 这个例子监视OnChanges钩子。
lib/src/on_changes_component.dart (ngOnChanges)
ngOnChanges(Map<String, SimpleChange> changes) { changes.forEach((String propName, SimpleChange change) { String cur = JSON.encode(change.currentValue); String prev = change.previousValue == null ? "{}" : JSON.encode(change.previousValue); changeLog.add('$propName: currentValue = $cur, previousValue = $prev'); }); }
ngOnChanges方法接受一个对象,该对象将每一个已更改的属性名称映射到保存当前和前一个属性值的SimpleChange对象。 这个钩子迭代已更改的属性并记录它们。
示例组件OnChangesComponent具备两个输入属性:hero和power。
@Input() Hero hero; @Input() String power;
宿主OnChangesParentComponent像这样绑定到它们:
<on-changes [hero]="hero" [power]="power"></on-changes>
如下是用户进行更改时的示例。
日志条目显示为power属性更改的字符串值。 但ngOnChanges并无捕捉到hero.name的变化,这一开始使人惊讶。
当输入属性的值改变时,Angular只会调用钩子。 hero属性的值是对hero对象的引用。 Angular并不在乎英雄本身的name属性发生了变化。 英雄对象引用没有改变,因此从Angular的角度来看,没有改变的反馈!
使用DoCheck钩子来检测并处理Angular本身没法捕获的更改。
使用此方法检测Angular忽略的更改。
DoCheck示例使用如下ngDoCheck钩子扩展了OnChanges示例:
lib/src/do_check_component.dart (ngDoCheck)
ngDoCheck() { if (hero.name != oldHeroName) { changeDetected = true; changeLog.add( 'DoCheck: Hero name changed to "${hero.name}" from "$oldHeroName"'); oldHeroName = hero.name; } if (power != oldPower) { changeDetected = true; changeLog.add('DoCheck: Power changed to "$power" from "$oldPower"'); oldPower = power; } if (changeDetected) { noChangeCount = 0; } else { // log that hook was called when there was no relevant change. var count = noChangeCount += 1; var noChangeMsg = 'DoCheck called ${count}x when no change to hero or power'; if (count == 1) { // add new "no change" message changeLog.add(noChangeMsg); } else { // update last "no change" message changeLog[changeLog.length - 1] = noChangeMsg; } } changeDetected = false; }
此代码检查某些感兴趣的值,捕获并比较其当前状态与之前的值。 当英雄或权力没有实质性变化时,它会向日志中写入特殊消息,以便您能够看到DoCheck被屡次调用。 结果是高亮的:
虽然ngDoCheck挂钩能够检测到英雄的name什么时候发生变化,但它的成本很是可怕。 这个钩子以巨大的频率被调用 - 在每一个变化检测周期以后,不管变化发生在何处。 在用户能够作任何事情以前,在这个例子中它被调用了二十次。
大部分初始检查都是由Angular在页面其余地方首次渲染(与数据无关)而触发的。 仅仅经过鼠标移动到另外一个输入框就会触发一个呼叫。 相对较少的调用显示相关数据的实际变化。 很显然,咱们的实施必须很是轻便,不然用户体验将受到影响。
AfterView样本探讨了Angular在建立组件的子视图后调用的AfterViewInit和AfterViewChecked挂钩。
如下是在输入框中显示英雄名字的子视图:
lib/src/after_view_component.dart (child view)
@Component( selector: 'my-child-view', template: '<input [(ngModel)]="hero">', directives: const [CORE_DIRECTIVES, formDirectives], ) class ChildViewComponent { String hero = 'Magneta'; }
AfterViewComponent在其模板中显示此子视图:
lib/src/after_view_component.dart (template)
template: ''' <div>-- child view begins --</div> <my-child-view></my-child-view> <div>-- child view ends --</div> <p *ngIf="comment.isNotEmpty" class="comment">{{comment}}</p>''',
如下钩子根据更改子视图内的值来执行操做,只能经过使用@ViewChild注解的属性查询子视图来实现。
lib/src/after_view_component.dart (class excerpts)
class AfterViewComponent implements AfterViewChecked, AfterViewInit { var _prevHero = ''; // Query for a VIEW child of type `ChildViewComponent` @ViewChild(ChildViewComponent) ChildViewComponent viewChild; ngAfterViewInit() { // viewChild is set after the view has been initialized _logIt('AfterViewInit'); _doSomething(); } ngAfterViewChecked() { // viewChild is updated after the view has been checked if (_prevHero == viewChild.hero) { _logIt('AfterViewChecked (no change)'); } else { _prevHero = viewChild.hero; _logIt('AfterViewChecked'); _doSomething(); } } // ... }
当英雄名字超过10个字符时,doSomething方法更新屏幕。
lib/src/after_view_component.dart (doSomething)
// This surrogate for real business logic sets the `comment` void _doSomething() { var c = viewChild.hero.length > 10 ? "That's a long name" : ''; if (c != comment) { // Wait a tick because the component's view has already been checked _logger.tick().then((_) { comment = c; }); } }
为何doSomething方法在更新comment以前等待一个tick?
Angular的单向数据流规则禁止在视图组成以后更新视图。 组件视图组合完成后,这两个钩子都会触发。
若是钩子当即更新组件的数据绑定comment属性,Angular会抛出一个错误(尝试它!)。
LoggerService.tick()推迟了浏览器更新周期的一第二天志更新......而且这足够长。
这里是AfterView的行动
请注意,常常在没有感兴趣的变化时,Angular常常调用AfterViewChecked。 编写瘦钩方法以免性能问题。
AfterContent示例探索在Angular将外部内容投影到组件后的Angular调用的AfterContentInit和AfterContentChecked挂钩。
内容投影是一种从组件外部导入HTML内容并将该内容插入组件模板中指定位置的方法。
Angular 1开发人员知道这种技术是跨越式的。
考虑之前的AfterView示例中的这种变化。 这一次,它不是在模板中包含子视图,而是从AfterContentComponent的父项导入内容。 这是父母的模板。
lib/src/after_content_component.dart (template excerpt)
template: ''' <div class="parent"> <h2>AfterContent</h2> <div *ngIf="show"> <after-content> <my-child></my-child> </after-content> </div> <h4>-- AfterContent Logs --</h4> <p><button (click)="reset()">Reset</button></p> <div *ngFor="let msg of logs">{{msg}}</div> </div> ''',
请注意,<my-child>标签隐藏在<after-content>标签之间。 除非您打算将该内容投影到组件中,不然毫不要在组件的元素标签之间放置内容。
如今看看组件的模板:
lib/src/after_content_component.dart (template)
template: ''' <div>-- projected content begins --</div> <ng-content></ng-content> <div>-- projected content ends --</div> <p *ngIf="comment.isNotEmpty" class="comment">{{comment}}</p> ''',
<ng-content>标记是外部内容的占位符。 它告诉Angular在哪里插入该内容。 在这种状况下,投影内容是来自父级的<my-child>。
内容投影的指示标记是(a)组件元素标签之间的HTML和(b)组件模板中存在<ng-content>标签。
AfterContent挂钩与AfterView挂钩相似。 关键的区别在于子组件
如下AfterContent挂钩根据内容子代(只能经过使用@ContentChild注解的属性查询它)中的值进行更改。
lib/src/after_content_component.dart (class excerpts)
class AfterContentComponent implements AfterContentChecked, AfterContentInit { String _prevHero = ''; String comment = ''; // Query for a CONTENT child of type `ChildComponent` @ContentChild(ChildComponent) ChildComponent contentChild; ngAfterContentInit() { // contentChild is set after the content has been initialized _logIt('AfterContentInit'); _doSomething(); } ngAfterContentChecked() { // contentChild is updated after the content has been checked if (_prevHero == contentChild?.hero) { _logIt('AfterContentChecked (no change)'); } else { _prevHero = contentChild?.hero; _logIt('AfterContentChecked'); _doSomething(); } } // ... }
该组件的doSomething方法当即更新组件的数据绑定comment属性。 不须要等。
回想一下,在调用AfterView钩子以前,Angular调用了AfterContent的两个钩子。 在完成该组件的视图以前,Angular会完成投影内容的组合。 AfterContent ...和AfterView ...钩子之间有一个小窗口来修改宿主视图。