在第3天,我們介紹了Signal API、Computed Signal和Effect。今天,我們將介紹新用戶可能會遇到的Signal技巧和陷阱。
function signal(
initialValue: T,
options?: CreateSignalOptions<T>
): WritableSignal<T>;
interface CreateSignalOptions {
equal?: ValueEqualityFn<T>;
}
type ValueEqualityFn = (a: T, b: T) => boolean
在初始化Signal時,可以將equal
選項傳遞給函數的第二個參數。equal
選項接受一個比較函數,用於判斷當前和新的Signal值是否相同。如果不使用equal
選項,則使用默認實現(===)。
type Person = {
firstName: string;
lastName: string;
}
const lowerCaseFullname = ({ firstName, lastName }: Person) => `${firstName}${lastName}`.toLowerCase();
person = signal<Person>({ firstName: 'Mary', lastName: 'Peterson' }, {
Equal: (a, b) => lowerCaseFullName(a) === lowerCaseFullName(b)
});
person2 = signal<Person>({ firstName: 'Mary', lastName: 'Peterson' });
pColor = signal<'yellow' | 'goldenrod'>('goldenrod');
pColor2 = signal<'yellow' | 'goldenrod'>('goldenrod');
update() {
this.person.set({ firstName: 'MaRy', lastName: 'PEterSON' });
this.person2.set({ firstName: 'MaRy', lastName: 'PETerSON' });
}
person
signal對其名字和姓氏執行不區分大小寫的比較。person2
signal使用默認實現來比較對象引用。update
方法用混合大小寫的名稱覆蓋兩個信號。
constructor() {
effect(() => {
console.log('person 2', this.person2().firstName, this.person2().lastName);
const color2 = untracked(this.pColor2);
this.p2Color.set(color2 === 'yellow' ? 'goldenrod' : 'yellow');
}, { allowSignalWrites: true });
effect(() => {
console.log('person 1', this.person().firstName, this.person().lastName);
const color = untracked(this.pColor);
this.pColor.set(color === 'yellow' ? 'goldenrod' : 'yellow');
}, { allowSignalWrites: true });
}
第一個effect跟踪person2
signal並切換p2Color
signal的背景顏色。第二個effect跟踪person
signal並切換pColor
信號的背景顏色。
@let p = person();
@let p2 = person2();
<p [style.background]="pColor()">Person: {{ p.firstName }} {{ p.lastName }}</p>
<p [style.background]="p2Color()">Person: {{ p2.firstName }} {{ p2.lastName }}</p>
<button (click)="update()">Click me to update signals<button>
當用戶點擊按鈕時,person
signal不會更新,因為相等檢查函數返回true。因此,背景顏色不會改變。另一方面,person2
signal的object reference在每次更新後都不同;因此,默認相等函數返回false,其背景顏色會改變。equal
選項可以提高OnPush
組件的性能,當signal持有object時。自定義相等函數比較屬性值,然後返回結果。當舊值和新值被認為相同時,組件不會被標記為髒,並且變化檢測(change detection)不會重新渲染它。
類似於內建signal,computed signal也有equal選項。Computed signal可以根據自定義相等函數的結果有選擇地從其他信號重新計算。
count = signal(0);
oddCount = computed(() => this.count(), {
equal: (_, b) => b % 2 === 0,
});
update() {
.....
this.count.update((prev) => prev + 1);
}
<p>{{ oddCount() }}</p>
oddCount
signal只有在count
信號是奇數時才會重新計算新值。當count
為 0,用戶點擊更新按鈕時,count
增加至 1, oddCount
的相等檢查函數為false,oddCount
改變為1。當用戶再次點擊時,count
變為2,相等檢查函數返回true。oddCount
不會重新計算,範本繼續顯示緩存值。當用戶第三次點擊時,oddCount
為3,範本顯示新值。
oddCount = computed(() => {
this.person.set({ firstName: 'John', lastName: 'Wayne' }); // it is not allowed
return this.count();
}, { equal: (_, b) => b % 2 === 0,
Computed signal不能寫入其他signal。它不應該包含更新其他signal或組件實例成員的邏輯。
count = signal(0);
showCount = signal(false);
showCountString = computed(() => {
if (showCount()) {
return `Count: ${this.count()}`;
}
return 'Nothing to show';
});
當showCount
為 false時,showCountString
返回「Nothing to show」,且不讀取count
。當count
增加時,不會導致showCountString
重新計算。
當showCount
切換為true時,showCountString
會執行if
分支並顯示count
的值。當count
增加時,showCountString
會派生一個包含新count
的新字符串。
當showCount
回復為false時,count
的依賴關係再次被移除,且當count
增加時,showCountString
不會重新計算。
預設情況下,effect
不允許寫入其他signal。為了繞過這個錯誤,可以將allowSignalWrites
選項傳遞給 effect
。然而,這不是最佳實踐(best practice),因為effect
的設計目的是執行非反應式代碼,例如記錄。
effect(() => {
console.log('person 1', this.person().firstName, this.person().lastName);
const color = untracked(this.pColor);
this.pColor.set(color === 'yellow' ? 'goldenrod' : 'yellow');
}, { allowSignalWrites: true });
}
在將{ allowSignalWrites: true }
傳遞給effect
後,pColor
信號可以覆寫color
。untracked(this.pColor)
不僅返回其值,而且還確保pColor
不是effect
的依賴項。當pColor
覆寫 color
時,effect
不會運行,避免了無限循環。
這結束了鐵人賽的第4天。
Stackblitz Demo: https://stackblitz.com/edit/stackblitz-starters-skvuez?file=src%2Fmain.ts