前言
在Angular开发过程中,想必你一定遇到过该种错误
Expression has changed after it was checked是在Angular中经典的变更检测的错误,而这句话的意思是这些更改在检测之后发生了变化。接下来我们聊一聊Angular中变更检测。
复现错误代码
// 父组件
@Component({
selector: 'app-parent',
template: `<app-child [data]="value"></app-child>`
})
export class ParentComponent implements AfterViewInit {
value = 'Initial';
ngAfterViewInit() {
// 在视图初始化后修改绑定属性
this.value = 'Modified after check';
}
}
// 子组件
@Component({
selector: 'app-child',
template: `{{ data }}`
})
export class ChildComponent {
@Input() data: string;
}
什么是变更检测?
变更检测是框架用于同步组件数据状态与视图的核心机制。其核心目标是自动检测数据变化并更新 DOM,确保视图与底层数据保持一致性,开发者无需手动操作 DOM。也就是在MVVM框架中数据驱动视图的核心。
如何触发变更检测?
用户交互触发(点击事件)
点击按钮后自动触发变更检测,更新按钮显示的文本。
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `<button (click)="updateText()">{{ displayText }}</button>`
})
export class AppComponent {
displayText = '初始文本';
updateText() {
this.displayText = '更新后的文本';
}
}
异步操作触发(定时器)
通过 setTimeout 模拟异步操作,当定时器回调完成,自动执行变更检测更新 DOM。
import { Component } from '@angular/core';
@Component({
template: `<div>{{ asyncData }}</div>`
})
export class AppComponent {
asyncData = '加载中';
constructor() {
setTimeout(() => {
this.asyncData = '数据已加载'; // 2秒后自动触发变更检测
}, 2000);
}
}
HTTP 请求完成触发
当 HTTP 请求返回数据后,Angular 会自动触发变更检测以更新视图。
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user',
template: `<div>{{ userData | json }}</div>`
})
export class UserComponent {
userData: any;
constructor(private http: HttpClient) {
// HTTP请求完成后自动触发变更检测
this.http.get('xxxxxxxxx').subscribe(data => {
this.userData = data; // 数据更新后视图自动刷新
});
}
}
EventEmitter 事件触发
通过EventEmitter
发射事件时,若父组件监听到事件并修改数据,会触发变更检测。
子组件:
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-child',
template: `<button (click)="sendMessage()">发送消息</button>`
})
export class ChildComponent {
@Output() messageEvent = new EventEmitter<string>();
sendMessage() {
this.messageEvent.emit('新消息'); // 触发事件发射
}
}
父组件
@Component({
selector: 'app-parent',
template: `
<app-child (messageEvent)="onMessageReceived($event)"></app-child>
<div>{{ receivedMessage }}</div>
`
})
export class ParentComponent {
receivedMessage: string = '';
onMessageReceived(message: string) {
this.receivedMessage = message; // 数据更新后视图自动刷新
}
}
Angular中的变更检测策略
在Angular中存在两种变更检测策略,Default
和OnPush
,默认情况下使用的为Default
策略。
Default
以一段简单的代码为例
父组件
@Component({
selector: 'app-root',
template: `
<h2>父组件计数器: {{ counter }}</h2>
<button (click)="increment()">增加计数器</button>
<app-child [childCounter]="counter"></app-child>
`
})
export class AppComponent {
counter = 0;
increment() {
this.counter++;
}
}
子组件
@Component({
selector: 'app-child',
template: `<h3>子组件接收的计数器: {{ childCounter }}</h3>`
})
export class ChildComponent {
@Input() childCounter: number;
}
两者的示意图如下
当用户点击父组件的按钮时,触发increment()
方法更新counter
值,开始执行变更检测
从根组件开始,按以下顺序执行变更检测
- 检测父组件:检查
AppComponent
的所有绑定(如{{ counter }}
),发现counter
值变化,更新 DOM。 - 检测子组件:遍历到子组件
ChildComponent
,检查输入属性childCounter
的绑定值是否变化。若变化,更新子组件 DOM。 - 整个组件树检测完毕后,视图与数据状态完全同步。
但在实际场景中根组件下面往往会挂载多个父组件,父组件下面又会挂载子组件,可能如下图所示
当组件树中任一组件触发变更检测时,Default 策略会检查整个组件树,从根组件开始递归遍历,检测到对应的变更后更新DOM,达到数据与视图的一致。
从这一点可以看到一个Default的一个缺点
即使只有组件A发生了变化,组件B、C、D都没有发生变化,仍然会从根组件开始遍历全部组件,倘若这个组件树的层次很深,节点又很多,使用Default策略就会有不必要的性能浪费了。于是我们迎来了OnPush
策略
OnPush
在@Compontent下使用changeDetection来启用OnPush
策略
@Component({
selector: 'app-A',
// 设置变化检测的策略
changeDetection: ChangeDetectionStrategy.OnPush,
template: ...
})
export class AComponent { ...
}
场景一:父组件A开启OnPush策略,父组件B触发了变更检测
执行流程
- 父组件 B 触发变更检测(例如用户事件、异步操作完成),此时 Angular 会从根组件开始启动变更检测流程。
- 检测到父组件A,父组件 A 启用
OnPush
策略,但此次触发源来自父组件 B。因此,父组件 A 及其子树(A-1、A-2)会被跳过,不执行任何检测逻辑。 - 检测到父组件B,由于父组件B采用的仍然是
Default
的策略,将对B、B-1、B-2都进行检测。
从这里可以得到两点信息
- 倘若开启OnPush策略的组件没有触发变更检测,那么会直接跳过。
- 父组件开启的OnPush策略会影响到其子组件。
场景二:父组件A开启OnPush策略,父组件A触发了变更检测
再说场景二之前,我们先明确哪些行为可以让设置了OnPush
的组件发生变更检测
- 输入属性引用变更
// 父组件
@Component({
template: `<child [data]="parentData"></child>`
})
export class ParentComponent {
parentData = { value: 'initial' };
updateData() {
this.parentData = { ...this.parentData, value: 'updated' }; // 创建新对象
}
}
// 子组件(OnPush策略)
@Component({
selector: 'child',
template: `{{ data.value }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
@Input() data: { value: string };
}
- 父组件通过
updateData
方法生成新的parentData
对象(引用变更)。 - 子组件因
@Input
属性引用变化,触发其变更检测,更新视图显示updated
。
- 内部事件触发
// 子组件(OnPush策略)
@Component({
selector: 'counter',
template: `
<button (click)="increment()">Increment</button>
{{ count }}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
increment() {
this.count++; // 修改内部状态
}
}
- 用户点击按钮触发
click
事件,Angular 自动执行变更检测,更新视图显示最新的count
值。
- 手动标记检测
// 组件(OnPush策略)
@Component({
template: `{{ dynamicValue }}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManualComponent {
dynamicValue = 'initial';
constructor(private cdr: ChangeDetectorRef) {}
updateWithoutRefChange() {
this.dynamicValue = 'updated'; // 直接修改属性值
this.cdr.detectChanges(); // 强制立即检测
// 或 this.cdr.markForCheck(); // 标记待检测(下次周期执行)
}
}
- 使用
detectChanges
或者markForCheck
进行变更检测。
回头开头当父组件A满足上述的条件时,整个变更检测的流程如下
- 从根组件开始,然后到父组件 A 开始,按深度优先顺序向下检测其所有子组件(包括 A-1、A-2),无论子组件是否使用
OnPush
策略。 - 然后再检测父组件B以及下面的子组件。
关于ChangeDetectorRef
回到开头中关于Expression has changed after it was checked的错误,解决方式大多数是使用ChangeDetectorRef
的detectChanges
进行强制变更检测。
作用:手动控制组件的变更检测,允许开发者优化性能,减少不必要的检测周期,或在特定场景下强制触发更新。
下面介绍一下它的几个相关方法
detectChanges
作用:手动触发当前组件及其子组件的变更检测,适用于需要立即更新视图的场景。
在之前的错误例子中使用detectChanges
便可解决
// 父组件
@Component({
selector: 'app-parent',
template: `<app-child [data]="value"></app-child>`
})
export class ParentComponent implements AfterViewInit {
value = 'Initial';
// 注入ChangeDetectorRef
constructor(private cdr: ChangeDetectorRef) {}
ngAfterViewInit() {
// 在视图初始化后修改绑定属性
this.value = 'Modified after check';
this.cdr.detectChanges();//强制更新
}
}
// 子组件
@Component({
selector: 'app-child',
template: `{{ data }}`
})
export class ChildComponent {
@Input() data: string;
}
markForCheck
作用:标记当前组件及其父组件需要在下一次变更检测周期中被检查。主要与 OnPush 变更检测策略配合使用。
尽管父组件 A 是OnPush
策略,但因其被标记为待检测,所以再下一轮变更检测周期中,Angular 从根组件开始,按深度优先顺序遍历组件树。
detach
作用:将当前组件的变更检测从全局检测周期中分离。
- 分离后,组件不会自动更新视图,需手动调用 detectChanges
- 用于性能优化,避免不必要检测(如高频事件、静态组件)。
reattach
作用:将组件重新附加到全局变更检测周期。恢复自动检测,与 detach() 配合使用。
总结
本文介绍了Angular的变更检测以及变更检测的策略(Default
和OnPush
)以及如何进行手动控制组件的变更检测(ChangeDetectorRef
),在日常开发中变更检测是最常见的概念之一,希望能帮到大家针对这块知识有所加深理解。