Angular 23 的 Zoneless 变更检测是 Angular 架构的一次重大革新。它移除了对 Zone.js 的依赖,让变更检测变得更可预测、更高效。
Zone.js 的历史包袱
Zone.js 从 Angular 2 开始就是变更检测的核心:
javascript
// Zone.js 的工作原理
Zone.current.fork({
onInvokeTask: (delegate, zone, target, task, ...args) => {
// 在异步任务前后自动触发变更检测
const result = delegate.invokeTask(target, task, ...args);
ngZone.runOutsideAngular(() => {
ngZone.run(() => {}); // 触发变更检测
});
return result;
}
});
Zone.js 的问题:
- 性能开销:每个异步操作都可能触发变更检测
- 调试困难:错误堆栈被 Zone.js 包装
- 不可预测:不知道什么时候会触发变更检测
- 体积膨胀:Zone.js 本身约 15KB
Zoneless 工作原理
Zoneless 模式下,变更检测由开发者显式控制:
typescript
@Component({
selector: 'app-counter',
template: `
<div>{{ count }}</div>
<button (click)="increment()">+1</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
// 需要手动标记变更
this.cdr.markForCheck();
}
constructor(private cdr: ChangeDetectorRef) {}
}
或者使用 Signal:
typescript
@Component({
selector: 'app-counter',
template: `
<div>{{ count() }}</div>
<button (click)="increment()">+1</button>
`
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(n => n + 1);
// Signal 自动触发变更检测
}
}
从 Zone.js 迁移
迁移是渐进式的:
步骤 1:启用 Zoneless 模式
typescript
// main.ts
bootstrapApplication(AppComponent, {
providers: [
// 禁用 Zone.js
provideZonelessChangeDetection()
]
});
步骤 2:修复变更检测
typescript
// 之前:依赖 Zone.js 自动检测
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users = users; // Zone.js 自动更新
});
}
// 之后:显式标记变更
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users = users;
this.cdr.markForCheck(); // 手动标记
});
}
步骤 3:使用 Signal(推荐)
typescript
@Component({...})
export class UserListComponent {
users = signal<User[]>([]);
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users.set(users); // Signal 自动更新
});
}
}
Zoneless 的优势
优势 1:性能提升
typescript
// Zone.js 模式:每次异步操作都可能触发变更检测
setTimeout(() => {
this.data = newData; // 触发变更检测
}, 1000);
// Zoneless 模式:只在需要时触发
setTimeout(() => {
this.data = newData;
this.cdr.markForCheck(); // 显式触发
}, 1000);
基准测试结果:
- 初始渲染:提升 20-30%
- 交互响应:提升 15-25%
- 内存占用:减少 10-20%
优势 2:调试更清晰
Zone.js 会包装错误堆栈:
// Zone.js 模式
Error at ZoneDelegate.invokeTask (zone.js:402)
at Zone.runTask (zone.js:175)
at ZoneTask.invoke (zone.js:494)
at timer (zone.js:2295)
at Component.handleClick (component.ts:15) // 真正的错误位置
// Zoneless 模式
Error at Component.handleClick (component.ts:15) // 直接定位
优势 3:更可预测
Zoneless 模式下,变更检测只在以下情况触发:
- 事件处理器执行后
- 调用
markForCheck()后 - Signal 值变化后
- 显式调用
detectChanges()后
Signal 与 Zoneless 的配合
Signal 是 Zoneless 模式的最佳搭档:
typescript
@Component({
selector: 'app-product-list',
template: `
<div *ngFor="let product of filteredProducts()">
{{ product.name }} - {{ product.price }}
</div>
`
})
export class ProductListComponent {
products = signal<Product[]>([]);
searchTerm = signal('');
filteredProducts = computed(() => {
const term = this.searchTerm();
return this.products().filter(p =>
p.name.toLowerCase().includes(term.toLowerCase())
);
});
constructor() {
// 自动响应数据变化
effect(() => {
console.log('产品数量:', this.filteredProducts().length);
});
}
}
Signal 的优势:
- 自动触发变更检测
- 模板中直接使用,无需 getter/setter
- computed 自动缓存,避免重复计算
迁移检查清单
迁移前的检查:
typescript
// 1. 检查是否有直接 DOM 操作
element.nativeElement.innerHTML = '...'; // 需要手动触发变更
// 2. 检查是否有第三方库依赖 Zone.js
// 某些库可能假设 Zone.js 存在
// 3. 检查是否有全局状态修改
window.someGlobalState = ...; // 不会触发变更检测
迁移后的验证:
typescript
// 1. 验证变更检测是否正常
expect(component.cdr).toBeDefined();
// 2. 验证 UI 更新是否正常
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('updated');
// 3. 验证性能提升
const startTime = performance.now();
// 执行操作
const duration = performance.now() - startTime;
expect(duration).toBeLessThan(100);
常见陷阱
陷阱 1:忘记标记变更
typescript
// 错误:修改数据但不标记变更
this.items.push(newItem); // UI 不更新
// 正确:标记变更
this.items.push(newItem);
this.cdr.markForCheck();
陷阱 2:过度使用 markForCheck
typescript
// 不好的做法:每个操作都标记
updateName() {
this.name = 'new name';
this.cdr.markForCheck();
}
updateEmail() {
this.email = 'new email';
this.cdr.markForCheck();
}
// 好的做法:合并操作
updateUser() {
this.name = 'new name';
this.email = 'new email';
this.cdr.markForCheck();
}
陷阱 3:忽略 Signal 的自动更新
typescript
// 不好的做法:混合使用 Signal 和手动标记
count = signal(0);
increment() {
this.count.update(n => n + 1);
this.cdr.markForCheck(); // 多余,Signal 已自动处理
}
// 正确:只用 Signal
count = signal(0);
increment() {
this.count.update(n => n + 1); // 自动更新
}
小结
Angular 23 的 Zoneless 变更检测让应用更高效、更可预测。迁移需要渐进式进行,优先使用 Signal 替代手动标记。2026 年的 Angular 开发,Zoneless 不是可选项,而是推荐的最佳实践。它带来了性能提升、调试改善和更清晰的架构。
