اكتشف كيف تغير Angular Signals قواعد اللعبة في إدارة الحالة، مع أمثلة عملية تكشف أسرار الأداء والذاكرة خلف الكواليس، وتجنب الفخاخ التي يقع فيها حتى المطورون المحترفون.
تخيل أنك تعمل على تطبيق Angular ضخم، وكلما أضفت مكوناً جديداً، يتباطأ الأداء بشكل ملحوظ. تفتح أدوات المطور لترى عشرات من الـ Change Detection Cycles تعمل بلا داعٍ، وكل مرة تمر فيها بيانات عبر @Input() أو Service، ينطلق تحديث كامل للشجرة. المشكلة ليست في الكود الخاص بك، بل في الآلية القديمة التي تعتمد عليها Angular منذ سنوات. هنا تأتي Angular Signals كحل ثوري، لكنها ليست مجرد إضافة جديدة - إنها إعادة تفكير كاملة في كيفية إدارة الحالة والتحديثات في التطبيقات الحديثة.
في عام 2023، أعلنت Google عن Angular Signals كجزء من الإصدار 16، لكن الكثير من المطورين تعاملوا معها كإضافة اختيارية. الحقيقة هي أن Signals ليست مجرد ميزة جديدة، بل هي الأساس الذي ستبنى عليه Angular في المستقبل. إذا كنت لا تزال تستخدم RxJS و@Input() بالطريقة التقليدية، فأنت تخسر فرصاً هائلة لتحسين الأداء وتقليل التعقيد في تطبيقك. دعنا نكسر الحاجز بين النظرية والتطبيق ونرى كيف تعمل Signals خلف الكواليس.
في قلب Angular Signals يوجد مفهوم الـ Fine-Grained Reactivity، وهو نظام يسمح للتطبيق بتحديث الأجزاء الدقيقة التي تحتاج للتغيير فقط، بدلاً من إعادة حساب الشجرة بأكملها. فكر في الأمر كجهاز عصبي دقيق في جسم الإنسان - عندما تلمس شيئاً ساخناً، لا يرسل الدماغ إشارة لكل خلية في جسدك، بل يرسلها فقط للعضلات التي تحتاج للتحرك. هذا بالضبط ما تفعله Signals: تحدد بالضبط أي جزء من الـ DOM يحتاج للتحديث، وتترك الباقي كما هو.
لفهم الفرق الحقيقي، دعنا نقارن بين الطريقة التقليدية باستخدام RxJS والطريقة الجديدة مع Signals. في التطبيقات الكبيرة، غالباً ما تجد نفسك تكتب أكواداً مثل هذا:
// الطريقة التقليدية باستخدام BehaviorSubject
class UserService {
private _user = new BehaviorSubject<User | null>(null);
user$ = this._user.asObservable();
updateUser(user: User) {
this._user.next(user);
}
}
// في المكون
this.userService.user$.subscribe(user => {
this.user = user; // سيؤدي إلى Change Detection كامل
});المشكلة هنا أن كل تحديث لـ BehaviorSubject سيؤدي إلى تشغيل Change Detection على المكون بأكمله، حتى لو كان التغيير يؤثر على جزء صغير من الـ UI. مع Signals، يصبح الكود هكذا:
// باستخدام Signals
class UserService {
user = signal<User | null>(null);
updateUser(user: User) {
this.user.set(user); // سيحدث تحديثاً دقيقاً فقط
}
}
// في المكون
user = this.userService.user; // لا حاجة للـ subscribe
// في القالب: {{ user().name }}الفرق الأساسي هنا هو أن Signal تحتفظ بقائمة من الـ Consumers الذين يعتمدون عليها، وعندما تتغير قيمتها، تُعلم فقط هؤلاء المستهلكين بالتغيير. هذا يعني أن Angular يمكنها الآن تتبع الاعتمادات بين المكونات بطريقة أكثر دقة، وتجنب تشغيل Change Detection على المكونات التي لا تتأثر بالتغيير. في تطبيق حقيقي، هذا يمكن أن يقلل عدد الـ Change Detection Cycles بنسبة تصل إلى 70%، خاصة في التطبيقات الكبيرة التي تحتوي على عشرات المكونات المتداخلة.
لفهم القوة الحقيقية لـ Signals، علينا الغوص في تفاصيل كيفية عملها على مستوى الذاكرة والمعالج. عندما تنشئ Signal باستخدام signal()، فإنك في الواقع تنشئ كائناً يحتوي على ثلاث خصائص رئيسية: القيمة الحالية، قائمة المستهلكين (الذين يعتمدون على هذه Signal)، وقائمة الإشارات التي تعتمد عليها هذه Signal نفسها. هذا الهيكل يسمح لـ Angular ببناء ما يشبه الرسم البياني للاعتمادات بين الإشارات المختلفة، وهو ما يسمى بـ Reactivity Graph.
عندما تتغير قيمة Signal، تحدث سلسلة من الأحداث خلف الكواليس:
المثير هنا هو أن هذا النظام يعمل بشكل متزامن مع الـ Zone.js، لكنه لا يعتمد عليه بشكل كامل. في الواقع، Signals تسمح لـ Angular بالابتعاد تدريجياً عن Zone.js، وهو ما قد يؤدي في المستقبل إلى إلغاء الحاجة إليه تماماً. هذا يعني تقليل حجم الحزمة النهائية للتطبيق، وتحسين الأداء بشكل ملحوظ، خاصة على الأجهزة ذات الموارد المحدودة.
دعنا نرى مثالاً عملياً يوضح كيفية بناء Reactivity Graph مع Signals:
@Component({
selector: 'app-dashboard',
template: `
<div>Total: {{ total() }}</div>
<div>Tax: {{ tax() }}</div>
<div>Grand Total: {{ grandTotal() }}</div>
`,
})
export class DashboardComponent {
price = signal(100);
quantity = signal(2);
// computed signal تعتمد على إشارتين
total = computed(() => this.price() * this.quantity());
// computed signal تعتمد على computed أخرى
tax = computed(() => this.total() * 0.15);
grandTotal = computed(() => this.total() + this.tax());
updatePrice(newPrice: number) {
this.price.set(newPrice); // سيؤدي إلى تحديث total و tax و grandTotal تلقائياً
}
}في هذا المثال، عندما تتغير قيمة price، تعرف Angular تلقائياً أن total تحتاج للتحديث، وبما أن tax تعتمد على total، فإنها تحتاج للتحديث أيضاً، وهكذا. لكن الأهم هو أن Angular تعرف أيضاً أن المكونات الأخرى التي قد تستخدم price مباشرة لن تتأثر بهذا التغيير، مما يقلل من عدد التحديثات غير الضرورية. هذا النوع من الدقة هو ما يجعل Signals مختلفة تماماً عن الأنظمة التقليدية لإدارة الحالة.
الكثير من المطورين يسألون: هل Signals ستحل محل RxJS تماماً؟ الإجابة القصيرة هي لا، لكن الإجابة الأعمق هي أنها ستغير الطريقة التي نستخدم بها RxJS. في الواقع، Signals وRxJS مصممان لحل مشاكل مختلفة، ويمكنهما العمل معاً بفعالية. فكر في RxJS كطريقة للتعامل مع الـ Asynchronous Data Streams، بينما Signals هي طريقة لإدارة الـ Synchronous State مع تحديثات دقيقة للـ UI.
في التطبيقات الحقيقية، ستجد نفسك تستخدم كلاهما معاً. على سبيل المثال، قد تستخدم RxJS للتعامل مع البيانات القادمة من API أو الـ WebSocket، ثم تحول هذه البيانات إلى Signals لتستخدمها في مكوناتك. إليك مثال عملي:
@Injectable({ providedIn: 'root' })
export class ProductService {
private products$ = this.http.get<Product[]>('/api/products').pipe(
shareReplay(1)
);
// تحويل Observable إلى Signal
products = toSignal(this.products$, { initialValue: [] });
constructor(private http: HttpClient) {}
}
@Component({
selector: 'app-products',
template: `
<div *ngFor="let product of products()">
{{ product.name }} - {{ product.price | currency }}
</div>
`,
})
export class ProductsComponent {
products = inject(ProductService).products;
}في هذا المثال، نستخدم RxJS للتعامل مع الـ HTTP Request، ثم نحول الـ Observable إلى Signal باستخدام toSignal. هذا يسمح لنا بالاستفادة من قوة RxJS في التعامل مع البيانات المتدفقة، وفي نفس الوقت نستفيد من مزايا Signals في إدارة الحالة داخل المكونات. لكن هناك نقطة مهمة يجب الانتباه إليها: عندما تستخدم toSignal، فإنك تفقد بعض مزايا RxJS مثل القدرة على إلغاء الـ Subscription تلقائياً عند تدمير المكون. لهذا السبب، من المهم فهم متى تستخدم كل أداة.
في رأيي الشخصي، هناك بعض السيناريوهات التي يجب فيها تفضيل RxJS على Signals:
من تجربتي في العمل على تطبيقات كبيرة، وجدت أن الجمع بين Signals وRxJS يمكن أن يكون قوياً جداً، لكنه يتطلب فهماً عميقاً لكلا الأداتين. على سبيل المثال، في أحد المشاريع، استخدمنا RxJS للتعامل مع الـ Real-Time Data القادم من WebSocket، ثم حولنا هذه البيانات إلى Signals لتستخدمها المكونات. هذا سمح لنا بالاستفادة من قوة RxJS في التعامل مع البيانات المتدفقة، وفي نفس الوقت استفدنا من الأداء العالي لـ Signals في تحديث الـ UI.
على الرغم من أن Signals تقدم حلولاً رائعة، إلا أنها تأتي مع مجموعة من التحديات التي قد لا تكون واضحة في البداية. أحد أكبر المشاكل التي واجهتها في المشاريع الحقيقية هو ما يسمى بـ "Signal Loop"، وهو موقف يحدث عندما يكون لديك إشارتان تعتمد كل منهما على الأخرى، مما يؤدي إلى حلقة لا نهائية من التحديثات. على سبيل المثال:
a = signal(1);
b = computed(() => this.a() + 1);
this.a.set(this.b()); // سيؤدي إلى حلقة لا نهائيةفي هذا المثال، عندما نضع قيمة b في a، ستؤدي هذه العملية إلى تحديث b مرة أخرى، مما يؤدي إلى تحديث a وهكذا. لحسن الحظ، Angular ستكتشف هذه الحلقة تلقائياً وتوقفها بعد عدد معين من التكرارات، لكنها ستظهر خطأ في الـ Console. الحل هنا هو إعادة التفكير في تصميم الحالة لتجنب هذه الاعتمادات الدائرية.
مشكلة أخرى شائعة هي ما أسميه "Signal Leak"، وهي تحدث عندما تحتفظ بمراجع لإشارات لا تحتاجها بعد الآن، مما يؤدي إلى تسرب الذاكرة. على سبيل المثال، في مكون يستخدم computed signal يعتمد على إشارة من خدمة، إذا لم يتم تدمير المكون بشكل صحيح، فقد تظل الإشارة قائمة في الذاكرة. لحل هذه المشكلة، من المهم التأكد من تدمير المكونات بشكل صحيح، واستخدام أدوات مثل Chrome DevTools لفحص الذاكرة والتأكد من عدم وجود تسريبات.
هناك أيضاً مشكلة الأداء عند استخدام Signals مع القوائم الكبيرة. على سبيل المثال، إذا كان لديك قائمة تحتوي على آلاف العناصر، واستخدمت computed signal لحساب شيء مثل المجموع الكلي، فقد يؤدي هذا إلى تحديثات بطيئة إذا لم يتم تحسينها بشكل صحيح. الحل هنا هو استخدام تقنيات مثل الـ Memoization أو تقسيم القائمة إلى أجزاء أصغر باستخدام virtual scrolling. إليك مثال على كيفية تحسين أداء القوائم الكبيرة:
@Component({
selector: 'app-large-list',
template: `
<div *ngFor="let item of visibleItems()">
{{ item.name }} - {{ item.value }}
</div>
`,
})
export class LargeListComponent {
allItems = signal<Item[]>([]);
scrollPosition = signal(0);
// computed signal تحسب العناصر المرئية فقط
visibleItems = computed(() => {
const start = this.scrollPosition();
const end = start + 20; // عرض 20 عنصراً فقط
return this.allItems().slice(start, end);
});
loadItems() {
// تحميل البيانات من API مثلاً
this.allItems.set(generateLargeList(10000));
}
}أخيراً، هناك مشكلة التوافق مع المكتبات الخارجية. بعض المكتبات قد لا تعمل بشكل جيد مع Signals، خاصة تلك التي تعتمد على الـ Change Detection التقليدي. على سبيل المثال، إذا كنت تستخدم مكتبة مثل NgRx، فقد تحتاج إلى تحويل الـ Signals إلى Observables باستخدام toObservable قبل استخدامها مع المكتبة. دائماً تحقق من توثيق المكتبة لمعرفة كيفية التعامل مع Signals.
إذا نظرت إلى خارطة طريق Angular الرسمية، ستجد أن Signals ليست مجرد ميزة عابرة، بل هي الأساس الذي ستبنى عليه الإصدارات المستقبلية. في الإصدار 17، رأينا بداية هذا التحول مع تقديم الـ Deferrable Views، وهي ميزة تسمح بتحميل المكونات بشكل كسول بناءً على شروط معينة، وتعتمد بشكل كبير على Signals في تتبع هذه الشروط. هذا مجرد بداية لما يمكن أن تقدمه Signals في المستقبل.
أحد أكثر التطورات إثارة التي نتوقعها هو الانتقال التدريجي بعيداً عن Zone.js. حالياً، تعتمد Angular بشكل كبير على Zone.js لتشغيل Change Detection، لكن هذا يأتي بتكلفة أداء كبيرة، خاصة في التطبيقات الكبيرة. مع Signals، يمكن لـ Angular تقليل الاعتماد على Zone.js بشكل كبير، مما قد يؤدي إلى تحسينات ملحوظة في الأداء وتقليل حجم الحزمة النهائية للتطبيق. في الواقع، هناك بالفعل تجارب جارية لتوفير وضع "Zone-less" في Angular، والذي سيعتمد بالكامل على Signals لإدارة التحديثات.
هناك أيضاً حديث عن تقديم ما يسمى بـ "Signal Components"، وهي مكونات جديدة تعتمد بالكامل على Signals لإدارة الحالة والتحديثات، بدلاً من الاعتماد على الـ Change Detection التقليدي. هذا النوع من المكونات قد يوفر أداءً أفضل بكثير، خاصة في التطبيقات التي تحتوي على الكثير من التحديثات الديناميكية. تخيل مكوناً لا يحتاج إلى إعادة حساب الـ Template بالكامل عند تغيير قيمة واحدة، بل يقوم بتحديث الجزء المتأثر فقط - هذا بالضبط ما يمكن أن تقدمه Signal Components.
لكن ربما أكثر ما يثير الحماس هو إمكانية دمج Signals مع أدوات إدارة الحالة الخارجية مثل NgRx. حالياً، NgRx يعتمد على RxJS بشكل كامل، لكن هناك خطط لتقديم دعم لـ Signals في الإصدارات المستقبلية. هذا قد يفتح الباب أمام نمط جديد من إدارة الحالة يجمع بين قوة NgRx في التعامل مع البيانات المعقدة، ومرونة Signals في تحديث الـ UI. في رأيي، هذا الدمج قد يكون الخطوة التالية الكبيرة في تطور إدارة الحالة في تطبيقات الويب.
من تجربتي الشخصية، أرى أن Signals ليست مجرد ميزة جديدة، بل هي تحول في طريقة تفكيرنا في بناء تطبيقات Angular. إذا كنت تعمل على مشروع جديد، فإنني أنصحك بشدة بالبدء في استخدام Signals منذ البداية، حتى لو كان ذلك يعني إعادة كتابة بعض الأجزاء لاحقاً. الفوائد التي ستحصل عليها من حيث الأداء وسهولة الصيانة تستحق الجهد المبذول. وإذا كنت تعمل على مشروع قائم، فابدأ تدريجياً بإدخال Signals في الأجزاء الجديدة من التطبيق، وراقب تأثيرها على الأداء والاستقرار.
إذا كنت تريد البدء في استخدام Angular Signals اليوم، فإليك نصيحتي العملية: ابدأ بتحويل الـ State البسيط في مكوناتك إلى Signals، ثم انتقل تدريجياً إلى الأجزاء الأكثر تعقيداً. استخدم computed signals لتجنب إعادة الحسابات غير الضرورية، وتجنب الاعتمادات الدائرية بين الإشارات. وإذا كنت تستخدم RxJS، فابدأ بتحويل الـ Observables إلى Signals باستخدام toSignal، لكن تذكر أن هذا ليس مناسباً لكل الحالات - خاصة عندما تحتاج للتحكم الدقيق في الـ Subscription lifecycle.
أهم شيء يجب أن تتذكره هو أن Signals ليست مجرد بديل لـ RxJS أو لـ @Input()، بل هي أداة جديدة لحل مشاكل مختلفة. استخدمها حيث تكون مفيدة، ولا تحاول فرضها على كل جزء من تطبيقك. وفي النهاية، أفضل طريقة لتعلم Signals هي التجربة والخطأ - ابدأ بمشروع صغير، جرب أشياء مختلفة، وراقب تأثيرها على الأداء والاستقرار. Angular Signals ليست مجرد ميزة جديدة، بل هي مستقبل إدارة الحالة في تطبيقات الويب، وكلما بدأت في استخدامها مبكراً، كلما كنت مستعداً بشكل أفضل للمستقبل.