编程 Angular v20 深度解析:effect/linkedSignal/toSignal 稳定、Zoneless 开发者预览、增量式 Hydration——Google 的企业级框架再进化

2026-05-14 04:46:00 +0800 CST views 8

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

参考资源

  1. Angular v20 Release Notes:https://blog.angular.dev/angular-v20
  2. Angular Signals 文档:https://angular.dev/guide/signals
  3. Angular Zoneless 文档:https://angular.dev/guide/zoneless
  4. Angular Hydration 文档:https://angular.dev/guide/hydration
  5. Angular v20 版本正式发布:https://download.csdn.net/blog/column/9266010/149119597

文章字数统计:约 19,200 字

推荐文章

404错误页面的HTML代码
2024-11-19 06:55:51 +0800 CST
HTML + CSS 实现微信钱包界面
2024-11-18 14:59:25 +0800 CST
curl错误代码表
2024-11-17 09:34:46 +0800 CST
Vue3中的Scoped Slots有什么改变?
2024-11-17 13:50:01 +0800 CST
Nginx 防止IP伪造,绕过IP限制
2025-01-15 09:44:42 +0800 CST
mysql时间对比
2024-11-18 14:35:19 +0800 CST
程序员茄子在线接单