Event Loop ב-JavaScript לנוער: למה setTimeout(0) מחכה בתור?
שלוש פקודות נכנסות לקונסול: אחת רגילה, אחת בתוך Timer ואחת בתוך Promise. כולן קצרות, אבל הן אינן מסיימות לפי סדר הכתיבה. במקום לשנן תשובה, נבנה מודל שמאפשר לחזות מי רץ עכשיו ומי מחכה בתור.
console.log("first")
setTimeout(() => {
console.log("timer")
}, 0)
console.log("last")first
last
timer
Timer מוכן אינו Timer שרץ
הערך 0 אינו הוראה לדלג על שאר הקוד. הוא אומר לדפדפן שלא נדרשת השהיה מכוונת ארוכה יותר לפני שה-callback יכול להיות מועמד להרצה. אם המחסנית עסוקה, הוא מחכה.
בחומרי ה-JavaScript שנמסרו למכללה מופיעים פונקציות, קריאת קוד, קונסולת הדפדפן ופרויקטים אינטראקטיביים. המאמר משתמש ביסודות האלה כדי להסביר התנהגות יציבה של סביבת JavaScript בדפדפן; הוא אינו התחייבות לתוכן או לכלי של מחזור לימודים מסוים.
כלל ראשון למעבדה
לפני כל הרצה כותבים תחזית. רק אחר כך פותחים את הקונסול ומשווים בין המודל לבין התוצאה.
ארבע תחנות, לא ״קסם אסינכרוני״
הביטוי Event Loop מתאר את הבדיקה החוזרת שמחברת בין הקוד שרץ כעת לבין העבודה שמוכנה להמשיך. כדי לעקוב אחרי הסדר, מספיק לדעת באיזו תחנה נמצאת כל פעולה.
תחנה 1
הקוד הסינכרוני
Call Stack
JavaScript מריץ פקודה אחת בכל פעם. פונקציה שנקראה נכנסת למחסנית ויוצאת ממנה כשהיא מסיימת.
תחנה 2
הדפדפן מטפל בהמתנה
Web APIs
Timer, אירוע לחיצה או בקשת רשת יכולים להמתין מחוץ למחסנית. ההמתנה אינה עוצרת את שאר הקוד.
תחנה 3
עבודות דחופות אחרי הקוד
Microtask Queue
תגובות של Promise נכנסות לתור המיקרו־משימות. הן מטופלות אחרי שהמחסנית מתרוקנת ולפני המשימה הבאה.
תחנה 4
המשימה הבאה
Task Queue
Callback של Timer מוכן ממתין כאן. ה-Event Loop מעביר אותו למחסנית רק כשהיא פנויה ואחרי המיקרו־משימות.
תחזית, הרצה, הסבר — בכל סבב
כל ניסוי מוסיף כלל אחד. הדוגמאות קצרות בכוונה: קל לראות את הסדר, לשנות שורה אחת ולבדוק אם ההסבר עדיין מחזיק.
console.log("A")
setTimeout(() => {
console.log("B")
}, 0)
console.log("C")אפס מילישניות עדיין אומר ״אחר כך״
באיזה סדר יודפסו A, B ו-C?
התוצאה
A → C → B
A ו-C הם קוד סינכרוני ולכן הם מסיימים קודם. ה-Timer יכול להיות מוכן כמעט מיד, אך ה-callback שלו עדיין ממתין לתורו.
console.log("A")
setTimeout(() => console.log("B"), 0)
Promise.resolve().then(() => {
console.log("C")
})
console.log("D")Promise נכנס למסלול קצר יותר
מי יודפס קודם: B או C?
התוצאה
A → D → C → B
אחרי A ו-D המחסנית ריקה. כעת JavaScript מרוקן את תור המיקרו־משימות, ולכן C של ה-Promise מופיע לפני B של ה-Timer.
setTimeout(() => {
console.log("timer 1")
Promise.resolve().then(() => {
console.log("microtask")
})
}, 0)
setTimeout(() => {
console.log("timer 2")
}, 0)גם בין שתי משימות בודקים Microtasks
האם timer 2 עוקף את ה-Promise שנוצר ב-timer 1?
התוצאה
timer 1 → microtask → timer 2
המשימה הראשונה מסתיימת ואז המיקרו־משימה שנוצרה בתוכה מטופלת. רק לאחר מכן עוברים למשימת ה-Timer הבאה.
שלושה משפטים שנשמעים הגיוניים — ומטעים
במקום להחליף מיתוס בסיסמה חדשה, לכל תיקון מצורף ניסוי קטן שמראה מה באמת קובע את הסדר.
״setTimeout(fn, 0) מפעיל את הפונקציה מיד״
המספר מציין זמן המתנה מינימלי לפני שה-callback יכול להיכנס לתור. הוא אינו קובע מתי המחסנית תהיה פנויה.
הבדיקה: מוסיפים console.log מיד אחרי ה-setTimeout ובודקים מי מודפס קודם.
״Promise תמיד מהיר יותר מ-Timer״
לא מדובר במרוץ מהירות אלא בכללי תזמון שונים. callback של Promise הוא Microtask, ואילו callback של Timer הוא Task.
הבדיקה: שמים את שניהם אחרי אותו קטע סינכרוני ומשווים את סדר ההדפסה.
״Event Loop מריץ כמה שורות JavaScript במקביל״
בזרם הראשי, המחסנית מריצה יחידת קוד אחת בכל רגע. הדפדפן יכול לטפל בהמתנות מחוץ למחסנית ולהחזיר callbacks בהמשך.
הבדיקה: מפרידים על הנייר בין ״רץ עכשיו״, ״ממתין בדפדפן״ ו״מוכן בתור״.
כשהסדר מפתיע, עוברים ארבע בדיקות
- 1
מסמנים כל פעולה
ליד כל שורה כותבים: סינכרונית, Timer, אירוע, Promise או פעולה אחרת שמחזירה callback.
- 2
עוקבים אחרי המחסנית
לא קופצים ישר לתורים. קודם מסיימים את כל מה שכבר רץ, כולל פונקציות שקראו לפונקציות נוספות.
- 3
מרוקנים Microtasks
כשהמחסנית מתרוקנת, בודקים אם Promise הוסיף עבודה. Microtask יכולה ליצור Microtask נוספת באותו סבב.
- 4
לוקחים Task אחת
רק עכשיו מעבירים callback מוכן של Timer או אירוע למחסנית, ואז חוזרים שוב לאותה בדיקה.
איך נראית הבנה, לא רק תשובה נכונה?
- התלמיד מסביר היכן כל callback ממתין, ולא רק מקריא את הפלט.
- הוא משנה שורה אחת ומעדכן את התחזית לפני ההרצה הבאה.
- הוא יודע לומר מתי המודל הפשוט אינו מספיק וצריך לבדוק תיעוד או התנהגות בדפדפן.
משימת 15 דקות
כותבים חידת תזמון משלכם
התחילו בשתי הדפסות סינכרוניות, הוסיפו Timer ו-Promise אחד, וכתבו את הפלט הצפוי בלי להריץ. לאחר הבדיקה, ציירו ארבע קופסאות: מחסנית, דפדפן, Microtasks ו-Tasks. העבירו ביניהן כל פעולה עד שהסדר ברור.
המטרה אינה לייצר קוד מסובך, אלא ניסוי קטן שאפשר להסביר שורה אחר שורה.
ממשיכים מהתור אל הפרויקט
עכשיו מחפשים את ה-Event Loop בקוד אמיתי
פתחו פרויקט דפדפן עם אירוע לחיצה, שמירה מקומית או בקשת API. סמנו מה קורה מיד ומה חוזר כ-callback. כך חידת קונסול הופכת לכלי דיבאגינג שימושי.
