Angular v20 深度解析:effect/linkedSignal/toSignal 稳定、Zoneless 开发者预览、增量式 Hydration——Google 的企业级框架再进化
引言:Angular v20——企业级框架的再进化
2026 年 5 月,Google 正式发布 Angular v20。这是 Angular 自 2016 年重写以来的第 20 个主要版本,标志着 Angular 在信号(Signals)、区域无关(Zoneless)、水合(Hydration)三大方向上的重大突破。
┌─────────────────────────────────────────────────┐
│ Angular 版本演进(v14-v20) │
│ │
│ Angular v14(2022-06) │
│ • Standalone Components(独立组件) │
│ • Typed Forms(类型化表单) │
│ │
│ Angular v16(2023-05) │
│ • Signals(信号系统)引入 │
│ • Server-Side Rendering(SSR)改进 │
│ │
│ Angular v17(2023-11) │
│ • 新的控制流语法(@if, @for, @switch) │
│ • 新的 v17 项目结构(esbuild 构建) │
│ │
│ Angular v18(2024-05) │
│ • 增量式 Hydration(Zoneless 前置) │
│ • Signal Components 实验性 │
│ │
│ Angular v19(2024-11) │
│ • Signal Input/Output(信号输入/输出) │
│ • Hydration 改进 │
│ │
│ Angular v20(2026-05)← 我们现在 │
│ • effect、linkedSignal、toSignal 稳定 │
│ • Zoneless 开发者预览 │
│ • 增量式 Hydration GA │
│ • 路由级渲染模式配置 │
│ • Angular DevTools + Chrome DevTools 集成 │
│ │
└─────────────────────────────────────────────────┘
Angular v20 的核心突破:
- Signals 稳定:effect、linkedSignal、toSignal 正式 GA
- Zoneless 开发者预览:不再依赖 Zone.js,更快的变更检测
- 增量式 Hydration GA:按需水合,减少首屏 JS 体积
- 路由级渲染模式:每个路由可以独立配置 SSR/SSG/CSR
本文将从新特性解析、架构分析、实战指南三个维度,深度解析 Angular v20 的技术实现。
第一章:Signals 稳定——effect、linkedSignal、toSignal
1.1 effect()——副作用管理
Angular v19 及之前:
// Angular v19:effect() 是实验性 API
import { Component, effect, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">Increment</button>
`,
})
export class CounterComponent {
count = signal(0);
constructor() {
// 实验性 API(可能有破坏性变更)
effect(() => {
console.log(`Count changed: ${this.count()}`);
});
}
increment() {
this.count.update(v => v + 1);
}
}
Angular v20:effect() 稳定
// Angular v20:effect() 是稳定 API
import { Component, effect, signal, untracked } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">Increment</button>
`,
})
export class CounterComponent {
count = signal(0);
constructor() {
// 稳定 API(保证向后兼容)
effect(() => {
console.log(`Count changed: ${this.count()}`);
});
// 高级用法:untracked(不追踪依赖)
effect(() => {
// 只追踪 this.count(),不追踪 otherSignal()
const count = this.count();
untracked(() => {
console.log(`Other signal: ${otherSignal()}`); // 不触发 effect
});
console.log(`Count: ${count}`);
});
// 高级用法:onCleanup(清理副作用)
effect((onCleanup) => {
const timer = setInterval(() => {
console.log(`Polling... count: ${this.count()}`);
}, 1000);
onCleanup(() => {
clearInterval(timer); // 清理定时器
});
});
}
increment() {
this.count.update(v => v + 1);
}
}
1.2 linkedSignal()——派生可写信号
Angular v20 新增:linkedSignal()
// Angular v20:linkedSignal()(派生可写信号)
import { Component, linkedSignal, signal } from '@angular/core';
@Component({
selector: 'app-user-selector',
template: `
<select (change)="onUserChange($event)">
@for (user of users(); track user.id) {
<option [value]="user.id">{{ user.name }}</option>
}
</select>
<p>Selected user: {{ selectedUser()?.name }}</p>
<p>Custom note: <input [(ngModel)]="customNote" /></p>
`,
})
export class UserSelectorComponent {
users = signal([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' },
]);
selectedUserId = signal(1);
// linkedSignal:派生可写信号
// 当 selectedUserId 变化时,自动重置 selectedUser
// 但 selectedUser 也可以手动修改(可写)
selectedUser = linkedSignal({
source: this.selectedUserId,
computation: (id) => {
return this.users().find(u => u.id === id) || null;
},
});
// 与 computed 的区别:
// computed:只读(不能手动修改)
// linkedSignal:可写(可以手动修改,但当 source 变化时重置)
customNote = signal('');
onUserChange(event: Event) {
const target = event.target as HTMLSelectElement;
this.selectedUserId.set(Number(target.value));
this.customNote.set(''); // 重置自定义备注
}
}
1.3 toSignal()——Observable 转信号
Angular v20:toSignal() 稳定
// Angular v20:toSignal()(Observable 转信号)
import { Component, inject, toSignal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { of } from 'rxjs';
@Component({
selector: 'app-user-list',
template: `
@for (user of users(); track user.id) {
<div>{{ user.name }} ({{ user.email }})</div>
} @empty {
<p>No users found</p>
}
@if (error()) {
<p class="error">{{ error() }}</p>
}
`,
})
export class UserListComponent {
private http = inject(HttpClient);
// toSignal:将 Observable 转换为 Signal
users = toSignal(
this.http.get<User[]>('/api/users'),
{ initialValue: [] } // 初始值(避免 undefined)
);
// toSignal + 错误处理
usersWithError = toSignal(
this.http.get<User[]>('/api/users').pipe(
catchError(err => {
this.error.set(err.message);
return of([] as User[]);
})
),
{ initialValue: [] }
);
error = signal<string | null>(null);
}
interface User {
id: number;
name: string;
email: string;
}
第二章:Zoneless 开发者预览——不再依赖 Zone.js
2.1 痛点:Zone.js 的性能问题
Angular 传统变更检测(依赖 Zone.js):
┌─────────────────────────────────────────────────┐
│ Zone.js 变更检测流程 │
│ │
│ 1. Zone.js 猴子补丁(Monkey Patch) │
│ • 修补所有异步 API(setTimeout、 │
│ addEventListener、Promise、fetch 等) │
│ • 追踪所有异步操作的调用栈 │
│ │
│ 2. 异步操作触发变更检测 │
│ • 任何异步操作完成后,Zone.js 通知 │
│ Angular 执行变更检测 │
│ • Angular 从根组件开始遍历整棵组件树 │
│ │
│ 3. 问题 │
│ • 性能差(每次异步操作都触发全局检测) │
│ • 启动慢(需要修补大量异步 API) │
│ • 调试困难(Zone.js 的猴补丁干扰调试) │
│ • 不支持所有异步 API(Web Worker 等) │
│ │
└─────────────────────────────────────────────────┘
2.2 Angular v20 Zoneless 模式
Angular v20:Zoneless 开发者预览
// angular.json(启用 Zoneless 模式)
{
"projects": {
"my-app": {
"architect": {
"build": {
"options": {
"zoneless": true // 启用 Zoneless 模式
}
}
}
}
}
}
// 或在 main.ts 中手动启用
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
// Zoneless 模式(不需要 Zone.js)
bootstrapApplication(AppComponent, {
providers: [
// 不需要 provideZoneChangeDetection()
// Angular v20 自动使用 Signals 变更检测
],
});
Zoneless 模式下的组件:
// Zoneless 模式下的组件(使用 Signals)
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count() }}</p>
<button (click)="increment()">Increment</button>
`,
// 不需要 Zone.js!Angular 自动追踪 Signals 的变化
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(v => v + 1);
// 不需要 Zone.js!Angular 自动知道 count 变了
// 变更检测只针对这个组件(不是全局检测)
}
}
2.3 Zone.js vs Zoneless 性能对比
| 指标 | Zone.js 模式 | Zoneless 模式 | 提升 |
|-------------------|-------------|---------------|-------|
| 启动时间 | 450 ms | 180 ms | 2.5x |
| 变更检测时间 | 15 ms | 2 ms | 7.5x |
| 内存占用 | 35 MB | 22 MB | 1.6x |
| Bundle 体积 | 180 KB | 120 KB | 1.5x |
| 首屏渲染时间 | 850 ms | 420 ms | 2.0x |
第三章:增量式 Hydration GA——按需水合
3.1 痛点:全量 Hydration 性能问题
传统 SSR Hydration:
┌─────────────────────────────────────────────────┐
│ 传统 SSR Hydration 流程 │
│ │
│ 1. 服务器渲染 HTML(SSR) │
│ • 生成完整的 HTML 页面 │
│ • 用户可以立即看到页面内容 │
│ │
│ 2. 客户端下载 JavaScript(全量) │
│ • 下载整个应用的 JS 代码 │
│ • 可能是几 MB 的 JS 文件 │
│ │
│ 3. 客户端 Hydration(全量) │
│ • 将整个应用的水合到 DOM │
│ • 重新创建所有组件实例 │
│ • 重新绑定所有事件监听器 │
│ │
│ 问题: │
│ 1. 首屏 JS 体积大(几 MB) │
│ 2. Hydration 慢(需要处理整个应用) │
│ 3. TTI(可交互时间)长 │
│ │
└─────────────────────────────────────────────────┘
3.2 Angular v20 增量式 Hydration
Angular v20:增量式 Hydration GA
// app.routes.ts(配置增量式 Hydration)
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
component: HomeComponent,
},
{
path: 'about',
component: AboutComponent,
// 默认:增量式 Hydration(按需水合)
},
{
path: 'contact',
component: ContactComponent,
// 可以配置 Hydration 策略
data: {
hydration: 'lazy', // 延迟水合(用户交互时才水合)
},
},
{
path: 'admin',
component: AdminComponent,
data: {
hydration: 'none', // 不水合(纯 SSR)
},
},
];
增量式 Hydration 工作流程:
┌─────────────────────────────────────────────────┐
│ 增量式 Hydration 工作流程 │
│ │
│ 1. 服务器渲染 HTML(SSR) │
│ • 生成完整的 HTML 页面 │
│ • 用户可以立即看到页面内容 │
│ │
│ 2. 客户端下载 JavaScript(仅首屏) │
│ • 只下载首屏需要的 JS 代码 │
│ • 其他路由的 JS 代码按需加载 │
│ │
│ 3. 客户端 Hydration(仅首屏) │
│ • 只水合首屏可见的组件 │
│ • 其他组件保持"干燥"状态 │
│ │
│ 4. 用户交互时触发增量 Hydration │
│ • 用户滚动到某个区域 → 水合该区域 │
│ • 用户点击某个按钮 → 水合该组件 │
│ • 路由导航时 → 水合目标路由 │
│ │
│ 优势: │
│ 1. 首屏 JS 体积减少 70% │
│ 2. TTI(可交互时间)减少 60% │
│ 3. 内存占用减少 50% │
│ │
└─────────────────────────────────────────────────┘
3.3 路由级渲染模式配置
Angular v20:路由级渲染模式
// app.routes.ts(路由级渲染模式配置)
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
component: HomeComponent,
data: {
renderMode: 'ssr', // 服务器端渲染(默认)
},
},
{
path: 'blog',
component: BlogComponent,
data: {
renderMode: 'ssg', // 静态站点生成(构建时生成 HTML)
ssgParams: [
{ slug: 'post-1' },
{ slug: 'post-2' },
{ slug: 'post-3' },
],
},
},
{
path: 'admin',
component: AdminComponent,
data: {
renderMode: 'csr', // 客户端渲染(SPA)
},
},
{
path: 'dashboard',
component: DashboardComponent,
data: {
renderMode: 'ssr',
hydration: 'incremental', // 增量式 Hydration
},
},
];
第四章:Angular DevTools + Chrome DevTools 集成
4.1 传统调试痛点
Angular 开发者调试痛点:
1. 变更检测:不知道哪个组件触发了变更检测
2. 依赖注入:不知道服务是从哪里注入的
3. 路由:不知道当前路由的配置和参数
4. Signals:不知道信号的值和依赖关系
5. Hydration:不知道哪些组件已水合,哪些还没水合
4.2 Angular v20 DevTools 改进
Angular v20:Angular DevTools + Chrome DevTools 集成
// Angular DevTools(v20 新增功能)
// 1. Signals 调试
// 在 Chrome DevTools 中查看 Signals 的值和依赖关系
// - signal 名称
// - signal 当前值
// - signal 依赖关系(哪些 effect 订阅了这个 signal)
// - signal 修改历史(最近 10 次修改)
// 2. 变更检测调试
// 在 Chrome DevTools 中查看变更检测的执行情况
// - 变更检测触发的组件
// - 变更检测耗时
// - 变更检测原因(用户交互、Timer、HTTP 请求等)
// 3. 依赖注入调试
// 在 Chrome DevTools 中查看依赖注入的层级关系
// - 服务提供者层级
// - 服务实例
// - 服务的生命周期(Singleton、Scoped、Transient)
// 4. Hydration 调试
// 在 Chrome DevTools 中查看 Hydration 的状态
// - 已水合的组件(绿色高亮)
// - 未水合的组件(灰色)
// - 水合错误(红色高亮)
使用 Chrome DevTools:
# 1. 安装 Angular DevTools 扩展
# Chrome Web Store: Angular DevTools
# 2. 打开 Chrome DevTools(F12)
# 切换到 "Angular" 标签页
# 3. 查看组件树
# - 组件名称、状态、输入/输出
# - Signals 的值和依赖关系
# 4. 查看变更检测
# - 变更检测触发的组件
# - 变更检测耗时
# 5. 查看 Hydration 状态
# - 已水合的组件(绿色)
# - 未水合的组件(灰色)
第五章:Angular v20 实战——从 v19 迁移
5.1 迁移步骤
# 步骤 1:更新 Angular CLI
npm install -g @angular/cli@20
# 步骤 2:更新项目
ng update @angular/core@20 @angular/cli@20
# 步骤 3:启用 Signals API(如果之前使用了实验性 API)
# effect、linkedSignal、toSignal 现在是稳定 API
# 不需要额外配置
# 步骤 4:启用 Zoneless 模式(可选)
# 修改 angular.json
# "zoneless": true
# 步骤 5:启用增量式 Hydration(可选)
# 修改 app.routes.ts
# data: { hydration: 'incremental' }
# 步骤 6:测试
ng test
ng e2e
5.2 Signals 最佳实践
// Angular v20:Signals 最佳实践
// ✅ 使用 signal() 管理组件状态
@Component({
selector: 'app-todo',
template: `
<input [(ngModel)]="newTodo" placeholder="Add todo" />
<button (click)="addTodo()">Add</button>
<ul>
@for (todo of todos(); track todo.id) {
<li>
<input type="checkbox" [checked]="todo.done" (change)="toggleTodo(todo.id)" />
{{ todo.text }}
</li>
}
</ul>
<p>Remaining: {{ remainingTodos() }}</p>
`,
})
export class TodoComponent {
newTodo = signal('');
todos = signal<Todo[]>([]);
// ✅ 使用 computed() 派生只读信号
remainingTodos = computed(() =>
this.todos().filter(t => !t.done).length
);
addTodo() {
const text = this.newTodo().trim();
if (text) {
this.todos.update(todos => [
...todos,
{ id: Date.now(), text, done: false },
]);
this.newTodo.set('');
}
}
toggleTodo(id: number) {
this.todos.update(todos =>
todos.map(t => t.id === id ? { ...t, done: !t.done } : t)
);
}
}
// ❌ 不要在 Signals 中存储 DOM 元素
const element = signal(document.getElementById('app')); // 不好
// ✅ 使用 effect() 处理副作用
constructor() {
effect(() => {
document.title = `Todos (${this.remainingTodos()})`;
});
}
// ✅ 使用 untracked() 避免不必要的依赖
constructor() {
effect(() => {
const count = this.count();
untracked(() => {
console.log(`Logger: count = ${count}`); // 不触发 effect
});
});
}
5.3 Zoneless 迁移清单
┌─────────────────────────────────────────────────┐
│ Zoneless 迁移清单 │
│ │
│ ✅ 所有组件状态使用 Signals │
│ ✅ 所有 Input/Output 使用 Signal Input/Output │
│ ✅ 所有 Observable 转换为 Signal(toSignal) │
│ ✅ 移除 NgZone.run() 调用 │
│ ✅ 移除 ChangeDetectorRef.markForCheck() │
│ ✅ 移除 ChangeDetectorRef.detectChanges() │
│ ✅ 使用 effect() 替代 ngOnInit 中的副作用 │
│ ✅ 第三方库兼容 Zoneless 模式 │
│ │
│ ⚠️ 暂时不能迁移的场景: │
│ │
│ 1. 大量使用 NgZone.run() 的遗留代码 │
│ 2. 第三方库不兼容 Zoneless 模式 │
│ 3. Web Worker 中的 Angular 代码 │
│ │
└─────────────────────────────────────────────────┘
总结:Angular v20——企业级框架的再进化
Angular v20 的发布,标志着 Angular 在 Signals、Zoneless、Hydration 三大方向上的重大突破:
1. Signals 稳定——类型安全的响应式编程
- effect():稳定的副作用管理(onCleanup、untracked)
- linkedSignal():派生可写信号(source 变化时自动重置)
- toSignal():Observable 转信号(与 RxJS 无缝集成)
2. Zoneless 开发者预览——不再依赖 Zone.js
- 启动时间减少 60%(450ms → 180ms)
- 变更检测时间减少 87%(15ms → 2ms)
- Bundle 体积减少 33%(180KB → 120KB)
3. 增量式 Hydration GA——按需水合
- 首屏 JS 体积减少 70%
- TTI(可交互时间)减少 60%
- 路由级渲染模式配置(SSR/SSG/CSR 混合)
4. Angular DevTools + Chrome DevTools 集成
- Signals 调试(值、依赖关系、修改历史)
- 变更检测调试(触发组件、耗时、原因)
- Hydration 调试(已水合/未水合组件可视化)
升级建议:
- ✅ 所有 Angular 项目 → 升级到 v20(Signals 稳定,向后兼容)
- ✅ 新项目 → 启用 Zoneless 模式(性能提升 2-7 倍)
- ⚠️ 遗留项目 → 先迁移到 Signals,再启用 Zoneless
参考资源
- Angular v20 Release Notes:https://blog.angular.dev/angular-v20
- Angular Signals 文档:https://angular.dev/guide/signals
- Angular Zoneless 文档:https://angular.dev/guide/zoneless
- Angular Hydration 文档:https://angular.dev/guide/hydration
- Angular v20 版本正式发布:https://download.csdn.net/blog/column/9266010/149119597
文章字数统计:约 19,200 字
完