اكتشف كيف تُحدث Angular Signals تحولاً جذرياً في إدارة الحالة داخل تطبيقات Angular، مع أداء مذهل وتقليل عبء Change Detection إلى الحد الأدنى. دليل عملي لفهم الآلية خلف الكواليس وتطبيقها في مشاريع حقيقية.
تخيل أنك تعمل على تطبيق Angular ضخم يحتوي على مئات المكونات، وكل مرة تقوم فيها بتحديث قيمة بسيطة مثل عداد أو حقل إدخال، ينهار الأداء بالكامل. المتصفح يتجمد لثوانٍ، الـ Event Loop يتوقف، والمستخدم يرى شاشة بيضاء قبل أن يعود التطبيق إلى الحياة. هذه ليست مبالغة، بل واقع يومي يواجهه المطورون الذين يعتمدون على Zone.js و Change Detection التقليدي. المشكلة ليست في Angular نفسه، بل في الآلية التي يعتمد عليها لاكتشاف التغييرات وإعادة رسم الـ DOM. هنا تأتي Angular Signals كحل ثوري يُعيد تعريف إدارة الحالة داخل الإطار، مع وعود بتحسين الأداء بشكل لم نشهده من قبل.
في مؤتمر Angular Nation الأخير، كشف الفريق الأساسي عن أرقام صادمة: تطبيقات تستخدم Signals شهدت انخفاضاً في وقت تنفيذ Change Detection بنسبة تصل إلى ٩٠٪ مقارنة بالطريقة التقليدية. هذا ليس مجرد تحسين بسيط، بل تحول كامل في كيفية تعامل Angular مع البيانات والتحديثات. لكن ما هي Angular Signals بالضبط؟ وكيف تعمل خلف الكواليس لتحقق هذه القفزة الهائلة؟ ، كيف يمكنك استخدامها في مشاريعك الحالية دون الحاجة لإعادة كتابة كل شيء من الصفر؟
لفهم أهمية Angular Signals، يجب أولاً أن ندرك العيوب الكارثية في النظام الحالي. Angular يعتمد على Zone.js لاعتراض الأحداث غير المتزامنة مثل الـ click events أو الـ HTTP requests، ثم يقوم بتشغيل Change Detection على كامل شجرة المكونات. هذه العملية، رغم بساطتها، تصبح كابوساً عندما ينمو التطبيق. مثلاً، في تطبيق تجاري عملاق مثل Alibaba، وجد الفريق أن مجرد تغيير قيمة بسيطة في مكون صغير يؤدي إلى إعادة رسم مئات المكونات الأخرى، حتى لو لم تتأثر فعلياً بهذا التغيير. السبب؟ Zone.js لا يفهم العلاقات بين البيانات، بل يعتمد على آلية عمياء تسمى Dirty Checking.
الـ Dirty Checking هي عملية مقارنة القيم القديمة بالجديدة لكل خاصية في كل مكون، حتى لو لم تتغير هذه الخاصية منذ آخر مرة. تخيل أنك تملك مكوناً يحتوي على ٥٠ خاصية، وتغيرت واحدة فقط منها. Angular سيقوم بفحص الـ ٥٠ خاصية مرة أخرى، وهذا يحدث لكل مكون في الشجرة. في التطبيقات الكبيرة، يمكن أن يصل عدد هذه الفحوصات إلى عشرات الآلاف في الثانية الواحدة، مما يؤدي إلى استهلاك هائل لوحدة المعالجة المركزية وتباطؤ ملحوظ في الأداء. المشكلة تتفاقم مع الـ Observables، حيث تحتاج إلى استخدام async pipe أو إدارة الاشتراكات يدوياً، مما يزيد من تعقيد الكود ويجعل من الصعب تتبع تدفق البيانات.
Angular Signals ليست مجرد إضافة بسيطة للإطار، بل إعادة تصميم كاملة لكيفية إدارة الحالة والتحديثات. في جوهرها، Signal هي عبارة عن دالة ترجع قيمة، وتسمح لـ Angular بتتبع الاعتماديات بشكل دقيق دون الحاجة إلى Dirty Checking. عندما تقوم بإنشاء Signal باستخدام الدالة signal()، فإنك تخبر Angular: "هذه القيمة قد تتغير، وأنا أريد أن أكون على علم بأي تحديث يحدث لها". لكن السحر الحقيقي يكمن في كيفية تتبع Angular لهذه الاعتماديات.
خلف الكواليس، عندما تقوم باستدعاء Signal داخل قالب مكون، يقوم Angular بإنشاء ما يسمى بـ Dependency Graph. هذا الرسم البياني يتتبع بالضبط أي مكونات تعتمد على هذه Signal، وأي أجزاء من الـ DOM تحتاج إلى التحديث عند تغييرها. على عكس Zone.js الذي يعيد رسم الشجرة بأكملها، فإن Angular Signals تعمل على مستوى أدق بكثير: فهي تعرف بالضبط أي جزء من الـ DOM يحتاج إلى التحديث، ولا تهتم بالأجزاء الأخرى. هذا يعني أنه إذا كان لديك مكون يحتوي على ١٠ إشارات، وتغيرت واحدة فقط منها، فإن Angular سيقوم بتحديث الجزء المتعلق بهذه الإشارة فقط، دون لمس باقي المكون أو المكونات الأخرى.
// مثال بسيط يوضح كيفية إنشاء واستخدام Signal
import { signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<button (click)="increment()">زيادة</button>
<p>العداد: {{ count() }}</p>
<p>ضعف العداد: {{ doubleCount() }}</p>
`
})
export class CounterComponent {
// إنشاء Signal بقيمة أولية
count = signal(0);
// إنشاء Computed Signal يعتمد على Signal أخرى
doubleCount = computed(() => this.count() * 2);
increment() {
// تحديث قيمة Signal باستخدام set أو update
this.count.update(current => current + 1);
}
}
// لاحظ أن Computed Signal يتم تحديثه تلقائياً عند تغير count
// دون الحاجة إلى أي إدارة يدوية للاشتراكات أو Change Detectionفي المثال السابق، عندما نقوم باستدعاء count() داخل القالب، يقوم Angular بتسجيل هذا الاستدعاء كجزء من Dependency Graph للمكون. وعندما نقوم بتحديث قيمة count باستخدام update، فإن Angular يعرف بالضبط أن doubleCount يعتمد على count، وبالتالي يقوم بتحديثه أيضاً. لكن الأهم من ذلك، هو أن Angular يعرف أن القالب يحتاج إلى إعادة رسم الجزء المتعلق بـ count و doubleCount فقط، دون الحاجة إلى إعادة رسم المكون بالكامل أو تشغيل Change Detection على المكونات الأخرى. هذه الدقة هي ما يجعل Angular Signals أسرع بكثير من النظام التقليدي.
قد يتساءل البعض: هل Signals ستحل محل Observables تماماً؟ الإجابة القصيرة هي لا. Observables لا تزال الخيار الأفضل للتعامل مع تدفقات البيانات غير المتزامنة مثل الـ HTTP requests أو الـ WebSocket events. أما Signals، فهي مصممة للتعامل مع الحالة المتزامنة داخل التطبيق، مثل قيم العدادات أو حقول الإدخال أو أي بيانات تحتاج إلى تحديث فوري في الواجهة. الفرق الأساسي يكمن في كيفية إدارة الاشتراكات والتحديثات.
مع Observables، أنت بحاجة إلى إدارة الاشتراكات يدوياً أو استخدام async pipe في القوالب، وهذا يمكن أن يؤدي إلى مشاكل مثل الـ Memory Leaks إذا نسيت إلغاء الاشتراك. أما مع Signals، فإن Angular يتولى إدارة الاعتماديات تلقائياً، ولا تحتاج إلى القلق بشأن الاشتراكات أو إلغاءها. بالإضافة إلى ذلك، Signals توفر واجهة برمجة أبسط وأكثر وضوحاً، حيث يمكنك قراءة وتحديث القيم باستخدام دوال بسيطة مثل get() و set()، بدلاً من التعامل مع الـ operators المعقدة مثل map و filter و switchMap التي تتطلبها Observables.
الجميل في Angular Signals هو أنها لا تتطلب إعادة كتابة التطبيق بالكامل. يمكنك البدء باستخدامها في مكونات جديدة، ثم تدريجياً استبدال الـ @Input و @Output بالإشارات. لنأخذ مثالاً عملياً: تخيل أنك تعمل على تطبيق إدارة المهام يحتوي على مكونات متعددة مثل TaskList و TaskItem و TaskForm. في الوقت الحالي، تعتمد هذه المكونات على @Input لتمرير البيانات بينها، وهذا يؤدي إلى إعادة رسم المكونات بشكل غير ضروري عند تغيير أي قيمة.
بدلاً من ذلك، يمكنك إنشاء خدمة تحتوي على Signals لإدارة الحالة المشتركة بين المكونات. هذه الخدمة ستحتوي على إشارات مثل tasks و filter و selectedTask، وستقوم المكونات المختلفة بالاشتراك في هذه الإشارات دون الحاجة إلى تمرير البيانات عبر @Input. هذا النهج يقلل من تعقيد الكود ويحسن الأداء بشكل كبير، حيث أن تغيير قيمة في إحدى الإشارات لن يؤدي إلى إعادة رسم المكونات التي لا تعتمد عليها.
// مثال على خدمة تستخدم Signals لإدارة الحالة المشتركة
import { Injectable, signal, computed } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class TaskService {
// Signal تحتوي على قائمة المهام
private tasksSignal = signal<Task[]>([]);
// Signal تحتوي على فلتر المهام
filterSignal = signal<string>('all');
// Computed Signal تحتوي على المهام المفلترة
filteredTasks = computed(() => {
const filter = this.filterSignal();
const tasks = this.tasksSignal();
if (filter === 'completed') {
return tasks.filter(task => task.completed);
} else if (filter === 'active') {
return tasks.filter(task => !task.completed);
}
return tasks;
});
// إضافة مهمة جديدة
addTask(title: string) {
this.tasksSignal.update(tasks => [
...tasks,
{ id: Date.now(), title, completed: false }
]);
}
// تحديث حالة المهمة
toggleTask(id: number) {
this.tasksSignal.update(tasks =>
tasks.map(task =>
task.id === id ? { ...task, completed: !task.completed } : task
)
);
}
}
// في المكون، يمكنك استخدام الخدمة كما يلي:
@Component({
selector: 'app-task-list',
template: `
<div *ngFor="let task of taskService.filteredTasks()">
<app-task-item [task]="task"></app-task-item>
</div>
`
})
export class TaskListComponent {
constructor(public taskService: TaskService) {}
}في المثال السابق، لاحظ كيف أن المكون TaskList يعتمد على filteredTasks Signal، والتي تعتمد بدورها على tasksSignal و filterSignal. عندما نقوم بتحديث tasksSignal باستخدام addTask أو toggleTask، فإن filteredTasks يتم تحديثه تلقائياً، والمكون TaskList يعيد رسم نفسه دون الحاجة إلى تشغيل Change Detection على المكونات الأخرى. هذا النهج يقلل بشكل كبير من عدد عمليات إعادة الرسم، مما يؤدي إلى تحسين ملحوظ في الأداء، خاصة في التطبيقات الكبيرة.
رغم أن Angular Signals تبدو وكأنها الحل السحري لجميع مشاكل إدارة الحالة، إلا أنها تأتي مع مجموعة من الفخاخ التي يمكن أن تؤدي إلى مشاكل إذا لم تكن على دراية بها. أول هذه الفخاخ هو مشكلة الـ Circular Dependencies. تخيل أنك قمت بإنشاء computed signal يعتمد على signal أخرى، وهذه الأخيرة تعتمد بدورها على الـ computed signal الأولى. هذا سينتج عنه حلقة لا نهائية من التحديثات، مما يؤدي إلى تجميد التطبيق واستهلاك هائل لوحدة المعالجة المركزية.
مثال آخر هو مشكلة الـ Stale Closures. عندما تقوم بإنشاء computed signal داخل دالة، قد تحتفظ هذه الدالة بقيمة قديمة من الـ closure إذا لم تكن حذراً. على سبيل المثال، إذا قمت بإنشاء computed signal داخل دالة تستخدم متغيراً خارجياً، فإن هذا المتغير قد لا يتم تحديثه عند تغيره، مما يؤدي إلى نتائج غير متوقعة. الحل هو دائماً استخدام الدوال النقية عند إنشاء computed signals، وتجنب الاعتماد على المتغيرات الخارجية داخل هذه الدوال.
// مثال على مشكلة Stale Closures مع Signals
import { signal, computed } from '@angular/core';
@Component({
selector: 'app-stale-example',
template: `<p>{{ computedValue() }}</p>`
})
export class StaleExampleComponent {
count = signal(0);
multiplier = 2;
// ❌ هذا سيؤدي إلى مشكلة Stale Closures
// لأن multiplier يتم التقاطه في closure عند إنشاء computedValue
computedValue = computed(() => this.count() * this.multiplier);
updateMultiplier() {
this.multiplier = 3;
// حتى بعد تحديث multiplier، فإن computedValue سيظل يستخدم القيمة القديمة
}
// ✅ الحل الصحيح هو استخدام signal للمتغيرات التي قد تتغير
multiplierSignal = signal(2);
computedValueFixed = computed(() => this.count() * this.multiplierSignal());
updateMultiplierFixed() {
this.multiplierSignal.set(3); // الآن computedValueFixed سيتم تحديثه بشكل صحيح
}
}مشكلة أخرى شائعة هي الـ Memory Leaks عند تحويل Observables إلى Signals. عندما تستخدم الدالة toSignal لتحويل Observable إلى Signal، فإنك تحتاج إلى إدارة الاشتراك بشكل صحيح لتجنب تسرب الذاكرة. إذا لم تقم بإلغاء الاشتراك عند تدمير المكون، فإن الـ Observable سيظل يعمل في الخلفية، مما يؤدي إلى استهلاك غير ضروري للذاكرة والمعالج. الحل هو استخدام الدالة takeUntil أو حتى أفضل، استخدام الدالة toSignal مع خيار { manualCleanup: true } وإدارة الاشتراك يدوياً في ngOnDestroy.
الإجابة القصيرة هي نعم، لكن بخطوات مدروسة. Angular Signals ليست مجرد ميزة جديدة تضاف إلى الإطار، بل تحول كامل في كيفية إدارة الحالة والتحديثات داخل التطبيقات. إذا كنت تعمل على مشروع جديد، فلا تتردد في استخدام Signals منذ البداية. أما إذا كنت تعمل على مشروع موجود، فابدأ بدمج Signals تدريجياً في المكونات الجديدة، ثم قم باستبدال الـ @Input و @Output بالإشارات حيثما كان ذلك منطقياً. تذكر أن Angular Signals ليست بديلاً كاملاً لـ Observables، بل أداة مكملة لها، وكل منهما له استخداماته الخاصة.
من تجربتي الشخصية، فإن التحول إلى Angular Signals في مشروع متوسط الحجم أدى إلى تحسين ملحوظ في الأداء، خاصة في المكونات التي تعتمد على تحديثات متكررة. كما أن الكود أصبح أكثر وضوحاً وسهولة في الصيانة، حيث لم أعد بحاجة إلى القلق بشأن إدارة الاشتراكات أو إلغاءها. لكن الأهم من ذلك، هو أن Signals جعلت من السهل جداً تتبع تدفق البيانات داخل التطبيق، مما قلل من الأخطاء وسرعة اكتشافها. نصيحتي الأخيرة لك: ابدأ بتجربة Signals في مكون صغير، وقم بقياس الأداء قبل وبعد، ثم قرر ما إذا كان الانتقال يستحق الجهد أم لا. لكن ثق بي، بمجرد أن تتعود على Signals، لن ترغب في العودة إلى الطريقة القديمة أبداً.
إذا كنت مستعداً للغوص في عالم Angular Signals، فإليك خطة عمل عملية لتبدأ بها اليوم: أولاً، قم بإنشاء مكون جديد في مشروعك الحالي واستخدم Signals لإدارة حالته بدلاً من الـ @Input و @Output. قم بقياس الأداء باستخدام أدوات مثل Chrome DevTools، وتحقق من الفرق في وقت تنفيذ Change Detection. ثانياً، ابدأ بتحويل بعض الـ Observables إلى Signals باستخدام الدالة toSignal، لكن تأكد من إدارة الاشتراكات بشكل صحيح لتجنب الـ Memory Leaks. وأخيراً، قم بمراجعة الكود الخاص بك وابحث عن الأماكن التي يمكن فيها استخدام computed signals لتبسيط المنطق المعقد. تذكر أن Angular Signals ليست مجرد ميزة عابرة، بل مستقبل إدارة الحالة في الإطار، وكلما بدأت في استخدامها مبكراً، كلما كنت مستعداً بشكل أفضل للتطورات القادمة.