چند ایده در مورد سیگنال
۱. سیگنالهای مبتنی بر زمان (Time-Based Signals)
این دسته از قابلیتها در مدیریت تعاملات کاربر (UI Events) بسیار حیاتی هستند و مستقیماً از اپراتورهای RxJS الهام گرفته شدهاند.
الف) debounceSignal (مشابه debounceTime)
این یکی از پرکاربردترین و ضروریترین قابلیتهاست.
- هدف: ایجاد یک سیگنال جدید که تنها زمانی مقدار خود را منتشر میکند که یک دوره زمانی مشخص از آخرین انتشار مقدار گذشته باشد.
- کاربرد: جلوگیری از فراخوانیهای مکرر در هنگام تایپ کردن کاربر در یک فیلد جستجو. کاربر تایپ میکند و درخواست به سرور تنها زمانی ارسال میشود که کاربر برای مثلاً ۳۰۰ میلیثانیه دست از تایپ کردن بردارد.
- پیادهسازی پیشنهادی: میتوان یک تابع سازنده (Factory Function) نوشت که یک سیگنال ورودی و یک زمان تأخیر میگیرد و یک
ComputedSignal جدید برمیگرداند.
نمونه کد مفهومی:
import { StateSignal, ComputedSignal } from './';
import { debounce } from '@alwatr/delay';
// Factory function
export function debounceSignal<T>(
sourceSignal: StateSignal<T>,
debounceTimeMs: number,
): ComputedSignal<T> {
const debouncedSet = debounce(
(value: T) => internalSignal.set(value),
debounceTimeMs,
);
const internalSignal = new StateSignal<T>({
signalId: `${sourceSignal.signalId}-debounced`,
initialValue: sourceSignal.value,
});
// Listen to the source and call the debounced setter
sourceSignal.subscribe(newValue => {
debouncedSet(newValue);
});
// The public API is a readonly computed signal
return new ComputedSignal({
signalId: `${sourceSignal.signalId}-debounced-computed`,
deps: [internalSignal],
get: () => internalSignal.value,
});
}
// --- نحوه استفاده ---
const searchInput = new StateSignal({ initialValue: '', signalId: 'searchInput' });
const debouncedSearch = debounceSignal(searchInput, 300);
new EffectSignal({
deps: [debouncedSearch],
run: () => {
console.log(`Sending request for: ${debouncedSearch.value}`);
}
});
searchInput.set('alwatr');
searchInput.set('alwatr signal'); // درخواست قبلی لغو و تایمر ریست میشود
۲. اپراتورهای تبدیل و فیلتر (Transformation & Filtering Operators)
این اپراتورها به شما اجازه میدهند سیگنالهای جدیدی را بر اساس مقادیر سیگنالهای دیگر بسازید. اگرچه ComputedSignal این کار را به صورت کلی انجام میدهد، اما ارائه توابع کمکی (Helpers) مشخص، کد را خواناتر میکند.
الف) mapSignal (مشابه map)
- هدف: ایجاد یک سیگنال جدید که مقادیر آن حاصل اعمال یک تابع بر روی مقادیر یک سیگنال دیگر است.
- کاربرد: شما یک
StateSignal<User> دارید و میخواهید یک ComputedSignal<string> برای نمایش نام کامل کاربر داشته باشید.
پیادهسازی پیشنهادی: این در واقع یک سادهسازی برای ComputedSignal است.
// Factory function
export function mapSignal<T, R>(
sourceSignal: IReadonlySignal<T>,
project: (value: T) => R,
): IReadonlySignal<R> {
return new ComputedSignal({
signalId: `${sourceSignal.signalId}-mapped`,
deps: [sourceSignal],
get: () => project(sourceSignal.value),
});
}
// --- نحوه استفاده ---
const userSignal = new StateSignal({ initialValue: { firstName: 'John', lastName: 'Doe' } });
const fullNameSignal = mapSignal(userSignal, user => `${user.firstName} ${user.lastName}`);
console.log(fullNameSignal.value); // "John Doe"
ب) filterSignal (مشابه filter)
- هدف: ایجاد یک سیگنال جدید که تنها مقادیر سیگنال منبع را منتشر میکند که از یک شرط (Predicate) خاص عبور کنند.
- کاربرد: فرض کنید یک سیگنال برای اعداد دارید و فقط میخواهید به اعداد زوج واکنش نشان دهید.
پیادهسازی پیشنهادی:
// Factory function
export function filterSignal<T>(
sourceSignal: StateSignal<T>,
predicate: (value: T) => boolean,
): StateSignal<T | undefined> {
const filteredSignal = new StateSignal<T | undefined>({
signalId: `${sourceSignal.signalId}-filtered`,
initialValue: predicate(sourceSignal.value) ? sourceSignal.value : undefined,
});
sourceSignal.subscribe(newValue => {
if (predicate(newValue)) {
filteredSignal.set(newValue);
}
});
return filteredSignal;
}
// --- نحوه استفاده ---
const numberSignal = new StateSignal({ initialValue: 0 });
const evenNumberSignal = filterSignal(numberSignal, num => num % 2 === 0);
evenNumberSignal.subscribe(num => {
if(num !== undefined) console.log(`Even number detected: ${num}`);
});
numberSignal.set(1); // اتفاقی نمیافتد
numberSignal.set(2); // "Even number detected: 2"
۳. ترکیب سیگنالها (Combination Signals)
این بخش برای هماهنگ کردن چندین منبع داده بسیار قدرتمند است.
الف) mergeSignals (مشابه merge)
- هدف: ایجاد یک
EventSignal جدید که هرگاه هر یک از EventSignal های ورودی یک رویداد منتشر کنند، آن رویداد را منتشر میکند.
- کاربرد: شما چندین دکمه (
onSaveClick, onEnterPress) دارید و میخواهید هر دوی آنها یک عمل یکسان (مثلاً ذخیره فرم) را تحریک کنند.
پیادهسازی پیشنهادی:
// Factory function
export function mergeSignals<T>(
signalId: string,
signals: EventSignal<T>[],
): EventSignal<T> {
const mergedSignal = new EventSignal<T>({ signalId });
signals.forEach(signal => {
signal.subscribe(value => {
mergedSignal.dispatch(value);
});
});
// Handle destruction
const originalDestroy = mergedSignal.destroy;
mergedSignal.destroy = () => {
// Unsubscribe from all sources (memory leak prevention)
// ... logic to unsubscribe ...
originalDestroy.call(mergedSignal);
};
return mergedSignal;
}
// --- نحوه استفاده ---
const onSaveClick = new EventSignal<{data: string}>({ signalId: 'onSaveClick' });
const onEnterPress = new EventSignal<{data: string}>({ signalId: 'onEnterPress' });
const onFormSubmit = mergeSignals('onFormSubmit', [onSaveClick, onEnterPress]);
onFormSubmit.subscribe(payload => console.log('Form submitted:', payload.data));
onSaveClick.dispatch({data: 'from click'}); // "Form submitted: from click"
onEnterPress.dispatch({data: 'from enter'}); // "Form submitted: from enter"
۴. ایده پیشرفته: ResourceSignal برای عملیات ناهمزمان
این یک مفهوم سطح بالاتر برای مدیریت حالتهای رایج در واکشی داده (Data Fetching) است.
- هدف: کپسوله کردن کامل یک عملیات ناهمزمان (مانند یک درخواست API) و مدیریت حالتهای آن (
initial, loading, success, error).
- الهام از: اپراتورهای
switchMap و exhaustMap در RxJS که برای مدیریت درخواستهای شبکه استفاده میشوند.
- پیادهسازی: این میتواند یک کلاس جدید باشد که یک
StateSignal داخلی برای نگهداری وضعیت فعلی ({state: 'loading' | 'success' | 'error', data?: T, error?: E}) دارد و یک متد trigger برای شروع عملیات.
کد مفهومی:
type ResourceState<T, E = Error> =
| { state: 'initial' }
| { state: 'loading' }
| { state: 'success', data: T }
| { state: 'error', error: E };
class ResourceSignal<T, P> {
private readonly stateSignal: StateSignal<ResourceState<T>>;
private readonly asyncAction: (params: P) => Promise<T>;
constructor(config: { signalId: string, action: (params: P) => Promise<T> }) {
this.stateSignal = new StateSignal({
signalId: `${config.signalId}-state`,
initialValue: { state: 'initial' },
});
this.asyncAction = config.action;
}
get value() {
return this.stateSignal.value;
}
subscribe(callback) {
return this.stateSignal.subscribe(callback);
}
async trigger(params: P): Promise<void> {
if (this.stateSignal.value.state === 'loading') return; // Prevent concurrent requests
this.stateSignal.set({ state: 'loading' });
try {
const data = await this.asyncAction(params);
this.stateSignal.set({ state: 'success', data });
} catch (err) {
this.stateSignal.set({ state: 'error', error: err as Error });
}
}
}
چند ایده در مورد سیگنال
۱. سیگنالهای مبتنی بر زمان (Time-Based Signals)
این دسته از قابلیتها در مدیریت تعاملات کاربر (UI Events) بسیار حیاتی هستند و مستقیماً از اپراتورهای RxJS الهام گرفته شدهاند.
الف)
debounceSignal(مشابهdebounceTime)این یکی از پرکاربردترین و ضروریترین قابلیتهاست.
ComputedSignalجدید برمیگرداند.نمونه کد مفهومی:
۲. اپراتورهای تبدیل و فیلتر (Transformation & Filtering Operators)
این اپراتورها به شما اجازه میدهند سیگنالهای جدیدی را بر اساس مقادیر سیگنالهای دیگر بسازید. اگرچه
ComputedSignalاین کار را به صورت کلی انجام میدهد، اما ارائه توابع کمکی (Helpers) مشخص، کد را خواناتر میکند.الف)
mapSignal(مشابهmap)StateSignal<User>دارید و میخواهید یکComputedSignal<string>برای نمایش نام کامل کاربر داشته باشید.پیادهسازی پیشنهادی: این در واقع یک سادهسازی برای
ComputedSignalاست.ب)
filterSignal(مشابهfilter)پیادهسازی پیشنهادی:
۳. ترکیب سیگنالها (Combination Signals)
این بخش برای هماهنگ کردن چندین منبع داده بسیار قدرتمند است.
الف)
mergeSignals(مشابهmerge)EventSignalجدید که هرگاه هر یک ازEventSignalهای ورودی یک رویداد منتشر کنند، آن رویداد را منتشر میکند.onSaveClick,onEnterPress) دارید و میخواهید هر دوی آنها یک عمل یکسان (مثلاً ذخیره فرم) را تحریک کنند.پیادهسازی پیشنهادی:
۴. ایده پیشرفته:
ResourceSignalبرای عملیات ناهمزماناین یک مفهوم سطح بالاتر برای مدیریت حالتهای رایج در واکشی داده (Data Fetching) است.
initial,loading,success,error).switchMapوexhaustMapدر RxJS که برای مدیریت درخواستهای شبکه استفاده میشوند.StateSignalداخلی برای نگهداری وضعیت فعلی ({state: 'loading' | 'success' | 'error', data?: T, error?: E}) دارد و یک متدtriggerبرای شروع عملیات.کد مفهومی: