• בלוג
  • פיתוח מנגנון אירועים פשוט ב JavaScript

פיתוח מנגנון אירועים פשוט ב JavaScript

07/05/2020

במסגרת תוכניתנו "בואו נבנה אחד כדי ללמוד איך זה עובד", אני יודע שיש המון סיפריות שמנהלות העברה של אירועים בין חלקים במערכת, אבל פיתוח מנגנון כזה ב JavaScript זה תהליך די פשוט שכולל 2-3 תבניות מעניינות. מוכנים? הנה יוצאים לדרך.

1. מה אנחנו רוצים לבנות

אני רוצה לבנות מחלקה שנקראת EventBus, כאשר כל אוביקט ממנה הוא מעין מתג של אירועים. אוביקטים יכולים להירשם כדי לקבל עדכון כשיש אירוע, או לדווח על אירוע. המתג מעביר את הדיווח לכל מי שמאזין לאירוע, וכמובן שכמו שנרשמו לאירוע אפשר גם לבטל רישום.

דוגמת שימוש קטנה עשויה להיראות כך:

const bus = new EventBus();
const t = bus.subscribe('boom', function(x) { console.log('Boom!', x)});
const u = bus.subscribe('boom', function(x) { console.log('Boom!', x * x )});
bus.emit('boom', 10)
// Prints: Boom! 10 
// And then prints: Boom! 100

u()
bus.emit('boom', 10)
// Prints: Boom! 10

המנגנון כולל שתי פונקציות: subscribe ו emit. כן, אני יודע שהרבה ספריות יתנו לכם גם מנגנון לרישום חד-פעמי ועוד פינוקים, אבל את זה תוכלו לממש לבד אחרי שנסיים את הפוסט. בחזרה לעניינינו אפשר גם לראות מהדוגמא תבנית מאוד נחמדה של ביטול רישום. פונקציית ה subscribe מחזירה פונקציית חדשה, נקרא לה unsubscribe, שמבטלת את הרישום ש subscribe ביצעה.

דוגמת הקוד בעצם רושמת שתי פונקציות טיפול שונות באירוע שנקרא boom ואז שולחת את האירוע. כמובן שהתוצאה היא הפעלת שתי הפונקציות. בשלב שני אנחנו מסירים את אחד המאזינים ושולחים שוב את האירוע, הפעם רק פונקציית טיפול אחת מופעלת (זו שלא הסרנו).

2. איך מנהלים את המידע

שלב ראשון הוא כתיבת העטיפה למחלקת EventBus והחלטה על שדות המידע. בשביל EventBus נוכל להסתפק בשדה מידע אחד מסוג אוביקט. המפתחות הם שמות האירועים וכל ערך הוא רשימה של פונקציות טיפול. הפונקציה subscribe תוסיף עוד פונקציית טיפול לרשימה המתאימה, והפונקציה emit תפעיל את כל פונקציות הטיפול.

אני מוותר על שמירת משתנה this כחלק מהמידע שאני מקבל ברישום, כך שכל מי שצריך לעשות bind יפעיל את ה bind מבחוץ לפני קריאה ל subscribe. כך אני לא צריך לשמור את משתני ה this אצלי והם בעצם נשמרים כחלק מפונקציות הטיפול. ידוע, לא הכי יעיל, אבל מספיק טוב בשביל התחלה וייתן לכם אפשרות לתרגיל אופטימיזציה נחמד אם תחליטו להוסיף את המנגנון.

קוד המחלקה עם ה constructor שיוצר את אוביקט המידע נראה לכן כך:

class EventBus {
  constructor() {
    this.listeners = {};
  }
}

3. רישום קוד טיפול באירוע

רוב הקוד של פונקציית הרישום הוא די פשוט: בודקים אם יש כבר רשימת מאזינים באירוע שקיבלנו, אם אין יוצרים רשימה ריקה, ואז מוסיפים את קוד הטיפול לרשימת קודי הטיפול שלנו.

החלק היחיד שיכול להיות מבלבל בפונקציה הוא מנגנון ביטול רישום. כדי לבטל רישום נרצה להחזיר פונקציית ביטול. קל להשתמש בפונקציית חץ וב filter כדי לממש את פונקציית ביטול הרישום.

מימוש הקוד ל subscribe לכן נראה כך:

class EventBus {
  constructor() {
    this.listeners = {};
  }

  subscribe(eventName, handler) {
    if (!this.listeners[eventName]) {
      this.listeners[eventName] = [];
    }

    this.listeners[eventName].push(handler);
    return () => {
      this.listeners[eventName] = this.listeners[eventName].filter(fn => fn !== handler);
    }
  }
}

4. שליחת אירוע

המנגנון האחרון הוא הפונקציה emit ששולחת אירוע לכל המאזינים. הטריק במימוש מנגנון זה הוא שאנחנו לא יודעים כמה פרמטרים כל אירוע כולל, ולכן נשתמש באופרטור שלוש נקודות כדי לקחת כל הפרמטרים ש emit קיבלה ולהעביר אותם כמו שהם לכל אחת מפונקציות הטיפול. הקוד:

class EventBus {
  constructor() {
    this.listeners = {};
  }

  subscribe(eventName, handler) {
    if (!this.listeners[eventName]) {
      this.listeners[eventName] = [];
    }

    this.listeners[eventName].push(handler);
    return () => {
      this.listeners[eventName] = this.listeners[eventName].filter(fn => fn !== handler);
    }
  }

  emit(eventName, ...args) {
    const listeners = this.listeners[eventName] || [];
    for (let fn of listeners) {
      fn(...args);
    }
  }
}

מה עוד אפשר להוסיף? לא מעט:

  1. לא טיפלתי ב Exceptions. בספריה אמיתית זה יכול להיות רלוונטי.

  2. אין מנגנון להוסיף Event Listener בהתחלה או בסוף של הרשימה, רק בסוף.

  3. אין מנגנון בו Event Listener אחד יכול לעצור את הטיפול (בדומה ל stopPropagation).

  4. אין אפשרות לשמור את ה this של מחלקה חיצונית פעם אחת, ולהעביר אותו למספר פונקציות טיפול.

  5. אין פינוקים כמו once או removeAllListeners.

ובכל זאת הלב של טיפול באירועים ושמירת רשימות של מטפלים קיים כאן, וכמובן התבנית של חיבור וניתוק קודי טיפול מאירוע, והכל בסך הכל בפחות מ 25 שורות כך שאני מרוצה.