[譯]Angular-關(guān)于`ExpressionChangedAfterItHasBeenCheckedError`你需要知道的一切

文章翻譯已征得原作者同意抵乓,原文鏈接:link

正文

最近在stackoverflow上總看有人問到使用Angular時會報 ExpressionChangedAfterItHasBeenCheckedError 的錯誤悯蝉。大部分提問的人不太明白Angular的變化監(jiān)測機(jī)制,或者不明白為什么需要這個報錯信息峭沦。一些開發(fā)者甚至認(rèn)為這是個bug,但是這其實不是bug足绅。

本篇文章將深入講解導(dǎo)致這個錯誤的原因與被監(jiān)測到的原理压语,還會展示該錯誤經(jīng)常出現(xiàn)的場景,并且給出幾個可行的解決方案编检。在最后一章將會解釋Angular為什么需要這個監(jiān)測機(jī)制。

關(guān)于變化監(jiān)測

每個Angular應(yīng)用都是以組件樹的形態(tài)呈現(xiàn)的扰才。Angular在變化監(jiān)測階段會按以下的順序?qū)γ總€組件執(zhí)行操作(List1):

還有一些其他的操作在變化監(jiān)測階段被執(zhí)行,我在這篇文章中詳細(xì)列出了這些流程:Everything you need to know about change detection in Angular

每一步操作后衩匣,Angular會保存與這次操作有關(guān)的values值蕾总,這個值被存在組件view的 oldValues 屬性中。(開發(fā)模式下)在所有組件完成變化監(jiān)測之后Angular會開始下一個監(jiān)測流程琅捏,第二次監(jiān)測流程并不會再次執(zhí)行上面列出的變化監(jiān)測流程生百,而會比較之前變化監(jiān)測循環(huán)保存的值(存在oldValues中的)與當(dāng)前監(jiān)測流程的值是否一致(List2):

  • 檢查被傳遞到子組件的values(oldValues)與當(dāng)前組件要被用于更新的values(instance.value)是否一致
  • 檢查被用于更新DOM元素的values(oldValues)與當(dāng)前要被用于這些組件更新的values(instance.value)是否一致
  • 對所有子component執(zhí)行相同的檢查

注意:這些額外的檢查(List2)只發(fā)生在開發(fā)模式下,我會在后面的章節(jié)中解釋其中原因柄延。

接下來我們來看一個例子蚀浆。假設(shè)你有一個父組件A和一個子組件B,A組件中有兩個屬性:nametext搜吧,A組件的模板中使用了 name 屬性:

template: '<span>{{name}}</span>'

然后在模板中加入B組件市俊,并且通過輸入屬性綁定給B組件輸入 text 屬性:

@Component({
    selector: 'a-comp',
    template: `
        <span>{{name}}</span>
        <b-comp [text]="text"></b-comp>
    `
})
export class AComponent {
    name = 'I am A component';
    text = 'A message for the child component`;

那么Angular在開始變化監(jiān)測后會發(fā)生什么呢?(List1)變化監(jiān)測會從A組件開始檢查滤奈,第一步將 text 表達(dá)式中的 A message for the child component 向下傳遞到B組件摆昧,并且將這個值存在view上:

view.oldValues[0] = 'A message for the child component';

然后到了變化監(jiān)測列表里的第二步,調(diào)用相應(yīng)的生命周期函數(shù)蜒程。

接下來執(zhí)行第三步绅你,將 {{name}} 表達(dá)式解析為 I am A component 文本。將解析好的值更新到DOM上昭躺,并且存入 oldValues

view.oldValues[1] = 'I am A component';

最后Angular對B組件執(zhí)行相同的操作(List1)忌锯,一旦B組件完成以上的操作,此次變化監(jiān)測循環(huán)便完成了领炫。

如果Angular在開發(fā)模式下運(yùn)行汉规,那么將會執(zhí)行另一個監(jiān)測流程(List2)。text 屬性在傳遞給B組件時的值是 A message for the child component 并存入 oldValues 驹吮,現(xiàn)在想象一下A組件在此之后將 text 的值更新為 updated text针史。然后List2的第一步將會檢查 text 屬性是否被改變:

AComponentView.instance.text === view.oldValues[0]; // false
'updated text' === 'A message for the child component'; // false

這個時候Angular就該拋出這個錯誤了

ExpressionChangedAfterItHasBeenCheckedError

同理碟狞,如果更新已經(jīng)被渲染在DOM中并且被存在 oldValues 中的 name 屬性啄枕,也會拋出相同的錯誤

AComponentView.instance.name === view.oldValues[1]; // false
'updated name' === 'I am A component'; // false

現(xiàn)在你可能會有些疑惑,這些值怎么會被改變呢族沃?我們接著往下看频祝。

數(shù)據(jù)改變的原因

罪魁禍?zhǔn)滓话愣际亲咏M件或指令泌参,下面我們來看一個簡單的案例。我會先用盡可能簡單的例子來重現(xiàn)場景常空,稍后也會給出真實場景下的例子沽一。大家都知道父組件能使用子組件或指令,這里給出一個父組件為A漓糙,子組件為B铣缠,并且B組件有一個綁定屬性 text 。我們將在子組件的 ngOnInit (此時數(shù)據(jù)已綁定)生命周期鉤子中更新 text 屬性:

