angular中ExpressionChangedAfterItHasBeenCheckedError錯誤

weiewiyi發表於2022-04-18

最近在使用ngAfterViewInit的時候發生了一些錯誤。又發現自己對變更檢測的流程其實不是很理解,所以來梳理一下過程,並來講講這個錯誤為什麼會發生。

變更檢測

首先來了解一下angular對元件的檢測操作:

當Angular 對每個元件進行檢查,這些元件大致按指定順序執行的以下操作

  1. 更新所有子元件/指令的繫結屬性 (例如@Input)
  2. 在所有子元件/指令上呼叫 ngOnInit、OnChanges 和 ngDoCheck 生命週期hook
  3. 更新當前元件的 DOM
  4. 為子元件執行變更檢測
  5. 為所有子元件/指令呼叫 ngAfterViewInit 生命週期鉤子

image.png


但是在開發模式下會額外執行以下的操作檢查:

在每次操作之後,Angular 都會記住它用來執行操作的變數的值。它們儲存在元件檢視的 oldValues 屬性中。

Angular 執行下列的操作:

  • 檢查傳遞給子元件的值是否與oldValues相同
  • 檢查用於更新 DOM 元素的值是否與oldValues相同
  • 對所有子元件執行相同的檢查

丟擲ExpressionChangedAfterItHasBeenCheckedError錯誤的例子

舉個例子:

假設現在是開發模式

定義了A元件,並傳遞text給B元件

@Component({ 
selector: 'a-comp',
template: ` 
 <span>{{name}}</span>
 <b-comp [text]="text"></b-comp> `
 })

export class AComponent { 
    name = 'Im A'
    text = 'A to B`;
}
@Component({ 
selector: 'b-comp',
 })
export class BComponent { 

 @Input() text; 

constructor(private parent: AComponent) {} 

ngOnInit() { 
    this.parent.text = 'B to A';
 }
}

讓我們按照的5步變更檢測進行

  1. 更新所有子元件/指令的繫結屬性
    angular先執行B元件的text與A元件的text繫結,B元件的text = "A to B"。
    在開發模式下,會記錄這個值view.oldValues[0] = '傳遞給子元件';
  2. 在子元件上呼叫 ngOnInit等鉤子
    此時執行AComponent.text = "B to A";

執行到第二步的時候發生異常,丟擲ExpressionChangedAfterItHasBeenCheckedError
如下圖

image.png

這就是違反了開發模式下的檢查所丟擲的錯誤。

簡單來說,就是在前一步已經確立好值的情況下,下一步反過來了又將它改變。與oldValue衝突.

即ExpressionChangedAfterItHasBeenCheckedError這個單詞的字面意思。


第二個例子

假如我們在B這個子元件中這麼做會不會丟擲錯誤呢?

ngOnInit() { this.parent.name = 'updated name'; }

有可能你想:這不也是在ngOnInit中變更父元件的值嘛,肯定會報錯。

但結果是不會,原因是什麼呢?

別忘了,在A中是這麼定義name的:

@Component({ 
template: ` 
 <span>{{name}}</span>
 })

export class AComponent { 
    name = 'Im A'
}

看出點什麼了嗎? 沒錯,有關name的操作在第三步才執行。

即:更新本元件的DOM

image.png

總結:

其實原理很簡單:在前一步已經確立好值後下一步不要更改它,不要與oldValue衝突

專案中的例子

專案中遇到是動態元件方面的報錯例子,也是丟擲ExpressionChangedAfterItHasBeenCheckedError。

簡單介紹一下程式碼:

export class App {
    @ViewChild(FormItemDirective, {static: true})
    appFormItem: FormItemDirective;
 

    constructor(private r: ComponentFactoryResolver) {
    }

    ngAfterViewInit() {
        const f = this.r.resolveComponentFactory(BComponent);
        this.appFormItem.viewContainerRef.createComponent(f);
    }
}

根據5步流程,報錯的原因就很簡單了:

該元件在 ngAfterViewInit 中動態新增一個子元件。由於新增子元件需要修改 DOM,並且在 Angular 更新 DOM 後觸發 ngAfterViewInit 生命週期鉤子,又去修改DOM,因此會引發錯誤。

如圖:第五步時又去修改第三步已經確立的DOM。
image.png

解決方法

這裡以專案中的修改為例子,講講如何解決第5步中修改了第3步中已經確立的DOM發生的問題。

1. 把修改提前

很簡單的方法,既然是在第三步中確立的DOM, 那麼在第一步和第二步中修改它不就行了。

  • 在第一步中修改,可以使用@Input,因為更新子元件的繫結屬性是在第一步中完成。

    @Input()
    set setValue(value: Type) {
        // do someting
    }
  • 在第二步中修改,ngOnInit等鉤子是在第二步中完成

    ngOnInit() {
     // do something
    }

2. 強制變更檢測

另一種可能的解決方案是為父 A 元件強制執行另一個更改檢測週期。最好的地方是在 ngAfterViewInit 生命週期鉤子中,因為它是在對所有子元件執行更改檢測時觸發的,因此它們比較可能更新父元件屬性。

使用ChangeDetectorRef.detectChanges()就能完成這個操作。

export class AppComponent { 

constructor(private cd: ChangeDetectorRef) { } 

ngAfterViewInit() { 
    this.cd.detectChanges(); 
}

幾個問題

為什麼angular需要這麼驗證?

Angular 強制執行所謂的從上到下的單向資料流。處理父級更改後,不允許層次結構較低的元件更新父元件的屬性.

這確保了在變更檢測之後,整個元件樹是穩定的。如果需要與依賴於這些屬性的消費者同步的屬性發生變化,則樹是不穩定的。

為什麼只在開發模式下執行它?

可能因為整合模式不像開發模式執行時錯誤那樣嚴重。畢竟它可能會在下一次摘要執行中穩定下來。

但是,最好在開發模式下就解決它,而不是留給客戶端來嘗試除錯它。


參考文章:https://hackernoon.com/everyt...

相關文章