开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情
Error
在日常开发过程中,有时候我们需要在组件生命周期钩子中改变组件的状态,直接上代码。
import { AppComponent } from '../app.component';
@Component({
selector: 'main',
template: `
<h1>Main</h1>
<div>{{count}}</div>`
})
export class MainComponent {
count: number = 0;
constructor(){}
ngOnInit(){
}
ngAfterViewInit(): void {
this.count++;
console.log(this.count);
}
}
上面的代码肯定会报错,这个之前也有提到过,但是没有展开详细说。
先来思考几个问题:
- 现在count的值是多少?代码执行到this.count++后,页面上的count是多少?
- 页面加载完成后,count的值是多少,为什么?
- 怎么让这段程序不报这个错误,为什么?
解决办法
办法一
export class MainComponent {
count: number = 0;
...
ngAfterViewInit(): void {
setTimeout(() => {
this.count++;
});
}
}
把ngAfterViewInit中的代码放到异步函数里就不会报这个错误了,也就是我们把组件的状态推迟到下一次变更检测周期里,不在本次变更检测周期里执行就可以了。
办法二
export class MainComponent {
count: number = 0;
ngOnInit(){
this.count++;
}
ngAfterViewInit(): void {
......
}
}
办法三
export class MainComponent {
count: number = 0;
constructor(private cd: ChangeDetectorRef){}
ngOnInit(){
}
ngAfterViewInit(): void {
this.count++;
this.cd.detectChanges();
}
}
方法四
设置为production模式。
先来回答一下上面的问题:
- 组件中count的值是1,代码执行this.count++之后,页面上显示的count依然是0;
- 页面加载完成后,页面显示的count是1;
之前说过,我们认为组件在初始化阶段只会执行一次变更检测,在ngAfterViewInit钩子函数中,组件已经执行过了变更检测,即使你再改变count的值也于事无补了,那为什么页面上却能显示为最新值1呢??必然在ngAfterViewInit之后,又执行了一次变更检测,最终页面上才会显示最新的值1。 所以,我们之前的认为是不正确的,通过调试源代码发现的确是执行了两次变更请求。第一次在loadComponent中执行了一次,然后在对onMicrotaskEmpty订阅中又执行了一次。大家可以自行调试验证一下哦。
值得注意的是,目前的环境是dev模式,如果我把环境切换成production模式,也不会报错。
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
enableProdMode();
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
发现已经不在报错了,为什么切换到production模式下就不报错了呀?还记得之前ApplicationRef的tick()的代码吗?
*/
class ApplicationRef {
/** @internal */
constructor(_zone, _injector, _exceptionHandler, _initStatus) {
...
this.components = [];
this._onMicrotaskEmptySubscription = this._zone.onMicrotaskEmpty.subscribe({
next: () => {
this._zone.run(() => {
this.tick();
});
}
});
...
}
tick() {
...
try {
this._runningTick = true;
for (let view of this._views) {
view.detectChanges();
}
if (typeof ngDevMode === 'undefined' || ngDevMode) {
for (let view of this._views) {
view.checkNoChanges();
}
}
}
catch (e) {
// Attention: Don't rethrow as it could cancel subscriptions to Observables!
this._zone.runOutsideAngular(() => this._exceptionHandler.handleError(e));
}
finally {
this._runningTick = false;
}
}
_loadComponent(componentRef) {
this.attachView(componentRef.hostView);
this.tick();
this.components.push(componentRef);
// Get the listeners lazily to prevent DI cycles.
const listeners = this._injector.get(APP_BOOTSTRAP_LISTENER, []).concat(this._bootstrapListeners);
listeners.forEach((listener) => listener(componentRef));
}
}
我们发现源码里如果是开发模式,会执行view.checkNoChanges() ,相当于右做了一次变更检测,也就是如果是开发模式,会执行两次“变更检测”。checkNoChanges方法,会检测组件状态没有更新,如果没有变化就不会报错,如果有变化,就会报错。
思考:为什么开发模式下需要执行两次“变更检测” ?
尽管这个错误只会在开发模式下抛出,不会再production下报错,我们也不能对其置之不理。因为开发模式下会更加严格,如果不执行这次检查的话,会造成组件状态和视图不一致的情况,很容易出现问题。其实也就是帮助开发者少犯错误。