اكتشف كيف تغير Angular Signals قواعد اللعبة في إدارة الحالة، مع شرح تقني عميق وأمثلة عملية تكشف أسرار الأداء والذاكرة خلف الكواليس.
تخيل أنك تعمل على تطبيق Angular ضخم، وكلما أضفت ميزة جديدة، يتباطأ الأداء بشكل ملحوظ. تفتح أدوات المطورين لترى عشرات الـ Event Listeners التي لم تُزال، ومئات الـ Change Detection Cycles التي تُنفذ دون داعٍ. هذا ليس سيناريو خيالياً، بل واقع يعيشه كل مطور Angular بعد بضعة أشهر من تطوير المشروع. المشكلة ليست في Angular نفسه، بل في الطريقة التقليدية لإدارة الحالة باستخدام RxJS وZone.js. هنا تأتي Angular Signals كحل ثوري، لكنها ليست مجرد إضافة جديدة، بل إعادة تفكير كاملة في كيفية تفاعل المكونات مع البيانات.
عندما أعلنت Google عن Angular Signals في الإصدار 16، كان السؤال الأول الذي دار في ذهني: هل هذا مجرد بديل لـ RxJS أم شيء مختلف تماماً؟ بعد أشهر من التجربة في مشاريع حقيقية، أستطيع القول بثقة: Signals ليست بديلاً، بل نموذج برمجي جديد تماماً. إنها تحول جذري في كيفية تعامل Angular مع التحديثات، وتقدم حلولاً لمشاكل كانت تعتبر جزءاً لا يتجزأ من إطار العمل نفسه. لكن لماذا نحتاج إلى هذا التغيير أصلاً؟
لفهم أهمية Signals، يجب أولاً فهم المشكلة التي تحلها. في Angular التقليدي، يعتمد نظام Change Detection على Zone.js، وهي مكتبة تقوم بتتبع جميع الأحداث غير المتزامنة (مثل النقرات، طلبات HTTP، الـ Timeouts) وتطلق دورة كشف التغييرات عند اكتمالها. هذا النهج له مزايا واضحة: البساطة وسهولة الاستخدام. لكن عيوبه تصبح واضحة في التطبيقات الكبيرة:
في أحد المشاريع التي عملت عليها، كان لدينا جدول بيانات يحتوي على آلاف الصفوف، وكل تعديل في خلية واحدة كان يطلق دورة كشف كاملة تؤدي إلى تجميد واجهة المستخدم لمدة ثوانٍ. استخدمنا OnPush Change Detection Strategy لتقليل عدد الدورات، لكن هذا أدى إلى مشاكل أخرى مثل عدم تحديث المكونات التي تعتمد على بيانات غير مباشرة. كانت هذه التجربة هي التي دفعتني للبحث عن بدائل حقيقية، وليس مجرد تحسينات مؤقتة.
في جوهرها، Signals هي نظام تفاعلي دقيق (Fine-grained Reactivity System). بدلاً من الاعتماد على دورات كشف التغييرات الشاملة، تستخدم Signals نظام تتبع تبعيات ديناميكي. إليك ما يحدث بالضبط خلف الكواليس عندما تنشئ Signal:
// إنشاء Signal بسيط
import { signal } from '@angular/core';
const count = signal(0);
// عند قراءة قيمة Signal
console.log(count()); // 0
// عند تحديث قيمة Signal
count.set(5); // أو count.update(v => v + 1);عندما تنشئ Signal، يقوم Angular بإنشاء كائن خاص في الذاكرة يحتوي على القيمة الحالية والـ Subscribers (المكونات أو التأثيرات التي تعتمد على هذه القيمة). لكن الأهم هو ما يحدث عند قراءة Signal داخل قالب أو تأثير:
لفهم الفرق بشكل أعمق، دعونا نلقي نظرة على ما يحدث في الذاكرة. عندما تستخدم Signal، يتم إنشاء بنية بيانات خاصة تسمى Dependency Graph. هذه البنية تتبع العلاقات بين Signals والمكونات التي تعتمد عليها. على سبيل المثال، إذا كان لديك مكون يعرض قيمة Signal، ومكون آخر يعتمد على نفس Signal، فإن Dependency Graph سيبدو كالتالي:
Signal (count) → [ComponentA, ComponentB]
// عند تحديث count
count.set(10);
// Angular يقوم فقط بتحديث ComponentA و ComponentB
// دون الحاجة إلى تحديث المكونات الأخرى في الشجرةهذا النهج يختلف تماماً عن RxJS، حيث تحتاج إلى إدارة الـ Subscriptions يدوياً وتحديد متى وكيف يتم تحديث المكونات. مع Signals، يتم تتبع التبعيات تلقائياً، ويتم التحديث فقط عند الحاجة. هذا لا يحسن الأداء فحسب، بل يقلل أيضاً من تعقيد الكود بشكل كبير.
لننتقل الآن إلى الجانب العملي. كيف يمكنك استخدام Signals في مشروع Angular حقيقي؟ دعونا نبدأ بمثال بسيط، ثم ننتقل إلى حالات أكثر تعقيداً. لنفترض أنك تعمل على تطبيق لإدارة المهام، وتحتاج إلى تتبع عدد المهام المكتملة:
@Component({
selector: 'app-task-list',
template: `
<h2>المهام المكتملة: {{ completedCount() }}</h2>
<button (click)="addCompletedTask()">أضف مهمة مكتملة</button>
`,
})
export class TaskListComponent {
// باستخدام RxJS
private _completedTasks = new BehaviorSubject<number>(0);
completedTasks$ = this._completedTasks.asObservable();
// باستخدام Signals
completedCount = signal(0);
addCompletedTask() {
// RxJS Approach
this._completedTasks.next(this._completedTasks.value + 1);
// Signals Approach
this.completedCount.update(v => v + 1);
}
}في هذا المثال البسيط، يبدو الفرق طفيفاً. لكن عندما ننتقل إلى حالات أكثر تعقيداً، مثل إدارة الحالة العالمية أو التعامل مع البيانات المشتقة، يصبح الفرق واضحاً. لنأخذ مثالاً على إدارة حالة عربة التسوق في تطبيق تجارة إلكترونية:
@Injectable({ providedIn: 'root' })
export class CartService {
// RxJS Approach
private _items = new BehaviorSubject<CartItem[]>([]);
items$ = this._items.asObservable();
// Signals Approach
items = signal<CartItem[]>([]);
// مشتق تلقائي
total = computed(() => {
return this.items().reduce((sum, item) => sum + (item.price * item.quantity), 0);
});
addItem(item: CartItem) {
// RxJS
this._items.next([...this._items.value, item]);
// Signals
this.items.update(items => [...items, item]);
}
removeItem(itemId: string) {
// RxJS
this._items.next(this._items.value.filter(item => item.id !== itemId));
// Signals
this.items.update(items => items.filter(item => item.id !== itemId));
}
}لاحظ كيف أن computed في المثال أعلاه يقوم بإنشاء Signal مشتقة تتحديث تلقائياً عند تغيير items. هذا يختلف تماماً عن RxJS، حيث تحتاج إلى إنشاء Observable جديد باستخدام operators مثل map أو combineLatest. لكن الميزة الحقيقية تظهر عندما نستخدم هذه الحالة في المكونات:
@Component({
selector: 'app-cart',
template: `
<div *ngFor="let item of cartService.items()">
{{ item.name }} - {{ item.price }} × {{ item.quantity }}
</div>
<div>الإجمالي: {{ cartService.total() | currency }}</div>
`,
})
export class CartComponent {
constructor(public cartService: CartService) {}
}في هذا المثال، لا تحتاج إلى استخدام async pipe أو إدارة الـ Subscriptions. Angular يتتبع تلقائياً تبعيات القالب ويحدث فقط الأجزاء التي تغيرت. لكن ماذا عن الأداء؟ في مشروع حقيقي، قمنا بقياس الفرق بين النهجين في تطبيق يحتوي على 500 مكون و1000 Signal. كانت النتائج مذهلة:
رغم كل المزايا التي تقدمها Signals، إلا أنها ليست حلاً سحرياً. هناك تحديات وفخاخ يجب أن تكون على دراية بها قبل اعتمادها في مشروعك. أول هذه التحديات هو التعامل مع البيانات غير المتزامنة. على عكس RxJS، لا تأتي Signals مع دعم مدمج للعمليات غير المتزامنة مثل HTTP Requests. إليك كيف يمكنك التعامل مع هذا:
@Injectable({ providedIn: 'root' })
export class ProductService {
products = signal<Product[]>([]);
isLoading = signal(false);
error = signal<string | null>(null);
async loadProducts() {
this.isLoading.set(true);
this.error.set(null);
try {
const resp await fetch('https://api.example.com/products');
const data = await response.json();
this.products.set(data);
} catch (err) {
this.error.set('فشل تحميل المنتجات');
} finally {
this.isLoading.set(false);
}
}
}هذا النهج يعمل بشكل جيد للحالات البسيطة، لكن ماذا عن العمليات الأكثر تعقيداً مثل التسلسل أو الإلغاء؟ هنا قد تحتاج إلى الجمع بين Signals وRxJS. نعم، هذا ممكن، وقد يكون الحل الأمثل في بعض الحالات. إليك مثال على كيفية دمج الاثنين:
@Component({
selector: 'app-search',
template: `
<input [ngModel]="searchQuery()" (ngModelChange)="onSearchChange($event)">
<div *ngIf="isLoading()">جاري البحث...</div>
<div *ngFor="let result of results()">{{ result.name }}</div>
`,
})
export class SearchComponent {
searchQuery = signal('');
results = signal<Product[]>([]);
isLoading = signal(false);
private search$ = new Subject<string>();
constructor(private productService: ProductService) {
this.search$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(query => {
this.isLoading.set(true);
return this.productService.searchProducts(query).pipe(
catchError(() => of([])),
finalize(() => this.isLoading.set(false))
);
})
).subscribe(results => {
this.results.set(results);
});
}
onSearchChange(query: string) {
this.searchQuery.set(query);
this.search$.next(query);
}
ngOnDestroy() {
this.search$.complete();
}
}في هذا المثال، نستخدم RxJS لإدارة الجزء غير المتزامن من البحث (debounce، distinctUntilChanged، switchMap)، بينما نستخدم Signals لتخزين الحالة وعرضها في القالب. هذا الجمع بين التقنيتين يمنحك أفضل ما في العالمين: القوة التفاعلية لـ RxJS والبساطة والأداء العالي لـ Signals.
فخ آخر يجب الانتباه إليه هو الـ Memory Leaks. على عكس RxJS حيث تحتاج إلى إدارة الـ Subscriptions يدوياً، قد تعتقد أن Signals لا تسبب تسريبات ذاكرة. لكن هذا ليس صحيحاً تماماً. إذا قمت بإنشاء تأثير (effect) داخل مكون ولم تقم بإلغائه عند تدمير المكون، فقد يؤدي ذلك إلى تسريب ذاكرة. إليك كيفية تجنب هذا:
@Component({
selector: 'app-user-profile',
template: `...`,
})
export class UserProfileComponent implements OnInit, OnDestroy {
private userId = signal<string | null>(null);
private effectRef: EffectRef | null = null;
ngOnInit() {
this.effectRef = effect(() => {
const id = this.userId();
if (id) {
this.loadUserData(id);
}
});
}
ngOnDestroy() {
this.effectRef?.destroy();
}
private loadUserData(id: string) {
// تحميل بيانات المستخدم
}
}أخيراً، هناك تحدي يتعلق بالتوافق مع المكتبات الخارجية. إذا كنت تستخدم مكتبات تعتمد على RxJS أو Change Detection التقليدي، فقد تواجه بعض المشاكل عند محاولة دمجها مع Signals. على سبيل المثال، مكتبات الرسوم البيانية مثل D3.js أو Chart.js قد لا تعمل بشكل جيد مع Signals لأنها تتوقع تحديثات DOM مباشرة. في هذه الحالات، قد تحتاج إلى استخدام ViewChild أو ElementRef للوصول إلى عناصر DOM وتحديثها يدوياً
هذا السؤال يدور في أذهان الكثير من مطوري Angular. الحقيقة هي أن Signals وRxJS يهدفان إلى حل مشاكل مختلفة. RxJS هو مكتبة قوية لإدارة التدفقات غير المتزامنة والمعقدة، بينما Signals هي نظام تفاعلي دقيق لإدارة الحالة المتزامنة. في رأيي، لن تحل Signals محل RxJS تماماً، بل ستكملها في معظم الحالات.
في المستقبل القريب، أتوقع أن نرى:
لكن الأهم من ذلك هو التغيير في طريقة تفكيرنا كمطورين. مع Signals، نبدأ في التفكير في إدارة الحالة كسلسلة من التبعيات بدلاً من سلسلة من الأحداث. هذا التغيير في العقلية يمكن أن يؤدي إلى كود أكثر قابلية للصيانة وأفضل أداء، لكنه يتطلب أيضاً إعادة تعلم بعض المفاهيم الأساسية.
في أحد المشاريع الأخيرة، قمنا بإعادة كتابة جزء كبير من التطبيق باستخدام Signals. كانت النتيجة مذهلة: انخفض حجم الكود بنسبة 40%، وتحسن الأداء بشكل ملحوظ، وأصبح من الأسهل بكثير تتبع تدفق البيانات. لكن الأهم من ذلك هو أن الفريق أصبح أكثر إنتاجية، حيث لم يعد بحاجة إلى قضاء ساعات في إدارة الـ Subscriptions أو تحسين Change Detection.
إذا كنت تخطط لاعتماد Signals في مشروعك، إليك نصائحي العملية بناءً على تجربتي:
في النهاية، Angular Signals ليست مجرد ميزة جديدة، بل هي تحول جذري في كيفية بناء تطبيقات Angular. إنها تجعل الكود أكثر بساطة وأسرع أداء، لكنها تتطلب أيضاً تغييراً في طريقة تفكيرنا. إذا كنت تريد بناء تطبيقات Angular سريعة وقابلة للصيانة في المستقبل، فإن تعلم واستخدام Signals ليس خياراً، بل ضرورة.
ابدأ اليوم بإنشاء Signal بسيط في مكونك التالي، وشاهد بنفسك الفرق الذي تحدثه. صدقني، بمجرد أن تتعود على Signals، لن ترغب في العودة إلى الطرق القديمة لإدارة الحالة.