關於ExpressionChangedAfterItHasBeenCheckedError

最近在stackoverflow上彷佛天天都有一些關於angular報錯‘ExpressionChangedAfterItHasBeenCheckedError’的問題。發生這些問題一般是因爲angular的開發者不懂angular變動檢測的工做原理,以及爲何這個檢測的報錯是有必要的。不少開發者甚至認爲這是angular的bug。但其實不是的。這是一個用於防止模型數據和ui之間數組不一致的一個警告機制,以便不讓用戶在頁面上看到陳舊的或者錯誤的數據。javascript

這篇文章解釋了該報錯和檢測機制的潛在緣由,提供了幾種可能引起報錯的通用模式和可能的修復方案。文章最後解釋了爲何檢測機制是重要的。
 
1、相關變動檢測操做
一個運行中的angular應用是一個組件樹。在變動檢測期間,angular按照如下的特定順序檢查每個組件:
一、更新全部子組件/指令綁定的特性
二、調用全部的自組件/指令的ngOnInit, OnChanges和ngDocCheck生命週期勾子
三、更新當前組件的DOM視圖
四、爲一個子組件運行變動檢測
五、調用全部子組件/指令的ngAfterViewInit生命週期勾子
每一步操做後,angular會記住每一步用於操做的值,它們會被保存在控制器視圖的oldValues屬性中,在對全部的組件進行檢查後,angular進入下一個摘要週期(原詞是digest cycle,這裏不知道怎麼翻譯更準確),而不是執行上面列表中的操做,它將當前值與上一個摘要週期中保存的值進行比較,過程以下:
一、檢查傳遞給子組件的值與將用於更新這些組件屬性的值相同
二、檢查用於用於更新dom元素的值與將用於更新這些元素的值相同
三、對全部子組件執行相同的檢查
請注意該額外的檢查只會在開發模式下執行,我在文章最後一段已經解釋了緣由。
讓咱們看一個例子,假設你有一個父組件a和子組件b,a組件有一個‘name’變量和一個‘text’屬性,在該例子模版中使用引用名稱屬性的表達式:
template: '<span>{{name}}</span>'
而且該例子模版中還有一個b控制器,其經過輸入屬性綁定將text屬性傳遞給該組件。
@Component({
selector: 'a-comp',
template: `
{{name}}

`
})
export class AComponent {
name = 'I am A component';
text = 'A message for the child component`;
因此當angular運行變動檢測時會發生什麼。變動檢測從檢查a組件開始。上述列表中第一步是更新綁定的屬性,以便獲得text表達式值爲「A message for the child component」,並將該值傳遞給b組件,最後保存到控制視圖的oldValues屬性中:
view.oldValues[0] = 'A message for the child component';
而後開始調用上述列表中第二步提到的生命週期勾子。
如今開始執行第三步操做以及計算出表單式{{name}}的值爲「I am A component」,angular使用該值更新dom視圖而且將該值保存到oldValues:
view.oldValues[1] = 'I am A component';
接着angular執行下一步操做,而且爲b控制器運行相同的檢查。一旦b控制器變動檢測完畢,當前的摘要循環即結束。
若是angular是在開發環境下運行的,將會執行第二次的摘要來執行上述列表中的步驟。如今想象一下,在angular把text的值「A message for the child component」傳遞給b組件而且保存到控制器視圖中的oldValues後,text變量值在a組件上被以某種方式更新爲「updated text」,如今angular運行驗證摘要,而後第一步操做是檢查屬性text有無改變:
AComponentView.instance.text === view.oldValues[0]; // false
'A message for the child component' === 'updated text'; // false
顯而易見,text改變了,也就是在摘要週期中檢查傳遞給子組件的值與將用於更新這些組件屬性的值不相同了,因此拋出了ExpressionChangedAfterItHasBeenCheckedError錯誤。
一樣,若是name屬性值在被呈現和存儲後更新,也會獲得相同的報錯:
AComponentView.instance.name === view.oldValues[1]; // false
'I am A component' === 'updated name'; // false
你如今可能有一個疑問,即這些值是如何改變的,讓咱們往下看。
 
2、值改變的可能場景
一般致使變化的罪魁禍首每每是組件或者指令。讓咱們來簡單快速的演示一下。我會使用盡量最簡單的例子,可是以後就要開始展現真實的場景了。你應該知道,子組件和指令可以注入其父組件,所以讓咱們來將b組件注入到a組件中而且更新其綁定的屬性text,咱們將會在ngOnInit生命週期勾子中更新該屬性值,由於ngOnInit是在綁定完成以後被觸發的,代碼以下:
export class BComponent {
    @Input() text;
    constructor(private parent: AppComponent) {}
    ngOnInit() {
        this.parent.text = 'updated text';
    }
}
不出所料,咱們獲得了報錯:
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'A message for the child component'. Current value: 'updated text'.
如今,咱們用a組件表達式中的name屬性作一樣的操做:
ngOnInit() {
this.parent.name = 'updated name';
}
如今一切正常,怎麼回事?
若是你仔細觀察上述變動檢測的執行順序就會發現ngOnInit生命週期勾子是在dom更新操做以前被觸發的,這就是爲何上面沒有獲得報錯的緣由。咱們如今須要一個在dom更新操做完成以後的生命週期勾子,好比ngAfterViewInit就很是適合:
export class BComponent {
    @Input() text;
    constructor(private parent: AppComponent) {} 
    ngAfterViewInit() {
        this.parent.name = 'updated name';
    }
}
此次咱們又獲得了預料中的錯誤:
AppComponent.ngfactory.js:8 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'I am A component'. Current value: 'updated name'.
固然,真實場景的案例是錯綜複雜的,那些致使dom渲染的父組件屬性更新或操做每每是經過服務或者可觀察對象模式間接實現的,可是根本原理和緣由是相同的。
如今,讓咱們看一些真實場景下致使錯誤的共同模式。
一、共享服務
該應用設計爲在父組件和子組件之間共享一個服務。子組件爲服務設置一個值,繼而經過更新父組件的屬性實現反應,我稱這個父屬性的更新爲間接的,由於與上面的例子不一樣,如今子組件更新父組件屬性不是很是顯著的。
二、同步事件廣播(Synchronous event broadcasting)
該應用設計爲一個子組件發送一個事件,而後父組件監聽這個事件,該事件會致使父組件一些屬性值的更新。同時這些屬性被用於子組件的輸入綁定中。這也是一個間接的父組件屬性更新。
三、動態組件實例化(Dynamic component instantiation)ngAfterViewInit
這種模式不一樣於以前輸入綁定受到影響,而是會引發dom更新操做拋出錯誤。該應用設計爲在父組件的ngAfterViewInit中動態的添加一個子組件,因爲添加子組件須要dom修改,dom更新後繼而觸發ngAfterViewInit生命週期鉤子,拋出錯誤。
 
3、可能的修復解決方案
若是你看一下以下的錯誤描述
Expression has changed after it was checked. Previous value:…

不由思考,它是在變化檢測勾子中建立的嗎?java

一般,修復方案即經過正確的變動檢測機制來建立動態組件。例如上面章節中的最後一個例子,能夠將動態組件的建立過程移到ngOnInit生命週期勾子中,儘管文檔說明ViewChild只能在ngAfterViewInit以後使用,可是在建立視圖的時候,它歸屬於子組件,所以能夠更早使用。express

若是你用谷歌搜索相關資料,你可能會發現針對這個報錯的兩個最大衆化的解決方案 —— 異步屬性更新和強制附加變化檢測週期,儘管我把這兩個解決方案放在了這裏,同時還解釋了它們的工做原理,可是我不推薦使用這些方案,而是應該從新設計你的應用。這一點我會在文章末尾闡述緣由。
一、異步屬性更新
這裏須要注意的是,變動檢測和驗證摘要是同步執行的,這意味着若是咱們異步更新屬性,當驗證循環正在運行中時,屬性值不會變化更新,應用也就不會拋出錯誤了,讓咱們試一下:
export class BComponent {
    name = 'I am B component';
    @Input() text; 
    constructor(private parent: AppComponent) {}
    ngOnInit() {
        setTimeout(() => {
            this.parent.text = 'updated text';
        });
    }
    ngAfterViewInit() {
        setTimeout(() => {
            this.parent.name = 'updated name';
        });
    }
}
的確沒有拋出錯誤,setTimeout函數調度了宏任務,而後將在下面的vm回調中執行,可是須要在當前同步代碼完成以後經過使用promise回調來執行。
Promise.resolve(null).then(() => this.parent.name = 'updated name');
替代宏任務promise.then來建立一個微任務,在當前同步代碼完成執行以後,微任務隊列被處理,所以在驗證步驟以後將發生對屬性的更新。瞭解更多angular中的宏任何和微任務,能夠前往I reverse-engineered Zones (zone.js) and here is what I’ve found.(https://blog.angularindepth.com/i-reverse-engineered-zones-zone-js-and-here-is-what-ive-found-1f48dc87659b)

若是你在使用EventEmitter,你能夠傳遞true參數選項來設置異步機制 數組

new EventEmitter(true);
二、強制更新檢測
另外一個可能的解決方案是在第一種方案和認證階段之間,爲父級的a組件強制增長一個變動檢測週期,執行這一方案最好的地方是在ngAfterViewInit生命週期勾子中,由於它是在給全部子組件執行變動檢測時被觸發,因此有可能會更新父組件的屬性。
export class AppComponent {
    name = 'I am A component';
    text = 'A message for the child component';
    constructor(private cd: ChangeDetectorRef) {
    }
    ngAfterViewInit() {
        this.cd.detectChanges();
    }
}
沒有報錯,彷佛正確工做了,可是該解決方案存在一個問題,當觸發對父組件的更新檢測時,angular將運行對全部子組件的變動檢測,會存在父組件屬性更新的可能。
 
4、爲何須要驗證環
Angular從上到下強制執行所謂的單向數據流。 在父級更改處理完畢後,層級中較低的組件不容許更新父組件的屬性。 這確保了在第一個摘要循環以後整個組件樹是穩定的。 若是須要與依賴於這些屬性的使用者同步的屬性發生更改,則樹不穩定。 在咱們的例子中,一個B子組件依賴於父文本屬性。 只要這些屬性發生更改,組件樹就會變得不穩定,直到將此更改傳遞給子組件B。 DOM也是如此。 它是組件上某些屬性的使用者,它在UI上呈現它們。 若是某些屬性未同步,用戶將在頁面上看到不正確的信息。
這個數據同步過程就是變化檢測過程當中發生的狀況 - 特別是我在開始時列出的兩個操做。 那麼,若是在同步操做執行後更新子組件屬性的父屬性,會發生什麼狀況? 對,你留下了不穩定的樹,這種狀態的後果是不可能預測的。 大多數狀況下,您最終會在頁面上向用戶顯示不正確的信息。 這將很難調試。
那麼爲何不運行變化檢測直到組件樹穩定? 答案很簡單 - 由於它可能永遠不會穩定下來並永遠運行。 若是一個子組件更新父組件上的一個屬性,做爲對該屬性更改的反應,則會發生無限循環。 固然,正如我以前所說的,使用直接更新或依賴關係來發現這種模式是微不足道的,但在實際應用程序中,更新和依賴關係一般是間接的。
有趣的是,AngularJS沒有單向數據流,所以它試圖穩定樹。 但它一般會致使臭名昭着的10 $ digest()迭代達成。停止! 錯誤。 繼續,谷歌這個錯誤,你會驚訝於這個錯誤產生的問題的數量。
最後一個問題是爲何只在開發模式下運行它? 我想這是由於一個不穩定的模型並不像框架產生的運行時錯誤那樣嚴重。 畢竟它可能穩定在下一個摘要運行。 可是,開發應用程序時可能出現的錯誤比在客戶端上調試正在運行的應用程序更好。

 

 

譯自:Everything you need to know about the `ExpressionChangedAfterItHasBeenCheckedError` errorpromise

https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4
相關文章
相關標籤/搜索