Signal 基础
Signals 是 Angular 16 引入的响应式原语:可读写的 signal、派生的 computed、副作用 effect。用于组件内状态与跨组件细粒度更新,见 变更检测。
核心概念
Signal 是一个包裹了值的容器,读取时要像函数一样调用(count()),而不是像普通变量那样直接访问。这个「调用」的动作让 Angular 能够追踪「谁读取了这个 Signal」,从而在值变化时只通知真正依赖它的地方(模板绑定、computed、effect),而不必对整棵组件树做脏检查。这是 Signal 相比传统 Zone.js 变更检测更「精细」的根本原因。
signal 与 computed
import { signal, computed } from "@angular/core";
const count = signal(0);
const double = computed(() => count() * 2);
count.set(3);
count.update((n) => n + 1);
- 在模板或类中通过
count()读取(函数调用语法)。 - 写入使用
.set/.update,不要直接修改 signal 返回的对象引用(对象类型应替换不可变副本)。 computed是惰性且带缓存的:只有在依赖的 signal 变化后,下次读取才会重新计算;多次连续读取而依赖未变时直接返回缓存值。
对象与数组类型的更新
interface Todo { id: string; text: string; done: boolean }
const todos = signal<Todo[]>([]);
// 错误:原地修改,Angular 无法感知变化
// todos()[0].done = true;
// 正确:产生新数组/新对象
todos.update((list) =>
list.map((t) => (t.id === targetId ? { ...t, done: true } : t))
);
// 新增
todos.update((list) => [...list, { id: crypto.randomUUID(), text: "新任务", done: false }]);
effect
import { effect } from "@angular/core";
effect(() => {
console.log("count =", count());
});
effect 在依赖的 signal 变化后运行;Angular 19 起已移除 allowSignalWrites 选项,effect 内部默认即可写 signal,无需额外配置。仍需避免在 effect 中产生无法追踪的副作用循环(如在 effect 内部又 set 了自己读取的同一个 signal,形成死循环)——出现这种需求时,通常说明该逻辑更适合用 computed/linkedSignal 表达。
@Component({ standalone: true, template: `` })
export class SyncTitleComponent {
title = signal("首页");
constructor() {
effect(() => {
document.title = `${this.title()} · MyApp`; // 典型用途:同步到浏览器 API
});
}
}
effect 更适合「同步到外部世界」(DOM 标题、localStorage、第三方库实例),业务派生数据优先用 computed,避免 effect 滥用成隐式的数据流。
与 Zone 变更检测
Signal 更新可参与 Angular 的 signal-based 变更检测路径,减少不必要的全树检查(与 OnPush 等配合)。默认应用仍加载 Zone.js;是否启用 zoneless 以项目 angular.json 与官方实验文档为准。
input 作为 signal
title = input.required<string>();
subtitle = input<string>("默认副标题"); // 带默认值的可选输入
// 模板:{{ title() }}
input() 返回的是 Signal,与 @Input() 装饰器相比类型推断更好(Angular 17.1+)。可以直接在 computed 中组合多个 input:
@Component({
standalone: true,
template: `<h2>{{ heading() }}</h2>`,
})
export class HeaderComponent {
title = input.required<string>();
count = input(0);
heading = computed(() => `${this.title()}(${this.count()})`);
}
model:双向绑定的 Signal(Angular 17.2+)
@Component({
selector: "app-toggle",
standalone: true,
template: `<button (click)="checked.set(!checked())">{{ checked() ? '开' : '关' }}</button>`,
})
export class ToggleComponent {
checked = model(false); // 支持 [(checked)]="parentValue" 双向绑定
}
model() 生成的 Signal 既可读也可写,且父组件用 [(checked)] 语法即可完成双向绑定,替代了旧版 @Input() + @Output() 手写 xxxChange 事件的模式。
最佳实践
- 派生值优先用
computed,不要用effect去「模拟」派生(会导致多一次渲染周期、更难追踪数据流)。 - 服务中的可变状态保持
private,只导出asReadonly()或computed(见 状态管理)。 - 组件的
@Input()逐步迁移为input(),尤其是新写的 Standalone 组件,类型推断与只读语义更清晰。 effect中若要写其它 signal,明确评估是否会形成循环依赖(Angular 19+ 已默认允许 effect 内写 signal,不再需要allowSignalWrites)。- 避免在模板里直接写复杂表达式,抽成
computed,便于测试与复用。
常见坑
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 视图没更新 | 原地修改了对象/数组,signal 引用未变 | 使用 update 产生新引用 |
computed 报错「无法在渲染期间写入」 | 在 computed 函数体内调用了 .set() | computed 应是纯函数,只读不写 |
| effect 无限触发 | effect 内 set 了自己也读取的 signal | 拆分读/写依赖,或改用 computed |
| 忘记调用函数直接把 signal 传给模板判断 | 写成 @if (count) 而非 @if (count()) | signal 必须调用才能取值 |
| SSR 下 effect 报错或未执行 | effect 默认与视图注入上下文绑定,SSR/测试环境上下文不同 | 参考官方 SSR/Testing 章节调整调用位置 |
延伸阅读
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| Angular Signals 指南 | 官方 |
| computed | API |
| effect | API |
| input() | Signal 输入 |
| model() | 双向绑定 Signal |
相关文章
RxJS 互操作
Angular 内置 RxJS 处理异步流(HTTP、HttpClient、路由事件、FormControl 的 valueChanges 等)。Signal 适合同步/短链路状态;长生命周期流仍常用 Observable。涵盖 async 管道、toSignal/toObservable、常用操作符、最佳实践与常见坑。
生态集成
Angular 官方维护路由、HTTP、表单、动画等;UI 与状态扩展由社区与官方 Material 团队提供。涵盖官方/半官方库、常用第三方库、与 Vue/React 生态对照、选型建议与常见坑。
表单
Angular 提供 响应式表单(Reactive Forms) 与 模板驱动表单(Template-driven)。复杂表单推荐 Reactive Forms(模型在 TypeScript 中,易测试)。涵盖 FormGroup/FormArray、Typed Forms、自定义校验器、最佳实践与常见坑。
状态管理
Angular 没有强制单一状态库:服务 + DI、Signal、RxJS 即可覆盖多数场景;大型应用可选用 NgRx。涵盖选型、服务作为状态容器、跨组件共享、NgRx 与 SignalStore、常见坑。
HTTP 与数据
Angular 通过 HttpClient(@angular/common/http)发起 HTTP 请求,返回 RxJS Observable。见 RxJS 互操作、依赖注入。
变更检测
Angular 在数据变化后更新 DOM 的过程称为 变更检测(Change Detection)。理解默认策略、Zone.js 机制、OnPush 与 Signal 协同,有助于性能调优。运行时技巧见 runtime。
Series
signals
1 / 2