在 Angular 18 中,toSignal
函數新增了與訊號和計算函數相同的可選 equal
選項。 當省略 equal 選項時,將套用預設值 (===),並透過引用 (reference) 比較訊號值。如果複雜物件希望執行深度相等檢查,
oSignal` 將使用自訂相等函數覆寫 equal 選項。
type ValueEqualityFn<T> = (a: T, b: T) => boolean
ValueEqualityFn
類型是一個比較函數,它接受兩個物件並傳回一個布林值。 如果兩個物件相同,則函數將傳回 true,否則傳回 false。
export type Person = {
name: string;
height: string;
mass: string;
hair_color: string;
skin_color: string;
eye_color: string;
gender: string;
birth_year: string;
}
export type PersonSearchResults = {
count: number;
results: Person[];
}
export type FormValues = { name: string; lastName: string }
import { FormValues, PersonSearchResults } from '../person.type';
const URL = `https://swapi.py4e.com/api/people`;
@Injectable({
providedIn: 'root'
})
export class StarWarService {
private readonly formValuesSub = new Subject<FormValues>();
http = inject(HttpClient);
formValues$ = this.formValuesSub.asObservable();
update(value: FormValues) {
this.formValuesSub.next(value);
}
searchCharacter({ name, lastName }: FormValues) {
const str = `${name.trim()} ${lastName.trim()}`.trim();
const person$ = this.http.get<PersonSearchResults>(`${URL}/?search=${str}`)
.pipe(
map(({ count, results }) => (count <= 0) ? undefined : results[0]),
catchError((e) => of(undefined))
);
return str ? person$ : of(undefined);
}
}
StarWarService
建立 searchCharacter
方法,使用這個方法以呼叫 StarWars API,按姓名查詢人員。結果是 Person
的 Observable 或未定義 (undefined) 的。 formValues$
Observable 在 StarWarFormComponent
和 AppComponent
之間共用表單值。
@Component({
selector: 'app-person-form',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="formGroup">
<div>
<label for="name">
<span>Name: </span>
<input id="name" name="name" formControlName="name" />
</label>
</div>
<div>
<label for="lastname">
<span>Last name:</span>
<input id="lastname" name="lastname" formControlName="lastName" />
</label>
</div>
<button type="submit">Submit</button>
</form>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StarWarFormComponent {
service = inject(StarWarService);
formGroup = new FormGroup({
name: new FormControl('', { nonNullable: true }),
lastName: new FormControl('', { nonNullable: true }),
});
constructor() {
this.formGroup.events.pipe(
filter((e) => e instanceof FormSubmittedEvent),
filter(({ source }) => source.valid),
map(({ source }) => {
const { name, lastName } = source.value as FormValues;
return { name: name.trim(), lastName: lastName.trim() } as FormValues;
}),
takeUntilDestroyed(),
).subscribe((value) => this.service.update(value));
}
}
此元件封裝了一個反應式表單,供使用者輸入星際大戰角色的名字和姓氏。提交表單時,表單值將被儲存到服務中的 formValuesSub
Subject中。
import { FormValues } from "./starwars/person.type";
export function equal(a: FormValues, b: FormValues) {
const { name, lastName } = a;
const { name: bName, lastName: bLastName } = b;
return name.toLocaleLowerCase() === bName.toLocaleLowerCase()
&& lastName.toLocaleLowerCase() === bLastName.toLocaleLowerCase();
}
equal
函數以不區分大小寫的方式比較兩個 FormValues
物件。當兩個物件具有相同的名稱和姓氏時,它們被視為相等。
@Component({
selector: 'app-root',
imports: [StarWarComponent, StarWarFormComponent],
template: `
<app-person-form />
<div>
<app-person [person]="insensitiveCharacter()" />
<p>Timestamp: {{ timestamp() }}</p>
</div>
<div>
<app-person [person]="character()" />
<p>Timestamp: {{ timestamp2() }}</p>
</div>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
service = inject(StarWarService);
lastJedi = toSignal(this.service.formValues$, {
initialValue: { name: '', lastName: '' } as FormValues,
equal
});
timestamp = signal('');
timestamp2 = signal('');
insensitiveCharacter$ = toObservable(this.lastJedi).pipe(
switchMap((name) => this.service.searchCharacter(name)),
tap(() => this.timestamp.set(new Date().toLocaleTimeString('en-US')))
);
insensitiveCharacter = toSignal(this.insensitiveCharacter$);
lastJedi2 = toSignal(this.service.formValues$, {
initialValue: { name: '', lastName: '' } as FormValues,
});
character$ = toObservable(this.lastJedi2).pipe(
switchMap((name) => this.service.searchCharacter(name)),
tap(() => this.timestamp2.set(new Date().toLocaleTimeString('en-US')))
);
character = toSignal(this.character$);
}
App
元件注入StarWarService
服務來取得表單值的Observable。
lastJedi
訊號使用 toSignal
函數從 this.service.formValues$ Observable
建立訊號。 equal
屬性比較表單值而不是表單引用 (reference)。例如,{name:'Luke',lastName:'Skywalker'}
和 {name:'luke',lastName:'skyWalker'}
是相等的,而 {name:'Anakin',lastName:'Skywalker'}
和 {name : 'Darth'、lastName: 'Vader'}
是不同的。
customEqualCharacter$ = toObservable(this.lastJedi).pipe(
switchMap((name) => this.service.searchCharacter(name)),
tap(() => this.timestamp.set(new Date().toLocaleTimeString('en-US')))
);
當更新lastJedi
訊號時,就會進行搜索,更新 timestamp
訊號並產生一個Person
的Observable。
customEqualCharacter = toSignal(this.customEqualCharacter$);
toSignal
函數從 Observable 建立一個訊號並分配給 customEqualCharacter
訊號。
<app-person [person]="customEqualCharacter()" />
AppComponent
將 customEqualCharacter
訊號傳遞給 StarWarComponent
以顯示詳細資訊。
相較之下,lastJedi2
訊號使用預設的引用檢查(===)進行比較。
defaultCharacter$ = toObservable(this.lastJedi2).pipe(
switchMap((name) => this.service.searchCharacter(name)),
tap(() => this.timestamp2.set(new Date().toLocaleTimeString('en-US')))
);
當更新lastJedi2
訊號時,就會進行搜索,更新 timestamp2
訊號並產生一個Person
的Observable。
defaultCharacter = toSignal(this.defaultCharacter$);
同樣,defaultCharacter
訊號儲存星際大戰角色的值。
當使用者提交表單時,表單值就有了新的引用。因此,lastJedi2
訊號總是會改變,而timestamp2
訊號在HTTP請求完成後接收到新的時間。 lastJedi
訊號比較值,當目前的表單值與前一個值不同時,會發生 HTTP 請求。 timestamp
訊號的更新不如 timestamp2
訊號那麼頻繁。 我們觀察 timestamp
和 timestamp2
訊號,發現自訂 equal
函數可防止對訊號及其相關訊號和模板進行不必要的更新。
結論:
參考:
鐵人賽的第 42 天到此結束。