angular
- angular 中的变化检测,如果其中任意一个自组件发生状态改变,需要进行状态检测的话,都会引起从 root component 到最后一个 child component 的变化检测,直到最后一个组件也完成状态检测达到稳定状态为止。
在这个从上到下的变化检测流中,一旦 child A component 中的变化检测已经完成了,任何在 GrandChild component 或者更低层级的 component 都不允许去改变 child A component 中的属性
这个过程就是 angular 的单向数据流。
但是在 GrandChild component 里,有些钩子函数通过@Output 去改变 child A component 的数据值,是允许的,而有些又不允许。
允许的生命周期函数:
把 ngOnInit 换成 ngDoCheck、ngAfterContentInit、ngAfterContentChecked、ngOnChanges,效果是一样的,在 ChildAComponent 中 message 都能正常显示也不会报错。
不允许的生命周期周期函数:
ngAfterViewInit ,ngAfterViewChecked
这个时候会发现在 console 里会有 ExpressionChangedAfterItHasBeenCheckedError
出现这种错误的原因
在 angular 中强制了单向数据流,当有变化的时候,变化检测机制是沿着 component 关系树结构从上到下执行,直到最后一个 child component 变化检测完成,这个完整的变化检测才算结束。在这个过程中,parent component 的变化检测完成以后,任何更低一层级去改上一层级的属性,都不允许。如果是在生产环境里,也就是启用了 enableProdMode()会直接忽略这样的操作,页面也不会显示变化以后的值,也不会报错。但是在开发模式下,在每一次变换检测(change detection)以后,angular 会从上到下再多跑一个变化检测,确保每次改动之后所有的状态是 stable 的,这个时候发现有低层级改动上一层级的值,就会出现上面那个错误。
那为什么在 ngAfterViewInit 和 ngAfterViewChecked 会报错,而且其他几个钩子函数里不报错呢?
把 checkAndUpdateView 方法简化一下:
function checkAndUpdateView(view, ...) {
...
// update input bindings on child views (components) & directives,
// call NgOnInit, NgDoCheck, ngOnChanges,ngAfterContentInit, ngAfterContentChecked hooks if needed
Services.updateDirectives(view, CheckType.CheckAndUpdate);
// DOM updates, perform rendering for the current view (component)
Services.updateRenderer(view, CheckType.CheckAndUpdate);
// run change detection on child views (components)
execComponentViewsAction(view, ViewAction.CheckAndUpdate);
// call AfterViewChecked and AfterViewInit hooks
callLifecycleHooksChildrenFirst(…, NodeFlags.AfterViewChecked…);
...
}
从上面的代码可以看出来,在 GrandChild component 中,AfterViewChecked 和 AfterViewInit 是在它自己的变化检测(change detetion)之后再执行的,也就是它状态 stable 之后再执行的,这时候在去触发它上一层级属性的改动,被认为是违反 angular 的单向数据流。
从上面的分析可以看到:angular 在变化检测(change detection)过程中也会去触发生命周期钩子函数。比较有意思的是,有些钩子函数是在 DOM Rending/change detection 之前触发,有些是在之后触发。
- angular 整个渲染页面的过程是
更新 child component 的 input bindings,然后会触发 child component 中 OnInit、DoCheck、OnChanges 函数,如果页面有 ng-content,相应也会触发 ngAfterContentInit 和 ngAfterContentChecked。
angular 会继续渲染当前 component 也就是 parent component 页面。
触发 child component 中的变化检测(change detection)。
触发 child component 中的 AfterViewInit 和 theAfterViewChecked。
- angular 的生命周期
OnChanges,OnInit,DoCheck,AfterContentInit,AfterContentChecked,AfterViewInit,AfterViewChecked,OnDestroy。
钩子函数就是在对应的生命周期之前加上 ng
- ngOnChanges:
当前 component/directive 的@Input/@Output 绑定的值发生变化的时候会触发这个函数,在 ngOnInit 之前执行。需要注意的是:如果@Input 是个对象,对象里面的数据改变但是引用没有变化是不会触发这个函数。
- ngOnInit:
ngOnInit:在第一个 ngOnChanges 之后触发,只执行一次。这个函数用来初始化页面内容。
- ngDoCheck:
第一种情况是:只要有任何 change detection(比如 click handlers, http requests, route changes…)都会执行 ngDoCheck;
第二种情况是:状态发生变化,angular 自己又不能捕获这个变化会触发 ngDoCheck
- ngAfterContentInit:
页面有用 ng-content 进行组件内容投射,在初始化的时候会执行一次这个函数。
- ngAfterContentChecked:
在每次检查投射内容的时候执行,ngDoCheck 调用之后都会触发这个函数。
ngAfterViewInit:component 的页面或者是它的子页面初始的时候会执行一次这个函数。
ngAfterViewChecked:在每次检查 compoent 页面或者它的子页面的时候执行,ngDoCheck 调用之后都会触发这个函数。
ngOnDestroy:在 commponet 被销毁之前执行。
在使用钩子函数的时候需要注意以下几点:
constructor vs ngOnInit:在 constructor 里并不是所有数据都已经存在,比如@ContentChildren/@ContentChild/@ViewChildren/@ViewChild/@Input 在执行 constructor 的时候并不存在,相关代码最好都放在 ngOnInit。
ngOnChanges vs ngDoCheck:ngOnChanges 是在@Input 的值发生变化时触发;而 ngDoCheck 在每次 change detection 的时候都会触发或者是在状态发生变化 angular 自己又不能捕获时被触发。在用 ngDoCheck 的时候要非常小心,ngDoCheck 被触发的频率非常高,代码尽量精简,避免导致页面性能问题。
ngAfterContentInit vs ngAfterViewInit:跟 ng-content 相关的就用 ngAfterContentInit,当前 component 或者它的 child componet 相关的就用 ngAfterViewInit。
ngAfterContentChecked vs ngAfterViewChecked: 这两个函数也是每次都在 change detection 会被执行,用的时候也需要小心,避免页面性能问题。
ngOnChanges vs ngAfterViewChecked: 可以复用的 Component 考虑 ngOnChanges,不可以复用的 component 考虑 ngAfterViewChecked。