在第3天和第4天,我簡要介紹了effect
,例子是記錄各種signal
的值。根據我的經驗,effect
不僅僅是記錄signal
的值。一些例子是,
signals
host element
的CSS
變數今天,我將根據我的個人經驗寫一些Effect的例子。
在下面的範例中,我根據HTML輸入欄位中的id
檢索Star War
角色。 該id
用於呼叫Star War API來檢索角色,然後呼叫forkJoin
來檢索該角色出現的影片。
// star-war.api.ts
export type Person = {
name: string;
height: string;
mass: string;
hair_color: string;
skin_color: string;
eye_color: string;
gender: string;
films: string[];
}
// Create a Person type to hold an instance of a Star War character.
import { catchError, map, of, mergeMap, forkJoin } from 'rxjs';
import { inject, runInInjectionContext, Injector } from '@angular/core';
import { HttpClient } from '@angular/common/http';
const URL = 'https://swapi.dev/api/people';
export function getPersonMovies(id: number, injector: Injector) {
return runInInjectionContext(injector, () => {
const http = inject(HttpClient);
return http.get<Person>(`${URL}/${id}`).pipe(
mergeMap((person) => {
const urls = person?.films ?? [];
const filmTitles$ = urls.map((url) => http.get<{ title: string }>(url).pipe(
map(({ title }) => title),
catchError((err) => {
console.error(err);
return of('');
})
));
return forkJoin([Promise.resolve(person), ...filmTitles$]);
}),
catchError((err) => {
console.error(err);
return of(undefined);
}));
});
}
定義getPersonMovies
函數來擷取星際大戰角色以及該角色出現的影片。
稍後,我將在組件中導入該函數來檢索資料並將其顯示在HTML範本中。
使用者可以點擊按鈕或在文字方塊中輸入值來修改id
signal。
// character.component.ts
const initialId = 14;
@Component({
selector: 'app-character',
standalone: true,
imports: [FormsModule],
template: `
<h3>Display the 83 Star War Characters</h3>
<div class="border" #character>
@if(person(); as person) {
<p>Id: {{ id() }} </p>
<p>Name: {{ person.name }}</p>
<p>Height: {{ person.height }}</p>
... omitted other data for brevity...
} @else {
<p>No info</p>
}
<p style="text-decoration: underline">Movies</p>
@for(film of films(); track film) {
<ul style="padding-left: 1rem;">
<li>{{ film }}</li>
</ul>
} @empty {
<p>No movie</p>
}
</div>
<div class="container">
<button (click)="updateId(-2)">-2</button>
<button (click)="updateId(-1)">-1</button>
<button (click)="updateId(1)">+1</button>
<button (click)="updateId(2)">+2</button>
<input type="number" [(ngModel)]="searchId" name="searchId" id="searchId" />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CharacterComponent {
readonly min = 1;
readonly max = 83;
injector = inject(Injector);
searchId = signal(initialId);
id = signal(initialId);
person = signal<undefined | Person>(undefined);
films = signal<string[]>([]);
updateId(delta: number) {
this.id.update((value) => Math.min(this.max, Math.max(this.min, value + delta)));
}
constructor() {
toObservable(this.searchId).subscribe((value) => this.id.set(value));
effect((onCleanUp) => {
const sub = getPersonMovies(this.id(), this.injector)
.subscribe((result) => {
if (result) {
const [person, ...rest] = result;
this.person.set(person);
this.films.set(rest);
} else {
this.person.set(undefined);
this.films.set([]);
}
});
onCleanUp(() => sub.unsubscribe());
});
}
}
當程式碼更新id
signal時,effect
執行getPersonMovies
函數來擷取資料並訂閱Observable
以覆寫person
和movie
signals。
effect((onCleanUp) => {
const sub = getPersonMovies(this.id(), this.injector)
.subscribe(...subscribe logic…);
// unsubscribe the subscription before destroying the effect
onCleanUp(() => sub.unsubscribe());
});
subscribe
方法傳回一個subscription
,必須清除該subscription
以防止memory leaks
。幸運的是,effect
函數接受一個onCleanUp
函數,可以在銷毀effect
之前取消`subscription。
當我點擊按鈕來增加id
signal時,它與searchId
signal不同步。因此,我想在effect
裡面同步它們的值。
設定searchId
signal的最佳位置是在subscribe
方法裡面,但我想示範untracked
函數的用法。如果我在effect
內呼叫this.searchId.set(this.id())
,將會拋出錯誤訊息。當我在untracked
函數中執行邏輯時,它不會被視為effect
的依賴項。不會發生錯誤且文字方塊顯示相同的值。
untracked(() => {
if (this.id() !== this.searchId()) {
this.searchId.set(this.id());
}
});
#logIDsEffect = effect(() => console.log('id ->', untracked(this.id), 'searchID ->', this.searchId()));
關於#logIDsEffect
,我只想在searchId
signal更新時查看日誌訊息。untracked(this.id)
傳回值,它不是effect
的依賴項 (dependency
)。
當我點擊按鈕時,控制台 (Dev Console) 顯示一條訊息。 當我在文字方塊中輸入值時,控制台 (Dev Console) 也會顯示日誌訊息。
// generate-rgb.ts
export function generateRGBCode(): string {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
return `rgb(${r}, ${g}, ${b})`;
}
generateRGBCode
是一個產生隨機RGB
值以更新CSS
顏色屬性的函數。
import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, inject, Injector, Renderer2, signal, untracked } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
import { getPersonMovies, Person } from './star-war.api';
import { generateRGBCode } from './generate-rgb';
const initialId = 14;
@Component({
selector: 'app-character',
standalone: true,
imports: [FormsModule],
template: `
<h3>Display the 83 Star War Characters</h3>
<div class="border">
… dynamic data…
</div>
`,
styles: `
:host {
--main-font-size: 1.25rem;
}
.border {
color: var(--main-color);
font-size: var(--main-font-size);
}
`,
host: {
'[style.--main-color]': 'rgb()'
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CharacterComponent {
rgb = signal('brown');
hostElement = inject(ElementRef).nativeElement;
renderer = inject(Renderer2);
fontSize = computed(() => this.id() % 2 === 0 ? '1.25rem' : '1.75rem');
constructor() {
effect((onCleanUp) => {
const sub = getPersonMovies(this.id(), this.injector)
.subscribe((result) => {
this.rgb.set(generateRGBCode());
this.rgb.set(generateRGBCode());
this.rgb.set(generateRGBCode());
});
onCleanUp(() => sub.unsubscribe());
});
effect(() => {
this.renderer.setProperty(this.hostElement, 'style', `--main-font-size: ${this.fontSize()}`);
});
}
}
這個組件在host element
中定義了兩個CSS變數:--main-font-size
和--main-color
。 第一個變數指定border class
的text color
,而第二個變數指定border class
的font size
。
host: {
'[style.--main-color]': 'rgb()'
},
當rgb
signal改變值時,--main-color
變數接收一個新值。
effect((onCleanUp) => {
const sub = getPersonMovies(this.id(), this.injector)
.subscribe((result) => {
this.rgb.set(generateRGBCode());
this.rgb.set(generateRGBCode());
this.rgb.set(generateRGBCode());
});
onCleanUp(() => sub.unsubscribe());
});
經過反覆試驗,更新rgb
signal的最佳位置是在subscribe
方法。 我設定了三次rgb
signal,因為我想了解Angular組件執行effect
的頻率。
--main-font-size: 1.25rem;
hostElement = inject(ElementRef).nativeElement;
renderer = inject(Renderer2);
fontSize = computed(() => this.id() % 2 === 0 ? '1.25rem' : '1.75rem');
為了更新--main-font-size
,我inject了ElementRef
和Renderer2
在effect
裡面設定font size
的值。當id
signal為偶數時,字體大小為1.25rem
,否則,字體大小為1.75rem
。
effect(() => {
this.renderer.setProperty(this.hostElement, 'style', `--main-font-size: ${untracked(this.fontSize)}`);
});
fonSize
是computed signal,當id
signal更新時它也會更新。 fontSize
signal呼叫 untracked
函數以防止signal
讀取。
當組件被標記為髒並觸發change detection
時執行`effect。
#rgbEffect = effect(() => console.log('rgb ->', this.rgb()));
#rgbEffect
是一種在改變值時記錄rgb
signal的effect
。
effect((onCleanUp) => {
const sub = getPersonMovies(this.id(), this.injector)
.subscribe((result) => {
this.rgb.set(generateRGBCode());
this.rgb.set(generateRGBCode());
this.rgb.set(generateRGBCode());
});
});
subscribe
方法將三個隨機RGB
值分配給rgb
signal,但控制台 (Dev Console)僅記錄一次。
這是因為#rgbEffect
僅在change detection
發現髒組件並更新視圖(view
)時運行。因此,#rgbEffect
記錄最新的RGB
值並跳過中間值。
Effect
追蹤build-in或computed signal,並運行函數中的邏輯。Effect
可以在constructor內部或field initialization中建立。 如果effect在constructor之外,則需要injector。effect
不依賴signal
時,請使用untracked
函數。 如果您想執行一些不應被視為依賴項的程式碼,請將其包裝在未追蹤的函數中。Effect
在組件被標記為髒並在change detection
發生時執行。我們可以連續更新一個signal,但effect
會在組件重新渲染時執行一次。鐵人第10天就這樣結束了。