iT邦幫忙

2024 iThome 鐵人賽

0
JavaScript

Signal API in Angular系列 第 42

Day 42 - toSignal 函數中的相等性檢查

  • 分享至 

  • xImage
  •  

在 Angular 18 中,toSignal 函數新增了與訊號和計算函數相同的可選 equal 選項。 當省略 equal 選項時,將套用預設值 (===),並透過引用 (reference) 比較訊號值。如果複雜物件希望執行深度相等檢查,oSignal` 將使用自訂相等函數覆寫 equal 選項。

type ValueEqualityFn<T> = (a: T, b: T) => boolean

ValueEqualityFn 類型是一個比較函數,它接受兩個物件並傳回一個布林值。 如果兩個物件相同,則函數將傳回 true,否則傳回 false。

創建 StarWarService 服務

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 在 StarWarFormComponentAppComponent 之間共用表單值。

表單元件

@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()" />

AppComponentcustomEqualCharacter 訊號傳遞給 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 訊號儲存星際大戰角色的值。

timestamp訊號值與timestamp2訊號值

當使用者提交表單時,表單值就有了新的引用。因此,lastJedi2訊號總是會改變,而timestamp2訊號在HTTP請求完成後接收到新的時間。 lastJedi 訊號比較值,當目前的表單值與前一個值不同時,會發生 HTTP 請求。 timestamp 訊號的更新不如 timestamp2 訊號那麼頻繁。 我們觀察 timestamptimestamp2 訊號,發現自訂 equal 函數可防止對訊號及其相關訊號和模板進行不必要的更新。

結論:

  • ToSignalOptions 有 equal 屬性來執行兩個訊號的相等性檢查。
  • 當toSignal選項沒有提供equal時,使用預設值(===)。
  • 當 equal 為 true 時,只讀訊號不更新。依賴它的訊號和模板也不會更新。
  • 此進階功能是一種透過減少變更檢測週期數來優化效能的技術。

參考:

鐵人賽的第 42 天到此結束。


上一篇
Day 41 - 使用 AfterRenderEffect 生命週期鉤子進行反應式 DOM 讀寫
下一篇
Day 44 - toSignal 函數的初始值
系列文
Signal API in Angular43
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言