בעבוע אירועים ב-JS מוסבר - event bubbling ו-delegation¶
אם אי פעם הוספתם מאזין אירועים (event listener) לכפתור, ולחצתם על אייקון קטן בתוך הכפתור, ופתאום ה-event "עולה" גם לאלמנטים שמעל הכפתור בלי שביקשתם - זה בדיוק הנושא שאנחנו הולכים לפרק. זה נקרא בעבוע אירועים, ולהבין אותו לעומק זה ההבדל בין קוד DOM שעובד במקרה לקוד שאתם באמת שולטים בו.
מה זה בעבוע אירועים - הרעיון הבסיסי¶
כשאתם לוחצים על אלמנט בדף, האירוע לא נשאר רק על האלמנט הזה. הוא "מבעבע" (bubbles) כלפי מעלה, דרך כל האבות שלו בעץ ה-DOM, עד שהוא מגיע לתגית body, ולמעשה עד ל-document כולו. זה אומר שאם יש לכם div שמכיל button, ואתם לוחצים על ה-button, האירוע "click" קורה קודם על הכפתור עצמו, ואז ממשיך לקרות גם על ה-div שמסביבו, ואז על מה שמסביב ל-div, וכן הלאה כלפי מעלה.
document.getElementById("outer").addEventListener("click", () => {
console.log("נלחץ החוצה");
});
document.getElementById("inner").addEventListener("click", () => {
console.log("נלחץ הכפתור");
});
אם תלחצו על הכפתור, שני ה-console.log ירוצו, בסדר הזה - קודם "נלחץ הכפתור", ואז "נלחץ החוצה". זה בגלל הבעבוע - האירוע קורה קודם על האלמנט הכי פנימי, ואז ממשיך "כלפי מעלה".
למה זה קורה בכלל¶
זה לא באג, זו תכונה מכוונת של הדפדפן, שנועדה לתת גמישות. במקום שתצטרכו להוסיף מאזין אירועים לכל אלמנט קטן בנפרד, אתם יכולים להוסיף מאזין אחד לאלמנט אב, ולתפוס שם את כל האירועים שקורים על הילדים שלו. זה בדיוק מוביל לטכניקה שנקראת event delegation.
Event Delegation - הטכניקה שכל מפתח פרונטאנד צריך להכיר¶
תארו לעצמכם רשימה של מאה פריטים, כל אחד עם כפתור מחיקה משלו. הגישה הנאיבית היא להוסיף מאזין אירועים נפרד לכל אחד ממאה הכפתורים. זה עובד, אבל זה מבזבז זיכרון, ובעיקר - אם מוסיפים פריט חדש דינמית, צריך לזכור להוסיף מאזין גם לכפתור החדש שלו.
הפתרון האלגנטי מנצל בעבוע - שמים מאזין אירועים אחד בלבד על האלמנט האב שמכיל את כל הרשימה, ובודקים בתוך הפונקציה על מה בדיוק נלחץ:
document.getElementById("list").addEventListener("click", (event) => {
if (event.target.matches(".delete-button")) {
const item = event.target.closest(".list-item");
item.remove();
}
});
מאזין אחד בלבד, שמטפל בכל הכפתורים, כולל כאלה שעוד לא קיימים ויתווספו בעתיד. זה נקי יותר, יעיל יותר בזיכרון, ופותר אוטומטית את הבעיה של פריטים שנוספים דינמית.
איך עוצרים בעבוע כשלא רוצים אותו¶
לפעמים אתם כן רוצים למנוע מהאירוע להמשיך "לבעבע" כלפי מעלה. לדוגמה, אם יש לכם תפריט נפתח (dropdown) שאמור להיסגר בכל לחיצה מחוץ לו, אבל לא כשלוחצים בתוכו:
הפונקציה stopPropagation עוצרת את האירוע במקום, ולא נותנת לו להמשיך "לעלות" לאלמנטים שמעל. חשוב להשתמש בזה בזהירות - אם עוצרים בעבוע יותר מדי, עלולים לשבור התנהגויות אחרות שסומכות עליו, כולל event delegation שכתבתם במקום אחר בקוד.
איך זה קשור לריאקט¶
חשוב לדעת - ריאקט משתמש בגרסה משלו של מערכת אירועים שמבוססת בדיוק על אותו עיקרון של בעבוע, רק שהיא מנוהלת ברמה גבוהה יותר על ידי ריאקט עצמו, לא ישירות על ידי הדפדפן. זו אחת הסיבות שמומלץ להבין את הבעבוע ברמת ה-DOM הרגיל לפני שמגיעים לריאקט - כדי להבין שהמנגנון שמתחת למכסה המנוע דומה, גם אם הממשק שריאקט נותן לכם קצת שונה.
למה חשוב להכיר את זה¶
הבנה של בעבוע אירועים היא בדיוק סוג הידע ש"נראה תיאורטי" עד שנתקעים בבאג אמיתי שקשור בדיוק לזה - כפתור שגורם לפעולה כפולה, לחיצה שגם סוגרת וגם פותחת תפריט, או קוד שלא מטפל נכון באלמנטים שנוספו דינמית. מתכנתים שמבינים את המנגנון פותרים באגים כאלה תוך דקות. מתכנתים שלא, מנסים "תיקונים" אקראיים במשך שעות.
בקורס צד לקוח יש פרק שלם על DOM ואירועים שמסביר את זה עם דוגמאות ותרגילים אמיתיים, בדיוק כדי שהמנגנון הזה יתיישב אצלכם פעם אחת וטוב.
לסיכום¶
בעבוע אירועים זה מנגנון שבו אירוע DOM "עולה" מהאלמנט שבו הוא קרה, כלפי מעלה, דרך כל האבות שלו. זה מאפשר טכניקה שימושית מאוד שנקראת event delegation, שבה מאזין אחד על אלמנט אב מטפל בכל הילדים שלו, כולל כאלה שעוד לא נוצרו. להבין את זה לעומק חוסך לכם המון כאב ראש בהמשך.
יש לכם באג שקשור לאירועים ואתם לא מבינים למה הוא קורה? תשתפו בדיסקורד.