export class BComponent {
    @Input() text;

    constructor(private parent: AppComponent) {}

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

我們看見了預(yù)期的錯誤:

Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'A message for the child component'. Current value: 'updated text'.

現(xiàn)在我們對被用于父組件模板的 name 屬性做相同的操作:

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

這時候程序并沒有報錯昆禽,為什么會這樣呢蝗蛙?

如果你仔細(xì)看變化監(jiān)測(List1)的執(zhí)行順序,你會發(fā)現(xiàn)子組件的 ngOnInit 將在當(dāng)前component的DOM更新之前被調(diào)用(在記錄oldValues前改變了數(shù)據(jù))醉鳖,這就是為什么上面的例子中更改 name 屬性卻不會報錯捡硅。我們需要一個在DOM中values更新之后的鉤子來做實驗, ngAfterViewInit 是一個不錯的選擇:

export class BComponent {
    @Input() text;

    constructor(private parent: AppComponent) {}

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

我們又一次得到了預(yù)期的錯誤:

AppComponent.ngfactory.js:8 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'I am A component'. Current value: 'updated name'.

當(dāng)然現(xiàn)實中遇到的情況會更加錯綜復(fù)雜盗棵,父組件中屬性在二次監(jiān)測之前被更新通常是使用的外部服務(wù)或observabals間接導(dǎo)致的壮韭。但是其本質(zhì)原因是相同的。

現(xiàn)在我們來看一些真實案例纹因。

共享服務(wù)

例子:plunker泰涂。這個應(yīng)用中父組件和子組件共用一個共享服務(wù),子元素通過共享服務(wù)設(shè)置一個屬性的值并反映到父元素上辐怕。這個模式下子元素改變父元素的值的方式并不像上面簡單例子中那么顯而易見逼蒙,是間接更新了父元素的屬性。

同步事件廣播

例子:plunker寄疏。這個應(yīng)用中父元素監(jiān)聽一個子元素廣播的事件是牢,這個事件導(dǎo)致父元素的屬性被更新,這個屬性又被用于子元素的Input綁定陕截。這同樣間接更新了父元素的屬性驳棱。

動態(tài)的組件實例化

這種模式與之前兩種模式略有不同,前兩種模式都是List2中的第一步檢測拋出的錯誤农曲,而這種模式是由DOM更新檢測(List2第二步)拋出的錯誤社搅。例子:plunker。這個應(yīng)用中父組件在 ngAfterViewInit 生命周期中動態(tài)添加子組件乳规,該生命周期發(fā)生在當(dāng)前組件DOM初次更新之后形葬,而添加子組件將會修改DOM結(jié)構(gòu),那么前后兩次DOM中所使用的values值就不同了(前提是子組件帶有新的value引用)暮的,所以拋出了錯誤笙以。

可行解決方案

如果你仔細(xì)看報錯信息的最后一句:

Expression has changed after it was checked. Previous value:… Has it been created in a change detection hook ?

動態(tài)創(chuàng)建組件的情況下,解決這個問題最好的方案是改變創(chuàng)建組件時所處的生命周期鉤子冻辩。比如之前章節(jié)中動態(tài)創(chuàng)建組件的流程就可以被移到 ngOnInit 中猖腕。即使文檔中說明了 ViewChildren 只能在 ngAfterViewInit 之后被獲取到拆祈,但是創(chuàng)建視圖時就在填充子組件了,所以能提前獲取 ViewChildren 倘感。

如果你google過這個錯誤放坏,那么你應(yīng)該看過一些回答推薦使用異步更新數(shù)據(jù)和強(qiáng)制增加一個變化監(jiān)測循環(huán)兩種方法來解決這個錯誤。即使我把這兩種方法也列出來了老玛,我也更推薦重新設(shè)計你的應(yīng)用而不是使用這兩種方法來解決這個問題淤年,我將會在后面的文章給出理由。

異步更新

你應(yīng)該注意到一件事逻炊,不管是變化監(jiān)測還是第二次的驗證digest都是同步執(zhí)行的。這意味著如果我們在代碼中異步更新屬性的值犁享,那么在第二次驗證循環(huán)運(yùn)行時這些屬性是不會被改變的余素,那么也就不會報錯了。讓我們來試一下:

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 將函數(shù)加入macrotask隊列中桨吊,函數(shù)會在下一個VM周期里被調(diào)用。也可以通過使用promise里的 then 回調(diào)將函數(shù)加入當(dāng)前VM周期其他同步代碼被執(zhí)行完之后:

Promise.resolve(null).then(() => this.parent.name = 'updated name');

Promise.then 并不會被放入macrotask凤巨,而是創(chuàng)建一個microtask视乐。microtask隊列將在當(dāng)前周期中所有同步代碼被執(zhí)行完畢之后執(zhí)行,因此屬性的更新會發(fā)生在驗證步驟之后敢茁。想學(xué)習(xí)更多關(guān)于micro和macro task在Angular中的應(yīng)用可以看這篇文章:I reverse-engineered Zones (zone.js) and here is what I’ve found佑淀。

EventEmitter 傳一個 true 能使事件的emit變?yōu)楫惒剑?/p>

new EventEmitter(true);

強(qiáng)制變化監(jiān)測

另一個解決方案是在父組件A的第一和第二次驗證之間強(qiáng)制加一個變化監(jiān)測循環(huán)。觸發(fā)強(qiáng)制變化監(jiān)測的最佳位置是在 ngAfterViewInit 生命周期內(nèi)彰檬,這時候所有的子組件的流程都已經(jīng)執(zhí)行完畢伸刃,所以隨便在之前的哪個位置改變父組件的屬性都無所謂:

export class AppComponent {
    name = 'I am A component';
    text = 'A message for the child component';

