15_響應式編程核心:Signals 與異步管理
響應式編程核心:Signals與異步管理
Angular v16引入的Signals標志著框架響應式編程范式的重大演進。作為一種新的狀態管理原語,Signals提供了更直觀、更高效的方式來管理應用狀態和處理異步數據,解決了傳統RxJS Observable在某些場景下的復雜性問題。本章將全面解析Signals全家桶API,深入探討異步數據處理策略,并通過實戰案例展示基于Signals的狀態管理方案。
1 Signals全家桶API:響應式狀態的基石
Signals是一種包含值的特殊對象,當值發生變化時會自動通知其依賴者。這種自動追蹤依賴的特性,讓狀態管理變得更加簡單直觀,同時保持了高效的更新機制。
1.1 基礎操作:創建與更新信號
信號的創建與讀取
signal()函數用于創建一個可寫信號,初始值通過參數指定。讀取信號值時需調用信號(signal()),這一設計確保了依賴追蹤的準確性:
import { signal } from '@angular/core';
// 創建信號
const count = signal(0);
const user = signal({ name: '張三', age: 30 });
const isLoading = signal(false);
// 讀取信號值(必須調用信號)
console.log(count()); // 輸出: 0
console.log(user().name); // 輸出: "張三"
注意:信號值的讀取是一個主動操作(需調用
()),這與普通變量不同,確保了Angular能準確追蹤信號的依賴關系。
信號的更新方法
Signals提供了三種更新值的方法,適用于不同場景:
-
set():直接設置新值
適用于完全替換信號值的場景:// 基本類型更新 count.set(1); // 對象類型更新(完全替換) user.set({ name: '李四', age: 25 }); -
update():基于當前值計算新值
接收一個函數,以當前值為參數,返回新值,適用于需要基于舊值計算新值的場景:// 基本類型更新 count.update(current => current + 1); // 自增1 // 對象類型更新 user.update(current => ({ ...current, age: current.age + 1 })); // 年齡自增 -
mutate():直接修改當前值
接收一個函數,直接修改當前值(僅適用于對象或數組),避免創建新對象,適用于大型復雜對象的局部更新:// 直接修改對象屬性(無需創建新對象) user.mutate(current => { current.age = 31; // 直接修改屬性 }); // 操作數組 const items = signal(['a', 'b', 'c']); items.mutate(current => { current.push('d'); // 直接修改數組 current[0] = 'A'; });
更新方法對比:
| 方法 | 適用場景 | 優點 | 注意事項 |
|---|---|---|---|
set() |
完全替換值 | 簡單直接 | 對于對象類型會創建新引用 |
update() |
基于舊值計算新值 | 純函數方式,無副作用 | 對于復雜對象可能產生性能開銷 |
mutate() |
大型對象/數組的局部修改 | 性能好,避免創建新對象 | 破壞不可變性,僅建議內部使用 |
1.2 高級能力:計算信號與副作用
計算信號(computed())
computed()用于創建基于其他信號的衍生信號,當依賴的信號發生變化時,計算信號會自動更新:
import { signal, computed } from '@angular/core';
// 基礎信號
const firstName = signal('張');
const lastName = signal('三');
const price = signal(100);
const quantity = signal(5);
// 計算信號:拼接姓名
const fullName = computed(() => `${firstName()} ${lastName()}`);
// 計算信號:計算總價
const totalPrice = computed(() => {
console.log('計算總價'); // 僅在price或quantity變化時執行
return price() * quantity();
});
// 使用計算信號
console.log(fullName()); // 輸出: "張 三"
console.log(totalPrice()); // 輸出: 500
// 更新依賴信號,計算信號自動更新
price.set(120);
console.log(totalPrice()); // 輸出: 600(自動重新計算)
計算信號的特性:
- 惰性計算:僅在首次讀取或依賴信號變化后首次讀取時計算
- 緩存機制:依賴未變化時返回緩存值,避免重復計算
- 只讀性:計算信號無法直接修改,只能通過更新其依賴的信號間接修改
副作用(effect())
effect()用于創建響應信號變化的副作用(如日志記錄、DOM操作等),當依賴的信號變化時自動執行:
import { signal, effect } from '@angular/core';
const count = signal(0);
// 創建副作用
const stopEffect = effect(() => {
console.log(`Count changed to: ${count()}`);
// 可以執行DOM操作、日志記錄等副作用
});
// 觸發副作用
count.set(1); // 輸出: "Count changed to: 1"
count.update(c => c + 1); // 輸出: "Count changed to: 2"
// 停止副作用(可選)
stopEffect();
count.set(3); // 無輸出(副作用已停止)
副作用的高級配置:
import { effect, Injector } from '@angular/core';
// 配置副作用
effect(() => {
console.log(`User: ${user().name}`);
}, {
injector: myInjector, // 指定注入器
manualCleanup: true, // 需要手動清理
allowSignalWrites: false // 是否允許在副作用中修改信號(默認false)
});
最佳實踐:避免在
effect()中修改信號,除非明確設置allowSignalWrites: true,否則會導致無限循環或不可預期的行為。
關聯信號(linkedSignal())
Angular v17+引入的linkedSignal()用于創建與現有信號同步的信號,適用于需要在不同上下文間共享信號的場景:
import { signal, linkedSignal } from '@angular/core';
// 原始信號
const original = signal(10);
// 創建關聯信號
const linked = linkedSignal(original);
console.log(linked()); // 輸出: 10
// 更新原始信號,關聯信號同步變化
original.set(20);
console.log(linked()); // 輸出: 20
// 更新關聯信號,原始信號同步變化
linked.set(30);
console.log(original()); // 輸出: 30
linkedSignal()與普通信號的區別在于它不存儲值,而是與源信號保持雙向同步,適用于組件間共享狀態但不希望直接暴露原始信號的場景。
2 異步數據處理:從Observable到Signals
在Angular中,異步數據(如HTTP請求)傳統上使用RxJS Observable處理。Signals提供了與Observable的無縫集成,同時解決了訂閱管理的復雜性問題。
2.1 Observable轉Signal:toSignal()
toSignal()將Observable轉換為Signal,自動管理訂閱生命周期,簡化異步數據處理:
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface User {
id: number;
name: string;
}
@Component({
selector: 'app-user',
standalone: true,
imports: [],
template: `
@if (users(); as users) {
<ul>
@for (user of users; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
} @else if (users.loading) {
<p>加載中...</p>
} @else if (users.error) {
<p>錯誤: {{ users.error.message }}</p>
}
`
})
export class UserComponent {
private http = inject(HttpClient);
// 獲取用戶列表的Observable
private users$: Observable<User[]> = this.http.get<User[]>('/api/users');
// 轉換為Signal(帶加載和錯誤狀態)
users = toSignal(this.users$, {
initialValue: undefined, // 初始值
manualCleanup: false // 自動清理訂閱
});
}
toSignal()的關鍵特性:
- 自動訂閱/取消訂閱:組件銷毀時自動取消訂閱,避免內存泄漏
- 狀態包裝:返回的信號包含
loading和error狀態,簡化異步UI處理 - 初始值:通過
initialValue指定初始狀態,避免undefined檢查
2.2 資源API:自動管理異步操作
Angular v17+引入的資源API(toObservable()和withCancel)提供了更強大的異步操作管理能力,特別是自動取消未完成的請求:
import { Component, inject, ResourceLoader } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { switchMap, withCancel } from 'rxjs/operators';
@Component({
selector: 'app-product-search',
standalone: true,
template: `
<input type="text" [(ngModel)]="searchQuery" placeholder="搜索產品">
@if (products(); as products) {
<ul>
@for (product of products; track product.id) {
<li>{{ product.name }}</li>
}
</ul>
} @else if (products.loading) {
<p>搜索中...</p>
}
`
})
export class ProductSearchComponent {
private http = inject(HttpClient);
private loader = inject(ResourceLoader);
// 搜索關鍵詞信號
searchQuery = signal('');
// 將信號轉換為Observable,用于觸發搜索
searchQuery$ = toObservable(this.searchQuery);
// 產品搜索信號(自動取消未完成請求)
products = toSignal(
this.searchQuery$.pipe(
// 當搜索關鍵詞變化時,自動取消上一次請求
switchMap(query => this.http.get(`/api/products?query=${query}`).pipe(
withCancel(this.loader.cancel) // 關聯資源加載器的取消信號
))
),
{ initialValue: [] }
);
}
資源API的核心優勢:
- 自動取消:當組件銷毀或新請求發出時,自動取消未完成的請求
- 與Signals無縫集成:結合
toObservable()和toSignal()實現完整的響應式流程 - 減少樣板代碼:無需手動管理
Subscription或takeUntil
2.3 異步信號的組合與依賴
多個異步信號可以組合使用,形成復雜的依賴關系,且自動處理加載狀態:
import { computed, signal, toSignal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
// 獲取用戶詳情
const userId = signal(1);
const user$ = userId.pipe(
switchMap(id => http.get(`/api/users/${id}`))
);
const user = toSignal(user$, { initialValue: null });
// 獲取用戶訂單(依賴用戶ID)
const orders$ = userId.pipe(
switchMap(id => http.get(`/api/users/${id}/orders`))
);
const orders = toSignal(orders$, { initialValue: [] });
// 計算信號:用戶是否有未完成訂單
const hasPendingOrders = computed(() => {
// 依賴orders信號,自動處理異步狀態
return orders().some(order => order.status === 'pending');
});
// 切換用戶,自動更新所有依賴信號
userId.set(2); // 觸發user和orders重新請求
3 狀態管理實戰:基于Signals的購物車
下面通過一個完整的購物車案例,展示如何使用Signals管理應用狀態,包括狀態設計、更新邏輯和響應式UI集成。
3.1 購物車狀態設計與服務實現
首先設計購物車狀態結構,并創建服務封裝狀態操作:
// cart.service.ts
import { Injectable, signal, computed } from '@angular/core';
// 商品類型定義
export interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
imageUrl: string;
}
@Injectable({ providedIn: 'root' })
export class CartService {
// 私有信號:存儲購物車數據
private cartItems = signal<CartItem[]>([]);
// 公共計算信號:暴露購物車狀態(只讀)
cartItems$ = computed(() => this.cartItems());
// 計算信號:購物車商品總數
itemCount = computed(() =>
this.cartItems().reduce((total, item) => total + item.quantity, 0)
);
// 計算信號:購物車總金額
totalAmount = computed(() =>
this.cartItems().reduce((total, item) => total + (item.price * item.quantity), 0)
);
// 計算信號:是否為空
isEmpty = computed(() => this.cartItems().length === 0);
// 添加商品到購物車
addItem(item: Omit<CartItem, 'quantity'>, quantity: number = 1) {
this.cartItems.update(items => {
// 檢查商品是否已在購物車中
const existingItem = items.find(i => i.id === item.id);
if (existingItem) {
// 已存在:更新數量
return items.map(i =>
i.id === item.id
? { ...i, quantity: i.quantity + quantity }
: i
);
} else {
// 不存在:添加新商品
return [...items, { ...item, quantity }];
}
});
}
// 更新商品數量
updateQuantity(itemId: number, quantity: number) {
if (quantity < 1) {
this.removeItem(itemId);
return;
}
this.cartItems.update(items =>
items.map(item =>
item.id === itemId ? { ...item, quantity } : item
)
);
}
// 移除商品
removeItem(itemId: number) {
this.cartItems.update(items =>
items.filter(item => item.id !== itemId)
);
}
// 清空購物車
clearCart() {
this.cartItems.set([]);
}
}
3.2 組件集成與響應式UI
1. 商品列表組件(添加商品到購物車)
// product-list.component.ts
import { Component, inject } from '@angular/core';
import { CartService } from './cart.service';
import { CommonModule } from '@angular/common';
// 商品數據
const products = [
{ id: 1, name: '筆記本電腦', price: 5999, imageUrl: '/images/laptop.jpg' },
{ id: 2, name: '無線鼠標', price: 129, imageUrl: '/images/mouse.jpg' },
{ id: 3, name: '機械鍵盤', price: 399, imageUrl: '/images/keyboard.jpg' }
];
@Component({
selector: 'app-product-list',
standalone: true,
imports: [CommonModule],
template: `
<h2>商品列表</h2>
<div class="product-grid">
@for (product of products; track product.id) {
<div class="product-card">
<img [src]="product.imageUrl" [alt]="product.name">
<h3>{{ product.name }}</h3>
<p>¥{{ product.price }}</p>
<button (click)="addToCart(product)">加入購物車</button>
</div>
}
</div>
`,
styles: [`
.product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 1rem; }
.product-card { border: 1px solid #eee; padding: 1rem; text-align: center; }
.product-card img { max-width: 100%; height: 150px; object-fit: contain; }
`]
})
export class ProductListComponent {
private cartService = inject(CartService);
products = products;
addToCart(product: any) {
this.cartService.addItem(product);
}
}
2. 購物車組件(展示與管理購物車)
// cart.component.ts
import { Component, inject } from '@angular/core';
import { CartService } from './cart.service';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-cart',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<h2>購物車</h2>
@if (cartService.isEmpty()) {
<p>購物車是空的,快去添加商品吧!</p>
} @else {
<table>
<thead>
<tr>
<th>商品</th>
<th>單價</th>
<th>數量</th>
<th>小計</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@for (item of cartService.cartItems$(); track item.id) {
<tr>
<td>
<img [src]="item.imageUrl" [alt]="item.name" width="50">
{{ item.name }}
</td>
<td>¥{{ item.price }}</td>
<td>
<input
type="number"
[(ngModel)]="item.quantity"
min="1"
(change)="cartService.updateQuantity(item.id, item.quantity)"
>
</td>
<td>¥{{ item.price * item.quantity }}</td>
<td>
<button (click)="cartService.removeItem(item.id)">刪除</button>
</td>
</tr>
}
</tbody>
</table>
<div class="cart-summary">
<p>商品總數:{{ cartService.itemCount() }}</p>
<p>總計:¥{{ cartService.totalAmount() }}</p>
<button (click)="cartService.clearCart()">清空購物車</button>
<button class="checkout">結算</button>
</div>
}
`,
styles: [`
table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
th, td { border: 1px solid #eee; padding: 0.5rem; text-align: left; }
.cart-summary { text-align: right; margin: 1rem 0; }
.checkout { background: #007bff; color: white; border: none; padding: 0.5rem 1rem; margin-left: 1rem; }
`]
})
export class CartComponent {
cartService = inject(CartService);
}
3. 導航欄組件(展示購物車數量)
// navbar.component.ts
import { Component, inject } from '@angular/core';
import { CartService } from './cart.service';
@Component({
selector: 'app-navbar',
standalone: true,
template: `
<nav>
<a href="/">首頁</a>
<a href="/products">商品</a>
<a href="/cart" class="cart-icon">
購物車
@if (cartService.itemCount() > 0) {
<span class="badge">{{ cartService.itemCount() }}</span>
}
</a>
</nav>
`,
styles: [`
nav { display: flex; gap: 1rem; padding: 1rem; background: #f5f5f5; }
.cart-icon { position: relative; }
.badge {
position: absolute;
top: -8px;
right: -8px;
background: red;
color: white;
border-radius: 50%;
width: 16px;
height: 16px;
font-size: 12px;
text-align: center;
}
`]
})
export class NavbarComponent {
cartService = inject(CartService);
}
3.3 狀態管理模式分析
本案例采用的基于Signals的狀態管理模式具有以下特點:
- 單一數據源:購物車狀態集中在
CartService中,避免狀態分散 - 讀寫分離:內部使用可寫信號(
cartItems),外部暴露只讀計算信號(cartItems$、itemCount等) - 響應式更新:所有依賴狀態的UI自動響應變化,無需手動觸發更新
- 封裝業務邏輯:所有狀態更新邏輯(添加、刪除、更新數量)封裝在服務中,確保狀態一致性
- 零訂閱管理:組件無需手動訂閱/取消訂閱,避免內存泄漏
4 Signals最佳實踐與性能優化
4.1 信號設計原則
-
最小權限原則:
- 內部狀態使用可寫信號(
signal()) - 對外暴露只讀計算信號(
computed()) - 避免直接暴露可寫信號給外部組件
- 內部狀態使用可寫信號(
-
狀態歸一化:
- 復雜狀態采用扁平化結構,避免深層嵌套
- 關聯數據通過ID引用,而非嵌套對象
- 示例:
// 推薦:歸一化狀態 const users = signal({ 1: { id: 1, name: '張三' }, 2: { id: 2, name: '李四' } }); const userPosts = signal({ 1: [101, 102], // 用戶1的帖子ID列表 2: [103] // 用戶2的帖子ID列表 });
-
不可變性優先:
- 優先使用
set()和update()保持不可變性 - 僅在性能關鍵路徑使用
mutate() - 復雜對象更新可使用immer庫簡化:
import { produce } from 'immer'; user.update(produce(draft => { draft.age = 31; draft.address.city = '北京'; }));
- 優先使用
4.2 性能優化策略
-
減少計算信號的復雜度:
- 拆分復雜計算信號為多個簡單計算信號
- 避免在計算信號中執行重型操作
- 示例:
// 不推薦:復雜計算信號 const complexComputation = computed(() => { const filtered = data().filter(...); const sorted = filtered.sort(...); return sorted.map(...); }); // 推薦:拆分計算信號 const filteredData = computed(() => data().filter(...)); const sortedData = computed(() => filteredData().sort(...)); const mappedData = computed(() => sortedData().map(...));
-
控制副作用執行頻率:
- 使用防抖/節流限制高頻副作用
- 示例:
import { effect } from '@angular/core'; import { debounceTime } from 'rxjs/operators'; import { toObservable } from '@angular/core/rxjs-interop'; const searchQuery = signal(''); effect(() => { const query = searchQuery(); // 防抖處理 const debounced = toObservable(searchQuery).pipe(debounceTime(300)); debounced.subscribe(value => { console.log('搜索:', value); }); });
-
避免不必要的依賴追蹤:
- 減少計算信號和副作用中的信號讀取
- 緩存不變的計算結果
- 示例:
// 不推薦:每次執行都讀取信號 const filtered = computed(() => { return data().filter(item => item.value > threshold()); }); // 推薦:僅在threshold變化時重新計算 const thresholdValue = threshold(); const filtered = computed(() => { return data().filter(item => item.value > thresholdValue); });
4.3 與RxJS的協同使用
Signals與RxJS并非互斥,而是互補關系,應根據場景選擇合適的工具:
| 場景 | 推薦技術 | 理由 |
|---|---|---|
| 本地狀態管理 | Signals | 簡單直觀,自動追蹤依賴 |
| 復雜異步流(如合并多個請求) | RxJS | 豐富的操作符(switchMap、merge等) |
| UI響應式更新 | Signals | 與模板集成更自然 |
| 時間相關操作(節流、防抖) | RxJS | 強大的時間操作符 |
協同使用示例:
import { signal, computed, effect } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
// 信號:搜索關鍵詞
const searchQuery = signal('');
// 轉換為Observable并添加防抖
const searchQuery$ = toObservable(searchQuery).pipe(
debounceTime(300),
distinctUntilChanged()
);
// 轉換回信號:搜索結果
const searchResults = toSignal(
searchQuery$.pipe(
switchMap(query => fetchResults(query))
),
{ initialValue: [] }
);
5 總結
Signals為Angular帶來了全新的響應式編程體驗,通過自動依賴追蹤和簡潔的API,顯著降低了狀態管理的復雜度。本章詳細介紹了Signals全家桶API,包括基礎信號的創建與更新、計算信號的衍生邏輯、副作用的處理,以及關聯信號的同步機制。
在異步數據處理方面,toSignal()實現了Observable到Signal的無縫轉換,結合資源API自動管理訂閱生命周期,解決了傳統RxJS的樣板代碼問題。購物車實戰案例展示了如何基于Signals構建完整的狀態管理方案,體現了單一數據源、讀寫分離、響應式更新等現代狀態管理原則。
最佳實踐部分總結了信號設計原則和性能優化策略,強調了不可變性、狀態歸一化和與RxJS的協同使用。隨著Angular對Signals的持續增強,它將成為Angular應用中狀態管理的首選方案,尤其是在獨立組件架構中,能夠充分發揮其簡潔高效的優勢。
掌握Signals不僅能夠提升開發效率,還能構建更可預測、更易于調試的響應式應用,是現代Angular開發者必備的核心技能。

浙公網安備 33010602011771號