详解Angular中的变更检测(七)- 一个常见错误

95 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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);
  }
}

上面的代码肯定会报错,这个之前也有提到过,但是没有展开详细说。

先来思考几个问题:

  1. 现在count的值是多少?代码执行到this.count++后,页面上的count是多少?
  2. 页面加载完成后,count的值是多少,为什么?
  3. 怎么让这段程序不报这个错误,为什么?

解决办法

办法一

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模式。

先来回答一下上面的问题

  1. 组件中count的值是1,代码执行this.count++之后,页面上显示的count依然是0;
  2. 页面加载完成后,页面显示的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下报错,我们也不能对其置之不理。因为开发模式下会更加严格,如果不执行这次检查的话,会造成组件状态和视图不一致的情况,很容易出现问题。其实也就是帮助开发者少犯错误。

官网视频

视频