    constructor(private cd: ChangeDetectorRef) {
    }

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

嗯,一樣沒有報錯逢倍,好像可以很開心的運(yùn)行程序了捧颅。其實這里有個問題,當(dāng)在父組件A中觸發(fā)新添加的變化監(jiān)測時较雕,Anuglar同樣會為所有的子組件運(yùn)行一次變化監(jiān)測碉哑,那么父組件可能會被又一次更新。

為什么需要第二次監(jiān)測循環(huán)

Angular強(qiáng)制使用至上而下的單向數(shù)據(jù)流亮蒋,在父元素完成變化監(jiān)測之后不允許內(nèi)部子組件在第二次變化監(jiān)測前改變父組件的屬性扣典。這能確保第一次變化監(jiān)測后的組件樹是穩(wěn)定的。如果在監(jiān)測循環(huán)周期里有屬性的改變導(dǎo)致依賴這些屬性的使用者需要同步更新變化慎玖,那么這棵組件樹就是不穩(wěn)定的激捏。上面例子中子組件B依賴父組件的 text 屬性,每當(dāng)屬性的值改變凄吏,在這些改變被傳遞到B組件之前這棵組件樹都處于不穩(wěn)定的狀態(tài)远舅。這同樣體現(xiàn)在DOM與屬性之間的關(guān)系上闰蛔,DOM作為這些屬性的使用者,然后將這些屬性渲染到UI界面上图柏。如果某些屬性沒有同步更新到界面上序六,用戶將會看到錯誤的界面。

數(shù)據(jù)流的同步過程發(fā)生在文章開頭列出的兩堆操作中蚤吹,所以如果你在數(shù)據(jù)同步過程完成之后再通過子組件修改父組件中的屬性會發(fā)生什么呢例诀?是的,你留下了一個不穩(wěn)定的組件樹裁着,其中數(shù)據(jù)變更的順序?qū)o法預(yù)測繁涂。大部分時候這將會給用戶呈現(xiàn)出一個有錯誤數(shù)據(jù)的頁面,而且問題的排查將十分困難二驰。

可能你會問了扔罪,那為什么不等到組件樹穩(wěn)定之后再進(jìn)行變化監(jiān)測呢?答案很簡單桶雀,組件樹可能永遠(yuǎn)不會穩(wěn)定下來矿酵,一個子組件更新了父組件中的屬性,父組件的屬性又更新子組件的狀態(tài)矗积,子組件狀態(tài)的更新又觸發(fā)更新父組件的屬性...這將是個無限循環(huán)全肮。之前我展示了很多組件對屬性直接更新或依賴的情況,但實際中的應(yīng)用對屬性的更新和依賴通常是間接棘捣,不易排查的辜腺。

有趣的是,AngularJS(Angular 1.x)并沒有使用單向數(shù)據(jù)流也能很大程度的保證組件樹的穩(wěn)定哪自。但是我們經(jīng)常會看到一個臭名昭著的錯誤 10 $digest() iterations reached. Aborting! 瞧毙。隨便去google一下就能找到大量關(guān)于這個錯誤的問題矩动。

最后一個問題是男图,為什么第二次循環(huán)監(jiān)測只在開發(fā)模式下運(yùn)行栈戳?我猜想這是因為數(shù)據(jù)層不穩(wěn)定在框架運(yùn)行時并不會產(chǎn)生引人關(guān)注的錯誤,畢竟數(shù)據(jù)在下一次監(jiān)測循環(huán)后就會穩(wěn)定下來。當(dāng)然,在開發(fā)時期將可能得錯誤解決總好過在上線后的應(yīng)用中排查錯誤韵卤。

初次翻譯略顯生澀,如有錯誤歡迎指出月而。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末世杀,一起剝皮案震驚了整個濱河市肝集,隨后出現(xiàn)的幾起案子瞻坝,更是在濱河造成了極大的恐慌,老刑警劉巖杏瞻,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件所刀,死亡現(xiàn)場離奇詭異衙荐,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)勉痴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進(jìn)店門赫模,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蒸矛,你說我怎么就攤上這事瀑罗。” “怎么了雏掠?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵斩祭,是天一觀的道長。 經(jīng)常有香客問我乡话,道長摧玫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任绑青,我火速辦了婚禮诬像,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘闸婴。我一直安慰自己坏挠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布邪乍。 她就那樣靜靜地躺著降狠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪庇楞。 梳的紋絲不亂的頭發(fā)上榜配,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天,我揣著相機(jī)與錄音吕晌,去河邊找鬼蛋褥。 笑死,一個胖子當(dāng)著我的面吹牛睛驳,可吹牛的內(nèi)容都是我干的烙心。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼柏靶,長吁一口氣:“原來是場噩夢啊……” “哼弃理!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起屎蜓,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤痘昌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辆苔,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡算灸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了驻啤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片菲驴。...
    茶點(diǎn)故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖骑冗,靈堂內(nèi)的尸體忽然破棺而出赊瞬,到底是詐尸還是另有隱情,我是刑警寧澤贼涩,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布巧涧,位于F島的核電站,受9級特大地震影響遥倦,放射性物質(zhì)發(fā)生泄漏谤绳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一袒哥、第九天 我趴在偏房一處隱蔽的房頂上張望缩筛。 院中可真熱鬧,春花似錦堡称、人聲如沸瞎抛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽婿失。三九已至钞艇,卻和暖如春啄寡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背哩照。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工挺物, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人飘弧。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓识藤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親次伶。 傳聞我的和親對象是個殘疾皇子痴昧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 原文傳送門翻譯說明: 本文翻譯采用意譯并對原文進(jìn)行適當(dāng)排版以方便閱讀。術(shù)語采用加粗斜體表示冠王, 術(shù)語第一次出現(xiàn)時其后...
    蕭哈哈閱讀 4,698評論 2 7
  • 模板表達(dá)式“{{}}”不能引用任何全局命名空間中的成員(如:window赶撰、document等等)的原因: 我想原因...
    科研者閱讀 972評論 2 4
  • 組件基礎(chǔ) 組件用來包裝特定的功能,應(yīng)用程序的有序運(yùn)行依賴于組件之間的協(xié)同工作。組件是angular應(yīng)用的最小邏輯單...
    oWSQo閱讀 1,374評論 0 0
  • 彩妝 今日換了粉底豪娜,開心餐胀,女人要為自 己負(fù)責(zé),再懶也要化妝瘤载,哪怕是抹點(diǎn)粉底否灾,涂點(diǎn)口紅,不是為了取悅別人鸣奔,而是為了自...
    行走的小白兔閱讀 179評論 0 0
  • 本人參加#漫步青春#征文活動墨技,作者:余冬海,本人承諾挎狸,文章內(nèi)容原創(chuàng)且未在其他平臺發(fā)布健提。 ...
    南籬舊事_60fc閱讀 151評論 0 0