| طريقة التعامل مع الحدث (`Approach`) | آلية التنفيذ والإدارة | المميزات والعيوب الهندسية |
|---|---|---|
| مستمعات DOM التقليدية (`addEventListener`) | ربط مباشر بعنصر الشاشة وتحديث المتغيرات يدوياً | تتطلب إزالة يدوية دقيقة عبر `removeEventListener` لتجنب تسريب الذاكرة |
| تدفقات RxJS عبر `fromEvent` | تحويل الأحداث المستمرة إلى Observable تفاعلي متكامل | تتيح الدمج الفوري مع مشغلات المعالجة مثل `debounceTime` و `switchMap` وإلغاء الاشتراك الآمن |
يُعتبر التعامل مع تفاعلات المستخدمين وأحداث المتصفح (مثل النقر، الكتابة، أو تحريك الفأرة) أحد أهم ركائز البرمجة التفاعلية. بدلاً من استخدام مستمعات الأحداث التقليدية التي قد تسبب تسريبات في الذاكرة، توفر RxJS دالة الإنشاء fromEvent لتحويل أي حدث DOM مباشر إلى تدفق بيانات مستمر (Continuous Stream) يمكن التحكم به ومعالجته بكل سلاسة.
أحداث DOM مثل النقرات أو الكتابة لا تنتهي تلقائياً (Never Complete)؛ ولذلك فإن إدارتها تتطلب التخلص من الاشتراك (Unsubscribe) عند تدمير المكون (OnDestroy) لمنع تراكم العمليات في الذاكرة.
من خلال تحويل الحدث إلى تدفق عبر fromEvent، يمكنك تطبيق تقنيات تحسين الأداء مثل تأخير الاستجابة عند الكتابة (debounceTime) أو منع التكرار (distinctUntilChanged) بكل سهولة داخل الـ pipe.
| 1 | import { Component, OnInit, OnDestroy, ElementRef, ViewChild, AfterViewInit } from '@angular/core'; |
| 2 | import { CommonModule } from '@angular/common'; |
| 3 | import { fromEvent, Subscription } from 'rxjs'; |
| 4 | |
| 5 | @Component({ |
| 6 | selector: 'app-dom-click-stream', |
| 7 | standalone: true, |
| 8 | imports: [CommonModule], |
| 9 | template: ` |
| 10 | <div class="dom-demo-card"> |
| 11 | <h3>التعامل مع أحداث النقر عبر fromEvent</h3> |
| 12 | <button #actionBtn class="action-button">اضغط هنا لتوليد حدث تدفقي</button> |
| 13 | <p><strong>عدد مرات النقر المكتشفة:</strong> {{ clickCount }}</p> |
| 14 | </div> |
| 15 | `, |
| 16 | styles: [` |
| 17 | .dom-demo-card { background: #090d16; color: #fff; padding: 24px; border-radius: 12px; border: 1px solid #1e293b; margin-bottom: 16px; } |
| 18 | .action-button { background: #38bdf8; color: #090d16; border: none; padding: 10px 20px; font-weight: bold; border-radius: 6px; cursor: pointer; } |
| 19 | .action-button:hover { background: #0284c7; color: #fff; } |
| 20 | p { color: #94a3b8; margin-top: 12px; } |
| 21 | strong { color: #10b981; } |
| 22 | `] |
| 23 | }) |
| 24 | export class DomClickStreamComponent implements AfterViewInit, OnDestroy { |
| 25 | clickCount: number = 0; |
| 26 | private clickSub!: Subscription; |
| 27 | |
| 28 | @ViewChild('actionBtn') actionBtn!: ElementRef; |
| 29 | |
| 30 | ngAfterViewInit(): void { |
| 31 | // تحويل حدث النقر على عنصر DOM إلى تدفق تفاعلي مستمر |
| 32 | this.clickSub = fromEvent(this.actionBtn.nativeElement, 'click').subscribe({ |
| 33 | next: (event: any) => { |
| 34 | this.clickCount++; |
| 35 | console.log('تم رصد حدث النقر بنجاح:', event); |
| 36 | } |
| 37 | }); |
| 38 | } |
| 39 | |
| 40 | ngOnDestroy(): void { |
| 41 | // إيقاف الاستماع للتدفق لمنع تسريب الذاكرة عند مغادرة المكون |
| 42 | if (this.clickSub) { |
| 43 | this.clickSub.unsubscribe(); |
| 44 | } |
| 45 | } |
| 46 | } |
| 1 | import { Component, OnInit, OnDestroy, ElementRef, ViewChild, AfterViewInit } from '@angular/core'; |
| 2 | import { CommonModule } from '@angular/common'; |
| 3 | import { fromEvent, Subscription } from 'rxjs'; |
| 4 | import { map, debounceTime, distinctUntilChanged } from 'rxjs/operators'; |
| 5 | |
| 6 | @Component({ |
| 7 | selector: 'app-dom-input-stream', |
| 8 | standalone: true, |
| 9 | imports: [CommonModule], |
| 10 | template: ` |
| 11 | <div class="dom-demo-card"> |
| 12 | <h3>مراقبة مدخلات حقل البحث بشكل حي</h3> |
| 13 | <input #searchInput type="text" placeholder="اكتب للبحث الفوري..." class="search-input" /> |
| 14 | <p><strong>النص المُعالج بعد التحسين:</strong> {{ typedValue }}</p> |
| 15 | </div> |
| 16 | `, |
| 17 | styles: [` |
| 18 | .dom-demo-card { background: #090d16; color: #fff; padding: 24px; border-radius: 12px; border: 1px solid #1e293b; } |
| 19 | .search-input { width: 100%; padding: 10px; background: #1e293b; border: 1px solid #334155; color: #fff; border-radius: 6px; margin-bottom: 12px; box-sizing: border-box; } |
| 20 | p { color: #94a3b8; } |
| 21 | strong { color: #38bdf8; } |
| 22 | `] |
| 23 | }) |
| 24 | export class DomInputStreamComponent implements AfterViewInit, OnDestroy { |
| 25 | typedValue: string = ''; |
| 26 | private inputSub!: Subscription; |
| 27 | |
| 28 | @ViewChild('searchInput') searchInput!: ElementRef; |
| 29 | |
| 30 | ngAfterViewInit(): void { |
| 31 | // إنشاء تدفق من حقل الإدخال وتحسينه عبر المشغلات |
| 32 | this.inputSub = fromEvent(this.searchInput.nativeElement, 'input').pipe( |
| 33 | map((event: any) => event.target.value), // استخراج القيمة النصية مباشرة |
| 34 | debounceTime(300), // تأخير الاستجابة 300 ميلي ثانية لتقليل الضغط |
| 35 | distinctUntilChanged() // تجاهل القيم المتكررة المتطابقة |
| 36 | ).subscribe({ |
| 37 | next: (value) => { |
| 38 | this.typedValue = value; |
| 39 | console.log('قيمة البحث المعالجة:', value); |
| 40 | } |
| 41 | }); |
| 42 | } |
| 43 | |
| 44 | ngOnDestroy(): void { |
| 45 | if (this.inputSub) { |
| 46 | this.inputSub.unsubscribe(); |
| 47 | } |
| 48 | } |
| 49 | } |