اكتشف كيف تُحدث Angular Signals تحولاً جذرياً في إدارة الحالة، مع أداء فائق وكود أنظف. دليل عملي عميق لفهم الآلية خلف الكواليس وتطبيقها في مشاريع حقيقية دون الوقوع في الفخاخ الشائعة.
تخيل أنك تعمل على تطبيق Angular ضخم، وكلما أضفت مكوناً جديداً، يتباطأ الأداء بشكل ملحوظ. المشكلة ليست في الكود نفسه، بل في الطريقة التي يتعامل بها Angular مع التغييرات. كل مرة تُحدث فيها قيمة، يُعيد الإطار تقييم المكون بالكامل، حتى لو كان التغيير بسيطاً. هذا ليس مجرد بطء عابر، بل كارثة حقيقية: تطبيقات تفقد ٣٠٪ من أدائها بسبب إعادة الحسابات غير الضرورية. هنا تأتي Angular Signals كحل ثوري، لكنها ليست مجرد إضافة جديدة، بل إعادة تفكير كاملة في كيفية إدارة الحالة والتغييرات في التطبيقات الحديثة.
في الماضي، اعتمدنا على RxJS وZone.js لإدارة الحالة والتغييرات، لكن هذه الأدوات تأتي بثمن: تعقيد في الكود، صعوبة في تتبع تدفق البيانات، وأداء دون المستوى في التطبيقات الكبيرة. Angular Signals تقدم نموذجاً جديداً يعتمد على مفهوم الـ "reactivity" المبسط، حيث تُصبح التحديثات دقيقة وفعالة. لكن كيف تعمل بالضبط؟ وما الذي يحدث خلف الكواليس في الذاكرة والمعالج؟ هذا ما سنكتشفه في هذا المقال، مع أمثلة عملية وتطبيقات حقيقية.
Angular Signals هي نظام إدارة حالة جديد يُقدمه Angular بدءاً من الإصدار 16، لكنه ليس مجرد إضافة بسيطة، بل تحول جذري في كيفية تعامل الإطار مع التغييرات. الفكرة الأساسية بسيطة: بدلاً من إعادة تقييم المكون بالكامل عند تغيير قيمة، تُحدث الإشارات (Signals) فقط الأجزاء التي تعتمد على تلك القيمة. هذا يُعرف بـ "fine-grained reactivity"، وهو مفهوم ليس جديداً في عالم الويب (انظر إلى Svelte وSolid.js)، لكنه يُحدث ثورة في عالم Angular.
لفهم الفرق، دعنا نقارن بين الطريقة التقليدية باستخدام RxJS والطريقة الجديدة باستخدام Signals. في الطريقة التقليدية، إذا كان لديك مكون يعرض قائمة من البيانات، وكان لديك اشتراك في Observable، فإن أي تغيير في البيانات سيُعيد تشغيل الكود بالكامل داخل المكون، حتى لو كان التغيير يؤثر فقط على جزء صغير من الواجهة. أما مع Signals، فإن التغيير يُحدث فقط الجزء المتأثر من الـ DOM، مما يُقلل من عمليات إعادة الحساب ويُحسن الأداء بشكل كبير.
// الطريقة التقليدية باستخدام RxJS
import { Component } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Component({
selector: 'app-traditional',
template: `{{ data$ | async }}`
})
export class TraditionalComponent {
private dataSubject = new BehaviorSubject<string>('Initial');
data$ = this.dataSubject.asObservable();
updateData(newData: string) {
this.dataSubject.next(newData); // يُعيد تقييم المكون بالكامل
}
}
// الطريقة الجديدة باستخدام Signals
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-signals',
template: `{{ data() }}`
})
export class SignalsComponent {
data = signal<string>('Initial');
updateData(newData: string) {
this.data.set(newData); // يُحدث فقط الجزء المتأثر من الـ DOM
}
}الفرق هنا ليس مجرد في الكود، بل في ما يحدث خلف الكواليس. عندما تُحدث Signal، يُنشئ Angular قائمة بالتبعيات (dependencies) لكل قيمة، ويُحدث فقط المكونات التي تعتمد على تلك القيمة. هذا يعني أن التغييرات تُصبح دقيقة وفعالة، دون الحاجة إلى إعادة تقييم المكون بالكامل. لكن كيف يعمل هذا بالضبط في الذاكرة؟
لفهم كيف تعمل Signals، يجب أن نغوص في تفاصيل كيفية إدارة Angular للذاكرة والمعالج. عندما تُنشئ Signal، يُخصص Angular مساحة في الذاكرة لتخزين قيمتها، بالإضافة إلى قائمة بالتبعيات. هذه القائمة تحتوي على جميع المكونات أو الحسابات التي تعتمد على هذه Signal. عندما تُحدث قيمة Signal باستخدام `set` أو `update`، يُمرر Angular على قائمة التبعيات ويُعلم كل تبعية بأن القيمة قد تغيرت.
لكن الأمر لا يتوقف هنا. Angular يستخدم نظاماً ذكياً لتجنب التحديثات غير الضرورية. إذا كانت القيمة الجديدة مساوية للقيمة القديمة، فلن يُحدث أي شيء. هذا يُعرف بـ "equality check"، وهو جزء أساسي من كفاءة Signals. بالإضافة إلى ذلك، يُدير Angular التبعيات بطريقة تُجنب حدوث الـ "glitches"، وهي حالات تحدث عندما تُحدث قيمة قبل أن تكتمل الحسابات التي تعتمد عليها، مما يؤدي إلى نتائج غير متوقعة.
// مثال على تجنب التحديثات غير الضرورية
import { signal, effect } from '@angular/core';
const count = signal(0);
effect(() => {
console.log('Count changed:', count()); // لن يُطبع إلا إذا تغيرت القيمة
});
count.set(0); // لا يحدث تحديث لأن القيمة لم تتغير
count.set(1); // يحدث تحديث
count.set(1); // لا يحدث تحديث مرة أخرىلننتقل الآن إلى التطبيق العملي. لنفترض أنك تبني لوحة تحكم تعرض بيانات في الوقت الفعلي، وتحتاج إلى تحديث الواجهة كلما تغيرت البيانات. باستخدام Signals، يمكنك بناء مكونات ذكية تُحدث نفسها بكفاءة دون الحاجة إلى إعادة الحساب الكامل. إليك مثال عملي على مكون يعرض قائمة من المهام، ويُحدث الواجهة فقط عند تغيير حالة مهمة معينة.
@Component({
selector: 'app-todo-list',
template: `
<h2>قائمة المهام</h2>
<ul>
<li *ngFor="let task of tasks()">
{{ task.name }} - {{ task.completed ? 'مكتملة' : 'قيد التنفيذ' }}
<button (click)="toggleTask(task.id)">تبديل الحالة</button>
</li>
</ul>
<p>عدد المهام المكتملة: {{ completedCount() }}</p>
`
})
export class TodoListComponent {
tasks = signal<{ id: number; name: string; completed: boolean }[]>([
{ id: 1, name: 'كتابة المقال', completed: false },
{ id: 2, name: 'مراجعة الكود', completed: true },
{ id: 3, name: 'تحديث الوثائق', completed: false }
]);
// حساب مشتق (computed signal) لعدد المهام المكتملة
completedCount = computed(() => {
return this.tasks().filter(task => task.completed).length;
});
toggleTask(id: number) {
this.tasks.update(tasks => {
return tasks.map(task =>
task.id === id ? { ...task, completed: !task.completed } : task
);
});
}
}في هذا المثال، نستخدم `computed` لإنشاء Signal مشتق يحسب عدد المهام المكتملة. الميزة هنا هي أن `completedCount` لن يُعاد حسابه إلا عند تغيير حالة مهمة ما، وليس عند أي تغيير في القائمة. هذا يُحسن الأداء بشكل كبير، خاصة في القوائم الكبيرة. لاحظ أيضاً أننا نستخدم `update` لتعديل القائمة، وهو ما يُمكّننا من تعديل القيمة بناءً على حالتها الحالية دون الحاجة إلى قراءة القيمة أولاً ثم تعديلها.
قد تتساءل: ماذا عن RxJS؟ هل ستُستبدل بالكامل بـ Signals؟ الحقيقة هي أن Signals وRxJS يُكملان بعضهما البعض. يمكنك تحويل Signal إلى Observable بسهولة، والعكس صحيح. هذا يعني أنك لست مضطراً للتخلي عن مكتباتك المفضلة أو الكود القديم، بل يمكنك دمج Signals في مشروعك تدريجياً.
import { signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
import { map } from 'rxjs/operators';
// تحويل Signal إلى Observable
const count = signal(0);
const count$ = toObservable(count);
// تحويل Observable إلى Signal
const timer$ = interval(1000).pipe(map(() => new Date()));
const time = toSignal(timer$);
// استخدامهما معاً
count$.subscribe(value => {
console.log('Count changed:', value);
});
effect(() => {
console.log('Current time:', time());
});هذا التكامل يُتيح لك الاستفادة من قوة RxJS في التعامل مع الـ async operations، وفي نفس الوقت الاستفادة من بساطة وفعالية Signals في إدارة الحالة المحلية. على سبيل المثال، يمكنك استخدام RxJS للتعامل مع الـ HTTP requests، ثم تحويل النتيجة إلى Signal لعرضها في الواجهة بكفاءة.
رغم بساطة Signals، هناك بعض الفخاخ التي قد يقع فيها المطورون، خاصة عند الانتقال من نمط RxJS التقليدي. أحد هذه الفخاخ هو محاولة استخدام Signals كبديل كامل لـ Observables في جميع الحالات. الحقيقة هي أن Signals ليست مصممة للتعامل مع الـ async operations المعقدة مثل الـ HTTP requests أو الـ WebSockets. في هذه الحالات، يظل RxJS هو الخيار الأفضل.
فخ آخر هو إساءة استخدام `computed`. على الرغم من أن `computed` يُحسن الأداء في كثير من الحالات، إلا أن استخدامه بشكل مفرط قد يؤدي إلى تعقيد الكود دون فائدة حقيقية. مثلاً، إذا كان لديك حساب بسيط لا يعتمد على أي Signal أخرى، فلا حاجة لاستخدام `computed`. بدلاً من ذلك، يمكنك حساب القيمة مباشرة في القالب باستخدام دالة عادية.
// مثال على إساءة استخدام computed
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-bad-example',
template: `{{ fullName() }}`
})
export class BadExampleComponent {
firstName = signal('أحمد');
lastName = signal('محمد');
// لا حاجة لاستخدام computed هنا
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
// الأفضل استخدام دالة عادية
getFullName() {
return `${this.firstName()} ${this.lastName()}`;
}
}فخ ثالث هو تجاهل الـ "effects". على الرغم من أن `effect` يُستخدم بشكل أقل من `computed`، إلا أنه أداة قوية لتنفيذ عمليات جانبية (side effects) عند تغيير قيمة Signal. لكن يجب استخدامه بحذر، خاصة في المكونات، لأنه قد يؤدي إلى مشاكل في الأداء إذا تم استخدامه بشكل غير صحيح. مثلاً، إذا استخدمت `effect` لتحديث قيمة Signal أخرى داخل مكون، فقد يؤدي ذلك إلى حلقة تحديث لا نهائية.
Angular Signals ليست مجرد ميزة عابرة، بل هي جزء من تحول أكبر في كيفية بناء تطبيقات Angular. في المستقبل، نتوقع أن تُصبح Signals هي الطريقة الأساسية لإدارة الحالة في Angular، مع تقليل الاعتماد على RxJS في معظم الحالات. هذا لا يعني اختفاء RxJS، بل يعني أنها ستُستخدم فقط في الحالات التي تتطلب معالجة معقدة للبيانات غير المتزامنة.
من تجربتي الشخصية، أرى أن Signals ستُحدث تغييراً كبيراً في طريقة تفكير المطورين في إدارة الحالة. بدلاً من القلق بشأن إعادة الحسابات غير الضرورية أو تعقيد RxJS، ستُصبح إدارة الحالة أكثر بساطة وفعالية. لكن هذا التحول يتطلب وقتاً للتعود عليه، خاصة للمطورين الذين اعتادوا على نمط RxJS التقليدي.
إذا كنت تريد البدء مع Angular Signals اليوم، إليك نصيحتي العملية: لا تحاول استبدال كل شيء في مشروعك القديم دفعة واحدة. بدلاً من ذلك، ابدأ بمكون واحد صغير واستخدم Signals لإدارة حالته. لاحظ الفرق في الأداء وسهولة الكود. ثم انتقل تدريجياً إلى المكونات الأخرى.
ابدأ باستخدام `signal` و`computed` في المكونات البسيطة، ثم انتقل إلى استخدام `effect` للعمليات الجانبية. جرب تحويل بعض الـ Observables إلى Signals باستخدام `toSignal`، ولاحظ كيف يُصبح الكود أكثر بساطة. وأخيراً، لا تخف من التجربة والخطأ، فالأخطاء هي أفضل طريقة للتعلم.
// مثال عملي للبدء
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>العداد: {{ count() }}</p>
<button (click)="increment()">زيادة</button>
<p>مربع العدد: {{ squaredCount() }}</p>
`
})
export class CounterComponent {
count = signal(0);
squaredCount = computed(() => this.count() * this.count());
constructor() {
effect(() => {
console.log('Count changed:', this.count());
});
}
increment() {
this.count.update(c => c + 1);
}
}في النهاية، Angular Signals ليست مجرد إضافة جديدة، بل هي ثورة في كيفية بناء تطبيقات Angular. إنها تُعيد تعريف إدارة الحالة، وتُعيد التفكير في كيفية تفاعل المكونات مع بعضها البعض. إذا كنت تريد بناء تطبيقات سريعة وفعالة وسهلة الصيانة، فإن تعلم واستخدام Signals هو الخطوة التالية المنطقية في رحلتك مع Angular.