为什么在 ngAfterContentInit() 中使用 setTimeOut,0? 开发者实现的行为在竞争条件下如果状态不一致

问题描述

添加底部的这段代码的目的是对 ContentChildren 中的更改做出反应,ContentChildren 是链接<td> 元素的指令。摘自一本书,有以下一行:

setTimeout(() => this.updateContentChildren(this.modelProperty),0);

据我所知,这是一个奇特的结构。我不明白为什么作者添加一个延迟为 0 的超时。相反,他可以只用方法更新 ContentChildren:

this.updateContentChildren(this.modelProperty)

然而他没有这样做,我不明白为什么。谁能解释一下?

@Directive({
    selector: "table"
})
export class PaCellColorSwitcher {

    @Input("paCellDarkColor")
    modelProperty: Boolean;

    @ContentChildren(PaCellColor,{descendants: true})
    contentChildren: QueryList<PaCellColor>;

    ngOnChanges(changes: { [property: string]: SimpleChange }) {
        this.updateContentChildren(changes["modelProperty"].currentValue);
    }

    ngAfterContentinit() {
        this.contentChildren.changes.subscribe(() => {
            setTimeout(() => this.updateContentChildren(this.modelProperty),0);
        });
    }

    private updateContentChildren(dark: Boolean) {
        if (this.contentChildren != null && dark != undefined) {
            this.contentChildren.forEach((child,index) => {
                child.setColor(index % 2 ? dark : !dark);
            });
        }
    }

更新:当我在没有 setTimeOut(,0) 函数的情况下运行它时,我得到一个 ExpressionChangedAfterItHasBeenCheckedError,但我不明白为什么?

解决方法

首先,我想说这是不好的做法,你应该避免它。不过我不会撒谎,我自己也用过几次这个技巧。

之前的开发者加了 setTimeout 是因为 A,各种(Angular)组件的行为之间可能存在竞争条件,或者 B,一个组件在一个变更检测周期中改变了另一个组件的状态(由错误)。

开发者实现的行为

首先,我需要解释开发者通过将他的函数设置为一个零延迟的 setTimeout 来实现什么。 Javascript 是一种单线程语言,这意味着默认情况下 JS 引擎一次只做一件事。

超级简化(有点错误)的解释方式是,JS 引擎有一个要在列表中执行的语句列表,并且它总是选择第一个。当您在 setTimeout 中设置某些内容时,您将该语句(您的函数调用)放入此任务列表的末尾,因此它将在其他所有内容“排队”等待执行时执行,直到该点已被处理。

YouTube 上有一个关于此的精彩视频:What the heck is the event loop anyway?,我强烈建议您去看这个视频!

在竞争条件下

您的两个组件之间可能存在竞争条件,为了演示,假设 JS 引擎“任务列表”如下所示:

  • 组件 A 做了一些事情
  • 组件 A 想在它的子组件中设置一些东西:组件 B
  • Angular 运行一个 CD 周期并尝试渲染您的组件
  • 子组件 B 做了一些事情

这里的问题是在步骤 2 中您的子组件 (B) 尚未创建,因此尝试使用它会引发错误。通过将修改组件 B 的语句放在 setTimeout 中,您的任务列表将如下所示:

  • 组件 A 做了一些事情
  • Angular 运行一个 CD 周期并尝试渲染您的组件
  • 子组件 B 做了一些事情
  • 组件 A 想在它的子组件中设置一些东西:组件 B

这样你的 B 组件在创建时就会存在。

如果状态不一致

Angular 运行一个所谓的变更检测周期来检查应用程序状态发生了什么变化以及 UI 需要如何更新。在开发人员模式下,此检查在同一周期内运行两次,并比较输出。如果存在差异,框架将抛出​​上述 ExpressionChangedAfterItHasBeenCheckedError 错误以警告您这一点。

此时,你可以对自己说,这个问题不会出现在 prod 中,所以我很好,但你不好这个问题会导致性能恶化,因为 Angular 将运行更改检测周期比应有的多,因为它认为某些事情已经发生了变化,而实际上您并不打算更改任何内容。所以你应该追踪这个问题的路径原因并修复它。

Angular 官方网站有dedicated document page for this with a video guide 介绍如何解决问题。还有一个详细的 Stackoverflow 答案 here 关于这种行为以及为什么要检查。

因此,对于您的原始问题,通过添加 setTimeout 开发人员欺骗 Angular 通过此双重检查,因为 updateContentChildren 函数仅在当前更改检测完成后才会执行。这意味着您的内部状态始终比 UI“提前一秒”,因为某些工作总是在 CD 周期完成后完成。

,

这可能是有原因的。 setTimeOut 是一个异步函数,它将在同步函数完成时执行。 前段时间我使用了一个异步渲染组件的库,但是它有一个错误,并在右侧放置了一个组件。 所以我想用打字稿的样式来改变它,但这样做并没有看到任何变化。为什么?因为我的代码是同步的,所以在我把它放在左边之后,库把我的组件放在右边。最简单的解决方案是将样式放在 10 的 setTimeOut 中。它奏效了