跳到主要内容

Signals 响应式

Signals 是 Angular 16 引入的全新响应式状态管理方式,它提供了一种细粒度的响应式编程模型,让 Angular 能够更精确地追踪状态变化并优化更新。本章将详细介绍 Signals 的概念和使用方法。

什么是 Signals?

Signal 是一个包装值的容器,当值发生变化时,它会通知所有依赖它的消费者。与传统的响应式方案(如 RxJS)不同,Signals 是同步的、自动追踪依赖的,并且更易于理解和使用。

Signals 的核心优势

  1. 自动依赖追踪:Angular 自动追踪哪些组件使用了哪些 Signal
  2. 细粒度更新:只有真正变化的部分才会重新渲染
  3. 简单的 API:比 RxJS 更容易学习和使用
  4. 同步执行:不需要订阅和取消订阅
  5. 更好的性能:减少不必要的变更检测

可写信号(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);
}
}

小结

  1. Signal 是值的响应式包装器,变化时自动通知消费者
  2. WritableSignal 可以通过 set()update() 修改
  3. Computed Signal 基于其他信号派生值,自动追踪依赖
  4. Effects 用于执行副作用,在信号变化时自动运行
  5. Signal 输入Model 实现了组件间的响应式通信

下一步

掌握了 Signals 后,接下来学习 服务与依赖注入,了解如何组织共享逻辑和数据。