[译] 关于 `ExpressionChangedAfterItHasBeenCheckedError` 错误
原文链接:
Everything you need to know about the
关于 最近 stackoverflow 上几乎每天都有人提到 Angular 抛出的一个错误: 本文将解释引起这个错误的内在原因,检测机制的内部原理,提供导致这个错误的共同行为,并给出修复这个错误的解决方案。最后章节解释为什么数据更新检查是如此重要。 It seems that the more links to the sources I put in the article the less likely people are to recommend it ?. That’s why there will be no reference to the sources in this article.(译者注:这是作者的吐槽,不翻译) 相关变更检测行为一个运行的 Angular 程序其实是一个组件树,在变更检测期间,Angular 会按照以下顺序检查每一个组件(译者注:这个列表称为列表 1):
在变更检测期间还会有其他操作,可以参考我写的文章:《Everything you need to know about change detection in Angular》 。 在每一次操作后,Angular 会记下执行当前操作所需要的值,并存放在组件视图的
记住这个检查只在开发环境下执行,我会在后文解释原因。 让我们一起看一个简单示例,假设你有一个父组件 template: '<span>{{name}}</span>' 同时,还有一个 @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 执行变更检测的时候会发生什么呢?首先是从检查父组件 view.oldValues[0] = 'A message for the child component'; 第二步是执行上面列表 1 列出的执行几个生命周期钩子。(译者注:即调用子组件 第三步是计算模板表达式 view.oldValues[1] = 'I am A component'; 第四步是为子组件 如果处于开发者模式,Angular 还会执行上面列表 2 列出的 digest cycle 循环核查。现在假设当 AComponentView.instance.text === view.oldValues[0]; // false 'A message for the child component' === 'updated text'; // false 结果是发生变化,这时 Angular 会抛出 列表 1 中第三步操作也同样会执行 digest cycle 循环检查,如果 AComponentView.instance.name === view.oldValues[1]; // false 'I am A component' === 'updated name'; // false 你可能会问上面提到的 属性值突变的原因属性值突变的罪魁祸首是子组件或指令,一起看一个简单证明示例吧。我会先使用最简单的例子,然后举个更贴近现实的例子。你可能知道子组件或指令可以注入它们的父组件,假设子组件 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'. 现在我们再同样改变父组件 ngOnInit() { this.parent.name = 'updated name'; } 纳尼,居然没有报错!!!怎么可能? 如果你往上翻看列表 1 的操作执行顺序,你会发现 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 渲染,通常间接是因为使用服务(services)或可观察者(observables)引发的,不过根本原因还是一样的。 现在让我们看看真实世界的案例吧。 共享服务(Shared service)这个模式案例可查看代码 plunker。这个程序设计为父子组件有个共享的服务,子组件修改了共享服务的某个属性值,响应式地导致父组件的属性值发生改变。我把它称为非直接父组件属性更新,因为不像上面的示例,它明显不是子组件立刻改变父组件属性值。 同步事件广播这个模式案例可查看代码 plunker。这个程序设计为子组件抛出一个事件,而父组件监听这个事件,而这个事件会引起父组件属性值发生改变。同时这些属性值又被父组件作为输入属性绑定传给子组件。这也是非直接父组件属性更新。 动态组件实例化这个模式有点不同于前面两个影响的是输入属性绑定,它引起的是 DOM 更新从而抛出错误,可查看代码 plunker。这个程序设计为父组件在 解决方案如果你仔细查看错误描述的最后部分: Expression has changed after it was checked. Previous value:… Has it been created in a change detection hook? 根据上面描述,通常的解决方案是使用正确的生命周期钩子来创建动态组件。例如上面创建动态组件的示例,其解决方案就是把组件创建代码移到 如果你 google 下就知道解决这个错误一般有两种方式:异步更新属性和手动强迫变更检测。尽管我列出这两个解决方案,但不建议这么去做,我将会解释原因。 异步更新这里需要注意的事情是变更检测和核查循环(verification digests)都是同步的,这意味着如果我们在核查循环(verification loop)运行时去异步更新属性值,会导致错误,测试下吧: 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'; }); } } 实际上没有抛出错误(译者注:耍我呢!),这是因为 Promise.resolve(null).then(() => this.parent.name = 'updated name'); 与宏观任务(macrotask)不同, 如果你使用 new EventEmitter(true); 强迫式变更检测另一种解决方案是在第一次变更检测和核查循环阶段之间,再一次迫使 Angular 执行父组件 export class AppComponent { name = 'I am A component'; text = 'A message for the child component'; constructor(private cd: ChangeDetectorRef) { } ngAfterViewInit() { this.cd.detectChanges(); } 很好,没有报错,不过这个解决方案仍然有个问题。如果我们为父组件 为何需要循环核查(verification loop)Angular 实行的是从上到下的单向数据流,当父组件改变值已经被同步后(译者注:即父组件模型和视图已经同步后),不允许子组件去更新父组件的属性,这样确保在第一次 digest loop 后,整个组件树是稳定的。如果属性值发生改变,那么依赖于这些属性的消费者(译者注:即子组件)就需要同步,这会导致组件树不稳定。在我们的示例中,子组件 数据同步过程是在变更检测期间发生的,特别是列表 1 中的操作。所以如果当同步操作执行完毕后,在子组件中去更新父组件属性时,会发生什么呢?你将会得到不稳定的组件树,这样的状态是不可测的,大多数时候你将会给用户展现错误的信息,并且很难调试。 那为何不等到组件树稳定了再去执行变更检测呢?答案很简答,因为它可能永远不会稳定。如果把子组件更新了父组件的属性,作为该属性改变时的响应,那将会无限循环下去。当然,正如我之前说的,不管是直接更新还是依赖的情况,这都不是重点,但是在现实世界中,更新还是依赖一般都是非直接的。 有趣的是,AngularJS 并没有单向数据流,所以它会试图想办法去让组件树稳定。但是它会经常导致那个著名的错误 最后一个问题你可能会问为什么只有在开发模式下会执行 digest cycle 呢?我猜可能因为相比于一个运行错误,不稳定的模型并不是个大问题,毕竟它可能在下一次循环检查数据同步后变得稳定。然而,最好能在开发阶段注意可能发生的错误,总比在生产环境去调试错误要好得多。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |