Signals 响应式
Signals 是 Angular 16 引入的全新响应式状态管理方式,它提供了一种细粒度的响应式编程模型,让 Angular 能够更精确地追踪状态变化并优化更新。本章将详细介绍 Signals 的概念和使用方法。
什么是 Signals?
Signal 是一个包装值的容器,当值发生变化时,它会通知所有依赖它的消费者。与传统的响应式方案(如 RxJS)不同,Signals 是同步的、自动追踪依赖的,并且更易于理解和使用。
Signals 的核心优势
- 自动依赖追踪:Angular 自动追踪哪些组件使用了哪些 Signal
- 细粒度更新:只有真正变化的部分才会重新渲染
- 简单的 API:比 RxJS 更容易学习和使用
- 同步执行:不需要订阅和取消订阅
- 更好的性能:减少不必要的变更检测
可写信号(Writable Signals)
创建 Signal
使用 signal() 函数创建可写信号:
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>计数: {{ count() }}</p>
<button (click)="increment()">增加</button>
<button (click)="reset()">重置</button>
`
})
export class CounterComponent {
// 创建一个初始值为 0 的信号
count = signal(0);
increment() {
// 使用 update 方法基于当前值更新
this.count.update(value => value + 1);
}
reset() {
// 使用 set 方法直接设置新值
this.count.set(0);
}
}
Signal 的读取和修改
import { signal, WritableSignal } from '@angular/core';
// 创建信号
const name = signal('张三');
const age = signal(25);
const hobbies = signal(['阅读', '运动']);
// 读取信号值(调用信号函数)
console.log(name()); // 输出: 张三
console.log(age()); // 输出: 25
// 修改信号值
name.set('李四'); // 直接设置新值
age.update(v => v + 1); // 基于当前值更新
// 修改数组/对象(需要创建新引用)
hobbies.update(list => [...list, '编程']);
Signal 的类型
import { signal, WritableSignal, Signal } from '@angular/core';
// WritableSignal - 可写信号
const writableCount: WritableSignal<number> = signal(0);
writableCount.set(5);
writableCount.update(v => v + 1);
// Signal - 只读信号(通过 asReadonly 获取)
const readonlyCount: Signal<number> = writableCount.asReadonly();
// readonlyCount.set(5); // 错误!只读信号不能修改
计算信号(Computed Signals)
计算信号是基于其他信号派生出的只读信号。当依赖的信号变化时,计算信号会自动重新计算。
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-price-calculator',
template: `
<div>
<label>单价: <input type="number" [value]="price()" (input)="onPriceChange($event)"></label>
</div>
<div>
<label>数量: <input type="number" [value]="quantity()" (input)="onQuantityChange($event)"></label>
</div>
<p>总价: {{ total() }} 元</p>
<p>折扣价(9折): {{ discountedTotal() }} 元</p>
`
})
export class PriceCalculatorComponent {
price = signal(100);
quantity = signal(1);
// 计算信号 - 自动追踪依赖
total = computed(() => this.price() * this.quantity());
// 计算信号可以依赖其他计算信号
discountedTotal = computed(() => this.total() * 0.9);
onPriceChange(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.price.set(Number(value) || 0);
}
onQuantityChange(event: Event) {
const value = (event.target as HTMLInputElement).value;
this.quantity.set(Number(value) || 0);
}
}
计算信号的特性
import { signal, computed } from '@angular/core';
const firstName = signal('张');
const lastName = signal('三');
// 1. 惰性求值 - 只有被读取时才计算
const fullName = computed(() => {
console.log('计算 fullName'); // 只有读取 fullName() 时才会打印
return `${firstName()} ${lastName()}`;
});
// 2. 缓存 - 多次读取只计算一次
console.log(fullName()); // 计算并缓存
console.log(fullName()); // 返回缓存值
// 3. 自动更新 - 依赖变化时自动重新计算
firstName.set('李');
console.log(fullName()); // 重新计算
// 4. 动态依赖 - 只有实际读取的信号才是依赖
const showLastName = signal(true);
const displayName = computed(() => {
if (showLastName()) {
return `${firstName()} ${lastName()}`;
}
return firstName(); // 此时 lastName 不是依赖
});
效果(Effects)
Effects 用于在信号变化时执行副作用,如日志记录、数据同步等。
import { Component, signal, effect, inject } from '@angular/core';
import { LoggerService } from './logger.service';
@Component({
selector: 'app-user-settings',
template: `
<input [(ngModel)]="theme" (input)="onThemeChange($event)">
<p>当前主题: {{ theme() }}</p>
`
})
export class UserSettingsComponent {
theme = signal('light');
logger = inject(LoggerService);
constructor() {
// 创建 effect - 当 theme 变化时自动执行
effect(() => {
const currentTheme = this.theme();
this.logger.log(`主题切换为: ${currentTheme}`);
localStorage.setItem('theme', currentTheme);
});
}
onThemeChange(event: Event) {
this.theme.set((event.target as HTMLInputElement).value);
}
}
Effect 的注意事项
import { signal, effect, untracked } from '@angular/core';
const count = signal(0);
const name = signal('张三');
// Effect 在信号变化时执行
effect(() => {
console.log(`计数: ${count()}`);
});
// untracked - 在 effect 中读取信号但不追踪依赖
effect(() => {
const currentCount = count();
// name 的变化不会触发这个 effect
const currentName = untracked(() => name());
console.log(`计数: ${currentCount}, 名称: ${currentName}`);
});
// 清理函数 - effect 重新执行前调用
effect((onCleanup) => {
const timer = setInterval(() => {
console.log('定时器运行中...');
}, 1000);
onCleanup(() => {
clearInterval(timer);
console.log('清理定时器');
});
});
Signals 与组件
在组件中使用 Signals
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-todo-list',
template: `
<input #input placeholder="添加待办" (keyup.enter)="addTodo(input.value); input.value = ''">
<ul>
@for (todo of filteredTodos(); track todo.id) {
<li>
<input type="checkbox" [checked]="todo.completed" (change)="toggleTodo(todo.id)">
<span [style.text-decoration]="todo.completed ? 'line-through' : 'none'">
{{ todo.text }}
</span>
<button (click)="removeTodo(todo.id)">删除</button>
</li>
}
</ul>
<div>
<button (click)="filter.set('all')">全部</button>
<button (click)="filter.set('active')">未完成</button>
<button (click)="filter.set('completed')">已完成</button>
</div>
<p>共 {{ todos().length }} 项,{{ activeCount() }} 项未完成</p>
`
})
export class TodoListComponent {
todos = signal([
{ id: 1, text: '学习 Angular', completed: false },
{ id: 2, text: '编写代码', completed: true },
]);
filter = signal<'all' | 'active' | 'completed'>('all');
// 计算信号 - 过滤后的列表
filteredTodos = computed(() => {
const todos = this.todos();
const currentFilter = this.filter();
switch (currentFilter) {
case 'active':
return todos.filter(t => !t.completed);
case 'completed':
return todos.filter(t => t.completed);
default:
return todos;
}
});
// 计算信号 - 未完成数量
activeCount = computed(() =>
this.todos().filter(t => !t.completed).length
);
addTodo(text: string) {
if (!text.trim()) return;
this.todos.update(todos => [
...todos,
{ id: Date.now(), text: text.trim(), completed: false }
]);
}
toggleTodo(id: number) {
this.todos.update(todos =>
todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
);
}
removeTodo(id: number) {
this.todos.update(todos => todos.filter(t => t.id !== id));
}
}
Signal 输入属性
Angular 17.1+ 支持使用 Signal 作为组件输入:
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `
<div class="card">
<h3>{{ name() }}</h3>
<p>年龄: {{ age() }}</p>
<p>状态: {{ status() }}</p>
</div>
`
})
export class UserCardComponent {
// Signal 输入属性
name = input<string>(); // 可选输入
age = input.required<number>(); // 必需输入
isActive = input<boolean>(false); // 带默认值
// 基于输入的计算信号
status = computed(() => this.isActive() ? '活跃' : '离线');
}
父组件使用:
@Component({
selector: 'app-parent',
template: `
<app-user-card [name]="userName" [age]="userAge" [isActive]="true"></app-user-card>
`,
imports: [UserCardComponent]
})
export class ParentComponent {
userName = '张三';
userAge = 25;
}
Signal 模型输入(双向绑定)
import { Component, model } from '@angular/core';
@Component({
selector: 'app-counter-input',
template: `
<button (click)="decrement()">-</button>
<span>{{ value() }}</span>
<button (click)="increment()">+</button>
`
})
export class CounterInputComponent {
// model 创建可双向绑定的 Signal
value = model<number>(0);
increment() {
this.value.update(v => v + 1);
}
decrement() {
this.value.update(v => v - 1);
}
}
父组件使用双向绑定:
@Component({
selector: 'app-parent',
template: `
<app-counter-input [(value)]="count"></app-counter-input>
<p>父组件计数: {{ count }}</p>
`,
imports: [CounterInputComponent]
})
export class ParentComponent {
count = 10;
}
Signals 与 RxJS
Angular 提供了 Signals 和 RxJS 之间的互操作工具:
import { Component, signal, toSignal, toObservable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Component({
selector: 'app-data',
template: `
@if (data(); as d) {
<p>{{ d.name }}</p>
} @else {
<p>加载中...</p>
}
`
})
export class DataComponent {
private http = inject(HttpClient);
// Observable 转 Signal
data$ = this.http.get<{ name: string }>('/api/user');
data = toSignal(this.data$, { initialValue: null });
// Signal 转 Observable
count = signal(0);
count$ = toObservable(this.count);
}
最佳实践
何时使用 Signals
推荐使用 Signals 的场景:
- 组件的本地状态管理
- 需要在模板中显示的响应式数据
- 基于其他状态派生的计算值
- 需要细粒度更新的场景
继续使用 RxJS 的场景:
- 复杂的异步操作(如 HTTP 请求)
- 需要操作符链式处理的数据流
- 事件防抖、节流等场景
- 多播和复杂的订阅管理
性能优化
import { Component, signal, computed, untracked } from '@angular/core';
@Component({
selector: 'app-optimized'
})
export class OptimizedComponent {
items = signal<Array<{ id: number; name: string; data: any }>>([]);
// 好:计算信号自动缓存
itemCount = computed(() => this.items().length);
// 避免:在计算信号中执行昂贵操作
// 每次依赖变化都会重新执行
expensiveComputed = computed(() => {
const items = this.items();
// 如果这个操作很昂贵,考虑使用 memo 或其他优化方式
return items.map(item => JSON.stringify(item));
});
// 好:使用 untracked 避免不必要的依赖追踪
logCurrentState() {
const items = untracked(() => this.items());
console.log('当前项目数:', items.length);
}
}
小结
- Signal 是值的响应式包装器,变化时自动通知消费者
- WritableSignal 可以通过
set()和update()修改 - Computed Signal 基于其他信号派生值,自动追踪依赖
- Effects 用于执行副作用,在信号变化时自动运行
- Signal 输入 和 Model 实现了组件间的响应式通信
下一步
掌握了 Signals 后,接下来学习 服务与依赖注入,了解如何组织共享逻辑和数据。