הevent loop אולי נשמע כמו משהו מסובך, אבל האמת שהוא רעיון די פשוט.
בואו נתקדם שלב אחרי שלב.
single thread
javascript היא שפה שפועלת על thread אחד בלבד. (single thread)
בקצרה, single thread זה אומר שיש רק גוף אחד שאחראי לבצע את כל הקוד שקיים במערכת.
אז אם לדוגמא אתם טוענים דף של javascript, ובדף הזה יש כמה פונקציות, יש רק גוף אחד של javascript שעובר על הפונקציות אחת אחת ומטפל בהן.
זו עבודה מאוד קשה עבור אותו single thread. איך הוא יכול לתזז ולרוץ בין כל המשימות שיש במערכת?
נהוג להמשיל את הרעיון של single thread למלצר יחיד במסעדה מלאה אנשים.
תחשבו שהמלצר מגיע לשולחן מלא בסועדים ולוקח מכולם את ההזמנות. תוך כדי שהוא רושם את ההזמנות של השולחן הראשון, שולחן שני מקצה האחר של המסעדה מבקש שייגש אליו מלצר. המלצר לא יעצור את השירות שהוא נותן לשולחן הראשון ויעבור לשרת את השולחן השני, אלא יזכור שיש לו משימה נוספת ויסיים לאסוף את כל ההזמנות מהשולחן הראשון.
אחרי שהמלצר סיים עם השולחן הראשון, הוא עובר על רשימת "המשימות" שהצטברו לו, ועובר לבצע את המשימה הבאה.
ככה בדיוק עובד הרעיון של single thread ו- event loop.
למשל אם גולש באתר לוחץ על כפתור שאמור להפעיל איזשהי פונקציה שמבצעת קריאה לשרת, קריאה אסינכרונית שלא מסתיימת מיד, ומיד לאחר מכן הגולש לוחץ על כפתור אחר, לפני שהסתיים הטיפול בלחיצה על הכפתור הראשון. במקרה כזה מה עושה הsingle thread שאחראי על כל הקוד? הוא מפסיק את הטיפול בלחיצה על הכפתור הראשון?
לא.
ה- thread פשוט "ירשום לעצמו" שיש עוד לחיצה על כפתור שעליו לטפל בה, ורק אחרי שהטיפול בלחיצה הראשונה יסתיים, ה-thread יעבור לטפל בלחיצה על הכפתור השני.
המקום בו ה thread "רושם" את הפעולות שעליו לבצע כשהוא יתפנה, נקרא "event queue". זה "תור" של פעולות שיש לבצע. התור הזה פועל על פי עקרון ה First in first out. ז"א שהפעולה הראשונה שנכנסה לתור, היא תהיה גם הפעולה הראשונה שה thread יבצע כשהוא יתפנה מהפעולה הנוכחית שהוא מבצע.
כל עוד יש מטלות שנמצאות עדיין בתוך התור ומחכות לביצוע, ה thread ימשיך לחזור לתור ולמשוך משם מטלה אחרי מטלה, עד שלא יישארו שום מטלות בתור.
זה מה שנקרא "event loop". כמו לולאה שפועלת כל עוד יש מטלה בתוך התור, ומבצעת את המטלות של התור אחת אחרי השניה.
סינכרוני, אסינכרוני, ו call stack
javascript היא שפה שמורכבת משילוב של שני סוגים של קוד. קוד סינכרוני, וקוד אסינכרוני.
קוד סינכרוני – קוד שרץ שורה אחרי שורה, בו שום שורה לא מתחילה לפני שכל הפעולות שבשורה שלפניה מסתיימות.
קוד אסינכרוני – קוד שלא מחכה שכל הפעולות של השורה הנוכחית יסתיימו, וכבר ממשיך ומפעיל את השורה הבאה. (לדוגמא קריאות לשרת, שלוקח להם זמן להחזיר תשובה)
כשה-thread מתחיל להוציא לפוועל פונקציה מסויימת, זו יכולה להיות פונקציה פשוטה אחת וזהו, או פונקציה שמפעילה עוד פונקציה, שהיא בתורה מפעילה פונקציה נוספת.
וכאן יש הבדל בין פונקציות סינכרוניות לפונקציות אסינכרוניות.
אם נחזור לדוגמא של המלצר, נניח שהמלצר נמצא באמצע לקחת את ההזמנה של שולחן מספר 1, ופתאום אחד הסועדים בשולחן מתחיל לדון עם סועד אחר, איזו מנה הכי כדאי לו להזמין. המלצר שלנו לא יכול פשוט לומר לסועדים "אין לי זמן אני הולך אם אתם לא מזמינים מייד". הוא ייאלץ להמתין בסבלנות עד שהסועדים יסיימו להזמין את המנות שלהם. זו פעולה סינכרונית. המלצר לא יעבור לשולחן הבא, לפני שהוא יסיים לקחת את ההזמנה של השולחן הנוכחי.
באופן דומה, אם ה thread מפעיל פונקציה שמפעילה עוד פונקציה, שמפעילה עוד פונקציה, אין לthread ברירה, והוא צריך להמשיך לטפל בכל הפונקציות הללו עד שהן יסתיימו.
כדי לנהל את הפונקציות הפנימיות הללו, avascript משתמשת ברעיון שנקרא "call stack". סוג של "מחסנית" ששומרת בתוכה את הפעולות שמצטברות בתהליך .
אז עכשיו נוספה לנו עוד חתיכה בפאזל של ה single thread.
כל עוד יש איזשהי משימה שצריך לבצע בתוך התור של המשימות (event queue), הthread יבצע את המשימה הראשונה בתור, ולא יחזור שוב לתור עד שהוא יסיים את כל המשימות שיצטברו לו בתוך המחסנית בעקבות הטיפול במשימה הנוכחית.
אם המשימה הנוכחית שהthread אסף מהתור, היא פונקציה שמפעילה עוד פונקציה שהיא מפעילה עוד פונקציה, כל פונקציה כזו נכנסת למחסנית, ורק כאשר הthread יסיים את כל הפונקציות הללו וירוקן אותן מהמחסנית, רק אז הוא יחזור לבדוק אם יש עוד משימות בתור.
זה היה תיאור של הדרך בה הthread מטפל בקוד סינכרוני.
קוד אסינכרוני עובד בצורה מעט שונה.
בחזרה לדוגמא של המלצר שלנו, כאשר המלצר מעביר את ההזמנה משולחן 1 אל הטבח על מנת שיכין את האוכל, המלצר לא יכול לעמוד ליד המטבח ולחכות שהאוכל יהיה מוכן כדי להחזיר אותו לשולחן מספר 1. הוא מסיים להעביר את ההזמנה לטבח, ומיד עובר לאסוף לקחת הזמנה נוספת משולחן נוסף. זו פעולה אסינכרונית. המלצר אומר לטבח שיקרא לו כאשר האוכל מוכן.
עכשיו נחשוב על מצב בו המלצר ממשיך וניגש לשולחן 2 לקת מהם את ההזמנה, ובמקביל , שולחן 3 מבקש שהמלצר יבוא אליו כדי לקחת גם מהם את ההזמנה, וגם הטבח מודיע למלצר שהאוכל מוכן עבור שולחן 1.
במצב כזה, המלצר שלנו שעובר על פי עקרון first in first out, יזכור שהוא צריך לגשת לשולחן 3 ושאחרי זה הוא צריך להגיש את האוכל לשולחן 1.
ככה זה גם עובד בjavascript. כאשר משימה מסויימת שה-thread מטפל בה היא פעולה אסינכרונית, הפעולה הזו לא נכנסת למחסנית, ולא מעכבת את ה thread מלחזור לתור ולבדוק איזה עוד משימות עליו לבצע. כאשר הפעולה האסינכרונית תסתיים, ולצורך הדוגמא, השרת יחזיר את התשובה לה חיכינו, הטיפול במשימה הזו ייכנס גם הוא לתור, וינוהל גם הוא על פי העיקרון של first in first out, שזה אומר שכאשר יסתיים הטיפול בכל המשימות שקדמו למשימה הזו בתוך התור, רק אז יתפנה הthread לטפל במשימה שזרה כעת מהשרת.
סיכום
כל הנ"ל יכול להסביר הרבה מההתנהגות של javascript שניתן לראות בדפדפן לפעמים. למשל מצב בו ישנה חלונית אחת בדפדפן אשר תקועה בגלל שאיזשהי פונקציה רצה ברקע ותוקעת את כל התהליך, וגם אם תלחצו על כל מיני כפתורים בחלונית, שום דבר לא יפעל. אבל אחרי שהפונקציה התוקעת תסתיים ותשתחרר, פתאום כל הלחיצות שלחצתם יתחילו לפעול בזה אחר זה.
זה כמובן מאחר וכל פעם שלחצתם על כפתור, הפעולה הזו נכנסה לתור המשימות, ורק כאשר המחסנית (call stack) התרוקנה וכל המשימות הנוכחיות נגמרו רק אז הthread יכל לגשת לתור ולטפל במשימה הבאה.
במאמר הזה ניסיתי לסביר מה הוא עקרון ה event loop ב javasript, באיזה עקרונות נוספות הוא נוגע, ואיך כל זה עובד. מקווה שהצלחתי 🙂
מוזמנים להשאיר תגובות, עדיף נחמדות, ובקשות לנושאים נוספים שתרצו שאכתוב עליהם.