我觀看了 YouTube 視頻,其中 Angular 團隊負責人 Alex Rickabaugh 不鼓勵使用 effect。 然後,他示範了一種用 computed signal 取代 effect 的方法,這種方法並不直觀,並且需要開發人員進行思維轉變,在 computed signal 中包含 WritableSignal。
今天,我想用 signals 和 computed signals 來取代 explicitEffect。
searchId = signal(initialId);
id = signal(initialId);
person = signal<undefined | Person>(undefined);
films = signal<string[]>([]);
rgb = signal('brown');
fontSize = computed(() => this.id() % 2 === 0 ? '1.25rem' : '1.75rem');
該元件有一些 signals 來儲存 searchId、id、person、films 和 rgb 程式碼。 fontSize computed signal 根據 id 得出字體大小。
#logIDsEffect = explicitEffect([this.searchId],
([searchId]) => console.log('id ->', this.id(), 'searchID ->', searchId), { defer: true });
#rgbEffect = explicitEffect([this.rgb], ([rgb]) => console.log('rgb ->', rgb), { defer: true });
constructor() {
explicitEffect([this.id], ([id], onCleanUp) => {
const sub = getPersonMovies(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([]);
}
this.rgb.set(generateRGBCode());
this.rgb.set(generateRGBCode());
this.rgb.set(generateRGBCode());
});
this.renderer.setProperty(this.hostElement, 'style', `--main-font-size: ${this.fontSize()}`);
if (id !== this.searchId()) {
this.searchId.set(id);
}
onCleanUp(() => sub.unsubscribe());
});
}
此元件具有三種 effect,可在控制台中記錄 signals 或更新 signals 。這些 signals 需要用 computed state 取代。
程式碼審查後,流程是保留 id signal 並消除其餘 signals。 第一步是新增 computed state 並刪除 fontSize computed signal。
state = computed(() => {
return {
fontSize: this.id() % 2 === 0 ? '1.25rem' : '1.75rem',
};
});
當 id signal 更新時,state computed signal 會為 fontSize 屬性匯出新的字體大小。
host: {
'[style.--main-font-size]': 'state().fontSize',
},
使用 host 屬性而不是 Renderer2 和 ElementRef 來更新 CSS 變數。
state = computed(() => {
return {
fontSize: this.id() % 2 === 0 ? '1.25rem' : '1.75rem',
rgb: generateRGBCode(),
};
});
當 id signal 改變時, state computed signal 會為 rgb 屬性匯出新的 RGB 值。同樣,host 屬性也會更新 CSS 變數,並刪除 #rgbEffect effect,以便它不會記錄 rgb 變更。
host: {
'[style.--main-color]': 'state().rgb',
},
searchId signal 比其他 signals 需要更多的工作。 當 id signal 更新時,它也具有相同的值。 當 seachId signal 發生變化時,id signal 也接收到最新的值。
state = computed(() => {
const result = this.#personMovies();
return {
fontSize: this.id() % 2 === 0 ? '1.25rem' : '1.75rem',
rgb: generateRGBCode(),
searchId: signal(this.id()),
};
});
在 state computed signal 中,searchId 屬性是一個初始值為 this.id() 的 signal。 當 id signal 隨後發生變化時,state computed signal 會同步 searchId 屬性的值。
syncId(id: number) {
if (id >= this.min && id <= this.max) {
this.state().searchId.set(id);
this.id.set(id);
}
}
當使用者在文字欄位中輸入新的 id 時,syncId 方法會設定 searchId 屬性和 id signal。
<input type="number" [ngModel]="state().searchId()" (ngModelChange)="syncId($event)" />
輸入欄位不能使用雙向資料綁定將 searchId signal 綁定到 ngModel directive。 ngModelChange event emitter 呼叫 syncId 方法來更新 signal。
在 constructor 中,刪除RxJS程式碼,因為它沒有被使用。
toObservable(this.searchId).pipe(
debounceTime(300),
distinctUntilChanged(),
filter((value) => value >= this.min && value <= this.max),
map((value) => Math.floor(value)),
takeUntilDestroyed(),
).subscribe((value) => this.id.set(value));
與 syncId 方法相比,我更喜歡上面的RxJS程式碼;我寧願使用 effect 來同步 id 和 searchId signals 的值。
function getPersonMovies(http: HttpClient) {
return function(source: Observable<Person>) {
return source.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);
}));
}
}
這是一個自訂 RxJS 運算子,用於檢索星際大戰角色的詳細資訊和影片。
#personMovies = toSignal(toObservable(this.id)
.pipe(
switchMap((id) => this.http.get<Person>(`${URL}/${id}`)
.pipe(getPersonMovies(this.http))
),
), { initialValue: undefined });
#personMovies 使用 toSignal 和 toObservable 函數建立星際大戰詳細資訊的 signal。 我覺得toSignal(toObservable(this.id)) 很長,對於初學者來說不容易理解。
state = computed(() => {
const result = this.#personMovies();
return {
fontSize: this.id() % 2 === 0 ? '1.25rem' : '1.75rem',
rgb: generateRGBCode(),
person: signal(result && result.length > 0 ? result[0] : undefined),
films: signal(result && result.length > 1 ? result.slice(1): []),
searchId: signal(this.id()),
};
});
如果 HTTP 請求成功,則定義 result 陣列。 person 屬性是一個 signal,其值是 result 的第一個元素。 films 屬性是一個 signal,其值是 result 剩餘的元素。
<div class="border">
@if(state().person(); as person) {
<p>Id: {{ id() }} </p>
<p>Name: {{ person.name }}</p>
<p>Height: {{ person.height }}</p>
<p>Mass: {{ person.mass }}</p>
<p>Hair Color: {{ person.hair_color }}</p>
<p>Skin Color: {{ person.skin_color }}</p>
<p>Eye Color: {{ person.eye_color }}</p>
<p>Gender: {{ person.gender }}</p>
} @else {
<p>No info</p>
}
<p style="text-decoration: underline">Movies</p>
@for(film of state().films(); track film) {
<ul style="padding-left: 1rem;">
<li>{{ film }}</li>
</ul>
} @empty {
<p>No movie</p>
}
</div>
HTML 範本根據 state computed signal 顯示人物和影片。
結論:
effect。signas-in-computed,並在它們依賴的 signal 發生變化時更新屬性。toSignal 和 toObservable 發出 HTTP 請求。 toSignal(toObservable(this.id)) 又長又難讀,我們可以查看 ngxtension 庫中的 toObservableSignal 函數。鐵人賽的第 31 天到此結束。
參考:
Techstack Nation Don't use effect: https://www.youtube.com/watch?v=aKxcIQMWSNU&feature=youtu.be
Stackblitz Demo: https://stackblitz.com/edit/stackblitz-starters-cejcoj?file=src%2Fstar-war%2Fstar-war.service.ts