@Input setter vs effect:解決循環依賴問題
在 Angular 應用中,父子組件間的數據傳遞是常見需求,但如果處理不當,容易造成循環依賴問題。
本篇筆記記錄了使用 Signal API 和傳統 @Input 時解決循環依賴的最佳實踐。
問題情境
在我們的投票系統專案中,遇到了一個典型的循環依賴問題:
父組件從 API 獲取數據,傳給子組件
子組件處理數據,更新 UI 顯示(包括計算百分比)
用戶在子組件中選擇選項,子組件通知父組件
父組件更新數據,再傳回子組件
子組件處理更新的數據,但導致 UI 顯示錯誤(百分比被重置)
問題分析:不正確的實現(使用 effect)
// 子組件代碼
@Component({
selector: 'app-option',
//...
})
export class OptionComponent {
// 輸入
options = input.required<Option[]>();
voteRecords = input.required<VoteRecord[]>();
initialSelectedOptionId = input.required<string | null>();
// 本地狀態
selectedOptionId = signal<string | null>(null);
localVoteRecords = signal<VoteRecord[]>([]);
// 問題源頭:使用 effect 處理輸入變化
constructor() {
effect(() => {
const records = this.voteRecords();
const initialId = this.initialSelectedOptionId();
if (records) {
this.localVoteRecords.set(records);
}
if (initialId !== null) {
this.selectedOptionId.set(initialId);
}
}, { allowSignalWrites: true });
}
// 計算百分比
totalVotes = computed(() => {
return this.localVoteRecords().length;
});
private percentagesSignal = computed(() => {
// 百分比計算邏輯
// ...
});
}
這種實現方式的問題是:effect 會在每次 Signal 值變化時執行,即使實際值沒有發生變化。
循環依賴的具體流程
初始化階段:
父組件傳入初始
initialSelectedOptionId
值子組件的 effect 被觸發
子組件設置
selectedOptionId
並計算初始百分比
使用者交互階段:
使用者選擇選項 1(ID: 8cb146e1-ed48-483f-bb9f...)
子組件更新
selectedOptionId
子組件重新計算百分比(顯示正確值:14.285%)
子組件通知父組件選項已變更
父組件更新階段:
父組件接收通知,更新
userSelectedOptionId
父組件將相同的 ID 再次傳回子組件作為
initialSelectedOptionId
子組件再次更新階段:
子組件的 effect 再次被觸發(即使收到的是相同的 ID)
子組件重新設置
selectedOptionId
百分比計算再次執行,但結果錯誤(0% 而非 14.285%)
UI 顯示不正確的百分比
解決方案:使用 @Input setter
使用 @Input setter 可以精確控制何時更新本地狀態,有效阻止循環依賴:
@Component({
selector: 'app-option',
//...
})
export class OptionComponent {
// 私有 signal 存儲實際數據
private _localVoteRecords = signal<VoteRecord[]>([]);
private _selectedOptionId = signal<string | null>(null);
// 公開唯讀 signal 供模板使用
localVoteRecords = this._localVoteRecords.asReadonly();
selectedOptionId = this._selectedOptionId.asReadonly();
// 使用 @Input setter 處理輸入屬性
@Input()
set voteRecords(records: VoteRecord[]) {
if (records) {
this._localVoteRecords.set([...records]);
}
}
@Input()
set initialSelectedOptionId(id: string | null) {
// 關鍵:只在值真正不同時才更新,避免循環
if (id !== null && id !== this._selectedOptionId()) {
this._selectedOptionId.set(id);
}
}
// 計算邏輯保持不變
totalVotes = computed(() => {
return this._localVoteRecords().length;
});
// ...
}
@Input setter vs effect 優勢對比
條件性處理
✅ 可根據條件判斷是否更新
❌ 每次變化都會執行
循環依賴風險
✅ 低,可通過條件判斷防止
❌ 高,易造成無限循環
自動追蹤依賴
❌ 需手動指定對應關係
✅ 自動追蹤所有使用的 Signal
代碼可讀性
✅ 直觀明了
❌ 依賴關係不夠明確
與 Angular 標準契合
✅ 遵循 Angular 資料流模型
⚠️ 較新的 API,使用模式尚在發展
@Input setter 如何阻止循環依賴
@Input setter 通過以下機制阻止循環依賴:
門衛機制:setter 中的條件判斷只允許真正需要的更新通過
精確控制:只在值真正變化時執行更新邏輯
單向數據流:維持清晰的父到子的數據流向
例如,在上面的實現中,當父組件將用戶選擇的相同 ID 傳回子組件時:
@Input()
set initialSelectedOptionId(id: string | null) {
// 如果值相同,這個條件不會通過,因此不會執行更新
// 這有效地打破了循環
if (id !== null && id !== this._selectedOptionId()) {
this._selectedOptionId.set(id);
}
}
條件判斷確保只有當 ID 真正變化時,才會執行 _selectedOptionId.set(id)
,從而避免了不必要的重新計算。
結論與最佳實踐
優先使用 @Input setter:在處理可能導致循環依賴的輸入屬性時
添加條件判斷:在 setter 中加入條件,只在值真正變化時更新
區分私有狀態與公開 API:使用私有變數存儲實際狀態,公開唯讀 API
保持單向數據流:避免子組件直接修改從父組件接收的數據
遵循這些最佳實踐,可以構建出更加健壯、可維護的 Angular 應用,特別是在使用 Signal API 等新特性時。
Last updated