אבני הבניין של האינטרנט: סביבת-הריצה של ג’אווהסקריפט

שפת ג׳אווהסקריפט תוכננה כשפה פשוטה למדי, שפה מפורשת המורצת ע״י מפרשן (interpreter) [מה ההבדל בין מפרשן למהדר]. לא עוד.
המנועים המודרניים של ג׳אווהסקריפט מבצעים קומפילציית JIT לקוד מכונה יעיל למדי – ועל כן הם מתהדרים בשם “מנועים”. הביצועים, השתפרו בסדר גודל בכמה השנים האחרונות. הגרסה הבאה של ג׳אווהסקריפט (שם קוד Harmony) כוללת אלמנטים רבים חדשים בשפה שיעזרו לתמוך במערכות קוד גדולות (מחלקות, מודולים, ועוד).

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

מפת התמצאות

הנה מנועי הג’אווהסקריפט של כמה מהדפדפנים הנפוצים:

עץ המשפחה של מנועי הג’אווהסקריפט. מקור.
JIT Compilation (קימפול קוד לג’אווהסקריפט לקוד מכונה לפני ההרצה) היא כבר תכונה סטנדרטית של מנועי הג’אווהסקריפט. Crankshaft (בעברית: גל ארכובה) ו TraceMonkey עד OdinMonkey הם בעצם JIT compilers (בהפשטה) של מנועי הג’אווהסקריפט V8 ו SpiderMonkey בהתאמה. הקומפיילר הוא רק חלק מסוים מהעבודה שמנוע הג’אווהסקריפט מבצע.
כל המנועים המודרניים (כרום +13, פיירפוקס +4, ספארי +5.1, אינטרנט אקספלורר +9) תומכים* ב ECMAScript 5.1 – שהיא הגרסה האחרונה של שפת ג’אווהסקריפט. בעת פוסט זה התמיכה ב ECMAScript 6 (הגרסה הבאה) היא חלקית למדי.
* חריגות קלות: IE9 שלא תומך ב “use strict” ו Safari שעדיין לא תומך ב prototype.bind

טעינת הסקריפטים וסדר ההרצה

הרצת קוד ג’אווהסקריפט כוללת 3 שלבים מרכזיים: פענוח (parsing), אופטימיזציה ורישום הפונקציות (function resolution) והרצת הקוד (execution).
שלב 1: שלב הפענוח
בשלב זה מפרשים את “עץ התוכנית” של קוד הג’אווהסקריפט, מבצעים תיקונים קלים (הוספת נקודה פסיק בסוף משפט, דבר שעלול לעתים להסתיים בקוד שגוי) והמרה לשפת ביניים יעילה יותר (JIT compilation).
אם המפענח נתקל בשגיאה בקוד (למשל: פונקציה שלא נסגרה) הוא מדלג על קטע הקוד וממשיך לבלוק הבא. מדיניות סלחנית זו נקבעה עוד בימים בהם עוד היה מקובל לכתוב “בלוקים” של ג’אווהסקריפט בתוך ה HTML, כך שדילוג על בלוק יחיד היה יכול לאפשר הרצת דף בצורה נסבלת. תקלה בקובץ ג’אווהסקריפט תגרום לדילוג על כל הקובץ – מה שקרוב לוודאי וישבית את כל התוכנית.
שלב 2: שלב האופטימיזציה ורישום הפונקציות

בשלב זה עוברים על קוד הג’אווהסקריפט (בצורת “bytecode”) ומבצעים בו שיפורים:

  • inline של פונקציות קצרות לתוך הקוד שקורא להן
  • hoisting – העברת ההגדרה של משתנים ופונקציות לתחילת ה scope בהן הן הוגדרו. מנגנון זה, כך שמעתי, החל כאופטימיזציה ללולאות for אך הפך לחלק מהשפה, וחלק שגם יכול לגרום לבעיות (הסבר מיד).
  • inline caching – הוספת שכבה של caching מעל אובייקטי ג’אווהסקריפט, אחת מהאופטימיזציות היעילות בשנים האחרונות. מקור1 מקור2.
  • inline של קבועים (משתנים שבכל התוכנית אין להם השמה).
  • ביטול קוד שאי אפשר להגיע אליו (“dead-code elimination”).

הנה דוגמה המסבירה את מנגנון ה hoisting. אפשר למצוא עליו הסבר נוסף בפוסט מבוא מואץ לג’אווהסקריפט (חפשו: hoisting).

קוד שנראה כך:
בעצם מתורגם, לאחר ה hoisting, לכך:
שלב 3: שלב ההרצה
הרצת הקוד, כפי שהיינו מצפים שיקרה.

ג’אווהסקריפט והדפדפן

לכל טאב או חלון בדפדפן מוגדרת סביבת ג’אווהסקריפט עצמאית משלה. בסביבה זו יש אובייקט בשם window המהווה את ה context הגלובלי. אם אגדיר var x גלובלי בדף אחד הוא יהיה מופע שונה מ var x גלובלי בדף אחר (בהגדרת “var x”, בעצם הגדרתי member חדש בשם x על האובייקט window). באופן דומה, פקודות שניתן לייחס בטעות לשפת JavaScript כגון setTimeout או alert הן בעצם פקודות על האובייקט window (הערך ב mdn), כלומר window.setTimeout או window.alert.

את האובייקטים שמספקת סביבת הדפדפן נוטים לחלק לשני אזורים:

BOM – אובייקטים הקשורים ל Shell של הדפדפן.
DOM – אובייקטים הקשורים לדף ה HTML המרונדר.

ה DOM הוא מורכב למדי ומעבר ל scope של פוסט זה (הנה לינק לספר טוב אונליין).
להלן סקירה מהירה של האלמנטים המרכזיים ב BOM:

  • window – חלון[א] או טאב של הדפדפן. מלבד תפקידו כ context הגלובלי ואובייקט האב, יש על אובייקט ה window מתודות למדידת גודל חלון הדפדפן, מצב ה Scroll ומספר אירועים שקשורים לשינוי גודל החלון או ה scrolling.
  • location – מתאר את ה URL הנוכחי של החלון. ניתן לקרוא אלמנטים, לשנות או להאזין לשינויים. שם מדויק יותר היה פשוט “url.
  • history – היסטריית שינויי ה location בדפדפן. ניתן להפעיל פקודות back, forward לדחוף או להסיר אלמנטים מההיסטוריה.
  • navigator – פרטים על הדפדפן: user agent string, תמיכה בג’אווה או ב cookies, סוג וגרסת הדפדפן וכו’. קרוב לוודאי שמקור השם הוא בדפדפן Netscape) Navigator) ומאז הוא איתנו. שם מדויק יותר היה פשוט “browserInfo“.
  • screen – פרטים על התצוגה המסופקת לדפדפן: רזולוציה, עומק צבע וכו’.
  • frames – רשימת ה frames או ה iframes שבתוך החלון.

תגית , משלבת בתוך המסמך מעין “מופע חדש של דפדפן” עם אובייקט windows משלו, document משלו, היסטוריה משלו ועוד (הערך ב mdn). השימוש העיקרי של ה iframe הוא להציג בצורה מבודדת, בתוך חלון, תוכן שתוכנן לרוץ בדף עצמאי (לא מתנהג בצורה “חברותית”) או שמגיע מ domain אחר. מבחינות מסוימות iframe היא גישה מיושנת ומפתחי ווב רבים רואים אותה כ deprecated, מצד שני ליכולותיה עדיין אין תחליף – ולכן עדיין משתמשים בה.

הערה: השם iframe הוא קיצור של internal frame. ישנה גם תגית בשם frmae שהיא כמעט זהה, אך מחויבת להיות בתוך תגית frameset. כל מה שאומר על iframe נכון בעיקרון גם ל frame.

כל iframe הוא בעצם context חדש בדפדפן, כלומר אובייקט window, מרחב גלובלי חדש, DOMTree חדש וכו’. הוא כמעט כמו “חלון חדש בתוך חלון קיים” בדפדפן, מלבד 2 הבדלים:

  • ניתן לגשת מ iframe אל משתנים / פונקציות ב top frame (כלומר ה context המקורי של הדף). כל זאת תחת מגבלות ה Single Origin Policy – נושא אליו נצלול בפוסט אחר בסדרה.
  • עבור חלון חדש יהיה thread חדש בדפדפן, בעוד iframes באותו חלון משתפים את אותו ה thread. הצורך בשיתוף thread נובע מהדרך בה מתרחשת מקביליות בדפדפן – נושא שנדון בו מיד.

מודל המקביליות בדפדפן: Thread יחיד

לכל חלון פיסי בדפדפן יש thread יחיד. גישה זו היא שונה למדי ממודל ה threads של Java או NET.
היתרונות של גישה זו הם:

  1. יעילות גבוהה מאוד בקוד עם הרבה פעולות IO. כדאי להזכיר: Threads הוא מודל שעיקרו להקל על המפתח, ואינו הדרך היעילה להגיע לביצועים גבוהים [זהירות, פוסט].
  2. פשטות הנובעת מכך שאין צורך לדאוג לסנכרון. Deadlock או Racing Condition הם פשוט לא אפשריים כאשר ישנו thread בודד.
  3. מודל שהתאים מאוד ל”דפי אינטרנט דינמיים” – כלומר אתרים עם אינטראקציה ולא בהכרח אפליקציות.

לגישה זו יש גם כמה חסרונות משמעותיים:

  1. העובדה שב javaScript כל פעולות ה IO הן אסינכרוניות בהכרח [ב] יכולה להקשות מאוד על המעקב אחר flow בקוד. Promises [זהירות, פוסט] היא אחת הדרכים הנפוצות להתמודד עם קושי זה.
  2. אי-יכולת לנצל יותר מ core אחד במעבד.
  3. קוד שכולו CPU (כגון חישוב מתמטי מורכב) יתקע ממשק המשתמש ויהפוך את האפליקציה ללא רספונסיבית.

החסרונות הם אכן משמעותיים ובהמשך נראה את דרכי התמודדות איתם. סה”כ גישת “ה thread הבודד” זלגה כיום לצד-השרת (node, nginx וכד’) משיקולי יעילות.

ה Event Loop

אני מניח שאתם מגיעים עם רקע במדעי המחשב (Java, ++C או NET.) והרעיונות של מחסנית (stack) וערימה (heap) אינם זרים לכם.

בג’אווהסקריפט המודל דומה למודל הערימה והמחסניות של השפות הנ”ל, אך יש בו כמה שינויים:

  • ישנו stack יחיד – מכיוון שיש thread יחיד.
  • ישנו Queue – עליו נדבר מייד.
  • האובייקטים ב Heap הם בעיקר Closures (אם כי לא רק) – כך שהתלות בחלק הפומבי שלהם היא מה שמונע מהחלק הפנימי להתנקות ע”י ה garbage collector.

ה thread רץ במין לולאה אינסופית שנראית בערך כך:

מה ממלא את ה Queue?
  • פעולות IO שנסתיימו, כגון ajax.$ (שזו בעצם עטיפה יפה לאובייקט ה XmlHttpRequest).
  • events של הדפדפן כגון DOMElement.onClick או window.resize.
  • פעולות setInterval ו setTimeout. למי שלא מכיר, פעולות אלו מבקשות להפעיל פונקציה נתונה בעוד זמן מסויים (setTimeout) או כל פרק זמן קבוע (setInterval).

כל הפעולות הנ”ל לא יופעלו לעולם ע”י thread אחר (כאשר מדובר באותו החלון). הן יופעלו כאשר ה thread הראשי ביצע את כל הקוד שלפניו, רוקן את המחסנית מקוד להרצה – ורק אז הוא זמין לטיפול ב”בקשות אחרות”. אם נעמיס על ה thread הראשי פעולות חישוב כבדות, כל האירועים שממתינים ב Queue “ייתקעו” (ואיתם תתקע האפליקציה) עד אשר ה thread הראשי יתפנה אליהם.

הנה דוגמת קוד שתעזור להדגים את התנהגות ה event loop:

והנה תוצאת ההרצה:

חדי העין ישימו לב ש “work for later” יצא 3 ולא מה שהיינו מצפים: 0 עד 2. הסיבה לכך היא שהערך של i מוערך בעת הרצת הקוד (הקוד נקשר ל global closure) ולא בעת הקריאה ל setTimeout, כפי שאולי יכול להשתמע.

הפתרון הוא כמובן להעביר את i כפרמטר לפונקציה בעת הקריאה ל setTimeout:

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

תורת היחסות של סביבת-הריצה של ג’אווהסקריפט

שאלת טיזר: כמה intervals של 100ms קיימים בשנייה אחת?
תשובה מתמטית: עשר.
תשובה בסביבה-הריצה של ג’אווהסקריפט: ניתן לקוות ל 9, אבל לפעמים גם רק 2.

הא?! נסביר מייד.

מה ההבדל בין 2 צורות הכתיבה הבאות?

דעה רווחת היא שההבדל הוא שאופציה א’ תפעל כל INTERVAL קבוע, בעוד אופציה ב’ תפעל כל INTERVAL + הזמן שלוקח לבצע את doSomeWork.

תיאור זה הוא קרוב למציאות – אך איננו נכון. נאמר שקבענו setTimeout להחליף אייקון על המסך בעוד 100ms. ברגע שנזרק האירוע (timer) – מי ייקח את הקוד ויבצע אותו? אם ה main thread שקוע בתוך קוד שבמחסנית – אין מי שיטפל בבקשה. כל מה שיקרה בעקבות ה timer הוא רישום הפונקציה שביקשנו להריץ ל Queue. היא תטופל רק ברגע שה main thread יתפנה (וכל האלמנטים הקודמים ב Queue יטופלו).

ההבדל המדויק, אם כן, בין setInterval (“אופציה א'”) ל setTimeout (“אופציה ב'”) היא שבאופציה א’ יבצוע רישום של הפעלת הפונקציה doSomeWork ל Queue כל INTERVAL. בכדי להגן על ה Queue מפני זליגה (Queue Overflow), הפעלת הפונקציה תרשם רק אם היא איננה רשומה כבר ב Queue – ז”א לא יהיה יותר מעותק אחד שלה ב Queue.
באופציה ב’ יהיה רישום ל Queue ברגע שאנחנו קראנו לפקודת setTimeout – וכאן אין מנגנון הגנה מפני רישום חוזר (כי פחות סביר שהוא יקרה ללא שהתכוונו לכך). מצד שני ההפעלות לא ינסו להתרחש בפרקי זמן קבועים.

שימו לב שלמנגנון ה timers של הדפדפן ישנם חוסרי-דיוק משלו הנובעים מחלוקת העבודה במערכת ההפעלה וה threads בתוך הדפדפן. כדאי להניח על שגיאות של פלוס/מינוס 5ms בהפעלת ה timers.

חזרה לטיזר בתחילת הפסקה: כיצד אם כן ייתכן ש setInterval של 100ms יתרחש רק פעמיים בשנייה?

הנה דוגמת קוד שגורמת לכך:

כמה הערות על הקוד:

  • ()performance.now היא דרך מדויקת ואמינה יותר מ ()Date.now – עבור מדידת ביצועים.
  • באופציה ב’ תנאי היציאה מתרחש לאחר 9 פרקי זמן, מכיוון שפעולת הרישום מתרחשת כ INTERVAL אחד לפני שהקוד ירוץ בפועל.

העבודה שנעשית ב doSomeWork נראית אמנם דיי אגרסיבית, אבל יש גם תנאים מקלים:

  • הרצתי את הבדיקות על about:blank – כלומר דף ללא StyleTree ו DOM מינימלי.
  • הרצתי את הבדיקות על דפדפן כרום גרסה 27, ועל מעבד i5 שולחני שרץ במהירות 4Ghz – הקצה הגבוה של החומרה הסבירה שעליה תרוץ אפליציית ווב בימנו.

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

הנה התוצאות:

לא ניקיתי את ה DOM בין ההרצות כך שלהרצה מאוחרת יותר – יש עבודה קשה יותר בכל הרצה של ()doSomeWork.

הרצה ראשונה
כבר כאן אנו רואים שמנגנון ה setInterval (יוצר ה”טיק”) מזייף ואפילו מדלג על 3 טיקים (5, 7 ו 9).

הרצה שלישית
הנה 2 טיקים בלבד בשנייה. מי אמר שאין עוד אירועים שמתרחשים? (כמו “טוק”)

קצת יותר ברצינות: זמן של 100ms הוא “זמן דפדפן” ארוך, ואם הקוד שלכם יעיל – אינכם אמורים להגיע ל2 טיקים של 100ms בשנייה. מצד שני, ישנם רגעים שבהם הדפדפן עובד קשה יותר – ועליכם להניח שטיקים מסוימים “יאבדו”, או לפחות יתבצעו באיחור.

2 לקחים מעשיים

טריק ידוע בכדי “לא לתקוע” את הדפדפן עם פעולות חישוב יקרות הוא לעצור אותן בנקודות ציון מסוימות ו”לדחוף” את המשך הפעולה ל Queue בעזרת (setTimeout(calcFunction, 0. פעולת ההמשך (“calcFunction”) תרשם לסוף ה Queue ותאפשר לאירועים אחרים להתרחש. ההבדל יכול להיות דרמטי בין “דפדפן תקוע” (שהמשתמש מנסה לסגור) ל “דפדפן מגיב”.

דרך (עקיפה) אחרת לשחרר עבודה מה Thread הראשי (כלומר: היחידי) היא לשלב יכולות שעובדות באופן מובנה על threads אחרים בדפדפן. דוגמה טובה לכך תהיה שימוש ב CSS Animations המבוצעים ע”י ה thread של מנוע הרינדור – וכך משחררים את ה Thread הראשי להריץ קוד ג’אווהסקריפט. מעבר לכך – כנראה שניצלתם בדרך זו core אחר של הCPU שעד כה היה בכלל מובטל!

Web Workers

לצורך התמודדות עם חישובים ארוכים, או בכדי לנצל בצורה מירבית מעבד בעל מספר ליבות, נבנה תקן ה Web Workers – תקן חדש יחסית המאפשר להאציל עבודה מוגדרת מה Thread הראשי על Threads אחרים. עבודה עם web workers היא מעט מסורבלת מכיוון שה threads השונים אינם משתפים זיכרון – עליהם לתקשר בעזרת הודעות בלבד (רעיון דומה ל Actors, אותם הזכרתי בעבר).

הצורך בהעברת ההודעות בין ה web workers ל main thread נובע ממודל ״ה Thread היחיד״ של הדפדפן: אם הייתה גישה ישירה לזיכרון משותף – אנו נחשפים לבעיות concurrency אפשריות. באופן דומה אילוץ זה הכתיב שאם frames באותו חלון יכולים לתקשר אחד עם השני – אסור להם לרוץ ב threads נפרדים והם חייבים לשתף thread.

ההודעות שעוברות בין web workers עוברות by-copy ולא by-reference מהסיבות הנ”ל. נראה שרוב הדפדפנים ממש מעבירים אותן כהודעות IPC שעוברות serialization / de-serialization בכל פעם.

הנה רשימה קצרה של מה ש Web worker יכול / לא יכול לעשות.

ל Web Worker מותר:

  • לשלוח הודעות ל Workers אחרים.
  • ליצור ולהרוג workers אחרים.
  • לגשת (קריאה-בלבד) לאובייקטי ה navigatior וה location של ה BOM.
  • לבצע קריאות ajax (ע”י שימוש ב XmlHttpRequest).
  • להשתמש ב timers של הדפדפן.
  • לטעון קבצי ג’אווהסקריפט נוספים.
  • לפתוח connections לשרת, בעזרת web sockets.
  • לגשת ל cache ו local storage.

ל Web Worker אסור:

  • לגשת ל DOM
  • לגשת לאובייקט ה window (מלבד 2 החריגות לעיל).

סיכום

זה היה פוסט ארוך לכתיבה!
כתיבת פוסט זה הזכירה לי את קורס “מערכות ההפעלה” באוניברסיטה, וכנראה לא סתם. מערכת ההפעלה הייתה סביבת הריצה (Runtime Environment) של שרתים ואפליקציות שולחניות (לפני ש virtual machines נכנסו באמצע ושינו חלקית את התמונה).
כיום, הדפדפן הופך להיות סביבת ריצה חשובה לחלק הולך וגדל של אפליקציות, ועל כן הכרת סביבת ריצה זו – חשובה לא פחות.

שיהיה בהצלחה!

—–

[א] חלון חדש של דפדפן יכול להיות עדיין שייך לאותו process של מערכת ההפעלה.

[ב] מקרה קצת יוצא דופן הוא modal dialogs בדפדפן כגון window.confirm או window.alert. זו תכונה של הדפדפן ולא של שפת ג’אווהסקריפט.

מקורות נוספים:

הרצאה ב Velocity 2011 על מנועי ג’אווהסקריפט
כמה טיפים מעניינים לקראת סוף המצגת.
http://velocityconf.com/velocity2011/public/schedule/detail/18087

הרצאה על פרטי המימוש של מנועי ג’אווהסקריפט, מכנס SenchaCon 2010
כניסה לפרטים טכניים יותר ממה שכיסיתי בפוסט זה.
http://vimeo.com/18783283

מקביליות עם jQuery (ובכלל)

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

  • היכולת לארוז רק חלקים מהספרייה (אולי בגלל התחרות מ Zepto.js).
  • הפסקת התמיכה בגרסאות לא-חדשות של Internet Explorer.
  • איחוד מנגנוני הרישום לאירועים: bind, live ו delegate למנגנון ה on (חדשות ישנות יותר).
האמת שמדובר בשינוי שהוצג ב jQuery גרסה 1.5 (עם כמה תוספות ב 1.6 ו 1.7) – ואישית, אני לא זוכר שהוא עשה חצי מהמהומה של השינויים הנ”ל. בכל זאת זהו שינוי משמעותי וחשוב.
השינוי נוגע באזור ה Ajax ומאפשר גמישות שלא הייתה קיימת קודם לכן.
בואו נדבר עליו.

שייך לסדרה מבוא מואץ ל JavaScript ו jQuery

שינוי בפקודת ה Ajax
למי שלא מכיר, jQuery עוטף את ה XMLHttpRequest (או בקיצור XHR) של הדפדפן בצורה נוחה ומאפשר לקרוא בפשטות ajax.$ על מנת לבצע קריאת ajax. יש גם גרסאות מקוצרות בשם get.$ ו post.$.

עד גרסה 1.4 הדרך לבצע קריאת ajax הייתה כזו:
$.get('http://server.com/myurl', {
  success: onSuccess, // a callback function to be triggered on success
  failure: onFailure, // a callback function to be triggered on failure
  always: onAlways // a callback function that is triggered on either failure of success. Like "finally".
});

success, failure ו always הם callbacks שנקראים כאשר הקריאה מסתיימת, הצלחה או כישלון.

מגרסה 1.5 אפשר לכתוב את הקוד בדרך הבאה:
var request = $.get('http://server.com/myurl');
...
request.done(onSuccess);
request.fail(onFailure);
request.always(onAlways);

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

מה הטעם? האם הקוד הקודם לא נראה יותר ישיר ופשוט?

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

Making Promises
אפתח בכמה מילים גדולות: מה ש jQuery עשו, הוא לממש Design Pattern (יו… וואהו!) שידוע בשמות הבאים: Deferred Object או Promise או Future. יש אפילו הגדרה פורמלית בשם Promises/A שמגדירה כיצד תבנית זו אמורה להתנהג בג’אווהסקריפט.
לאכזבתם של לא-מעטים, jQuery לא נצמדה ל”הגדרה הפורמלית” ומימשה וריאציה קצת שונה של תבנית העיצוב, מימוש שהושפע כנראה מספריית Dojo שסיפקה יכולת דומה. אם אתם אורתודוקסים ל Promises/A, תוכלו למצוא מימושים “תקניים” בספריות כגון Q.js או Async.js שניתן להשתמש שהם. לרובנו, כל מה ש jQuery יעשו – מהווה את התקן בפועל.

על מנת להבין את המשמעות של הפעולה של ה Promise, נפרק אותה (Deconstruct) למרכיבים יותר בסיסים ונבחן אותם. הביטו על קוד ה JavaScript/jQuery הבא:

var deferred = new $.Deferred();
deferred.done(function(){ console.log('done'); }); 
deferred.fail(function(){ console.log('fail'); }); 
...
deferred.done(function(){ console.log('donedone'); });

האובייקט Deferred הוא לב העניין, אם כי אינו קשור ל ajax במאומה. הוא מעין handle-עתידי.
בעת קריאה ל ()deferred.resolve – יופעלו הפונקציות (ניתן לרשום מספר לא מוגבל של פונקציות, לכל אחד מהאירועים) שנרשמו ל done. במקרה זה – כתיבה של done ו donedone ללוג.
בעת קריאה ל ()deferred.reject – יופעלו הפונקציות שנרשמו ל fail. במקרה זה כתיבת fail ללוג.

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

ה Promise הוא אובייקט (יחידון = singleton) שחוזר מקריאה ל ()deferred.promise וכל מטרתו היא לאפשר גישה מוגבלת: לאפשר לרשום פונקציות ל done, fail ו always – אבל לא לבצע triggering ל reject או resolve.

Read/Write Pattern – שליטה ב”הרשאות” ע”י הפרדת הפעולות ל interfaces שונים. אפשר להחליף את user ב promise ואת userMaint ב Deferred על מנת לראות את תבנית העיצוב בהקשר לדיון שלנו.

הערה: ל reject או resolve ניתן לקרוא פעם אחת בלבד בחיי אובייקט ה Deferred. מאותו הרגע, כל פונקציית callback שתנסה להירשם – תופעל במקום.

כעת, לאחר שהבנו את ההתנהגות של Promise, בואו נראה אלו בעיות מבנה זה יכול לפתור לנו.

Concurrent Pattern: Monitor
השימוש ב Deferred יכול לסייע לנו לבנות מבנה של concurrent programming בשם בקר (monitor). בקר הוא כלי לטיפול באסינכרוניות, הגבוה ברמת ההפשטה שלו מ mutex או semaphore, אך נמוך מ “synchronized” של ג’אווה / #C. אנו עשויים למצוא אותו דיי שימושי בשפת ג’אווהסקריפט / שימושי ב ajax בהם אסינכרוניות היא נפוצה [ב].

Monitor – מחכה שמספר פעולות אסינכרוניות יגיעו למצב מסוים, ואז יוזם פעולה. סוג של “יישור קו”.

נאמר שאני מחכה בג’אוובסקריפט ל2 פעולות שיסתיימו: טעינה של נתונים מהשרת ואנימציית מעבר. רק ש2 אלו יסתיימו אני רוצה להציג dialog עם שאלה למשתמש.
הדרך הנאיבית לעשות זאת היא ש callback א’ ישנה ערך גלובאלי כלשהו, ו callback ב’ ימתין עד שהערך הגלובאלי השתנה. כאשר callback ב’ מצא את הערך לו הוא ציפה – אני יודע ש2 הפעולות הסתיימו.
הבעיה?
הקוד קשה לקריאה: יהיה צורך לעקוב אחרי הלוגיקה של ה callbacks להבין מה הם עושים.
קרוב לוודאי שייוצר קוד מסורבל ומאוד לא נוח לתחזוקה.
הקוד לא יעיל מבחינת ביצועים.

הרבה יותר אלנגנטי, יעיל ונוח יהיה פשוט מאוד לכתוב:

var promiseA = $.get(...);
var deferredAnimation = $.Deferred(); // call  deferredAnimation.resolve() when it is done
var promiseB = deferredAnimation.promise();

$.when(promiseA, promiseB).

done(function(promiseAargs, promiseBargs) {
   ... // what happens when both are done 
});

תענוג!

מודולריות
השימוש העיקרי, והנפוץ יותר ל Promises הוא עבור callbacks פשוטים.
כפי שציינו קודם לכן, הקוד

$.get('http://server.com/myurl', {
  success: onSuccess, // a callback function to be triggered on success
  failure: onFailure, // a callback function to be triggered on failure
  always: onAlways // a callback function that is triggered on either failure of success. Like "finally".
});

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

במערכות שכאלו, מה שסביר שיקרה הוא אחד מהבאים:

  • ה onSuccess callback יפנה לאזורים שונים במערכת ויבצע שינויים. למשל: העלמת שעון חול, הפיכת כפתור ל enabled, הוספת פריט לרשימה. קרוב לוודאי שפעולות אלו ישברו את ההכמסה של המודולים במערכת.
  • אם ישנה מודעות מספקת להכמסה, קרוב לוודאי שכל אובייקט יכלול פונקציית callback שיודעת להגיב לאירוע ולשנות את הערכים המקומיים למודול. אבל… איך מעבירים את ה callback למקום בה נעשית הצורה האסינכרונית? לעיתים זה יהיה פשוט כי הגיוני שתהיה תלות בין המודולים – ולעיתים קרובות נאלץ להעביר את ה callback בין אובייקטים רבים ולהוסיף אולי תלויות על מנת לבצע את הקישור. מי שראה קוד שמעביר מספר callbacks בין אובייקטים, על מנת לטפל באירועים שונים או פשוט callbacks ממקורות שונים, יודע עד כמה הקוד עלול להיות מסורבל ו”ללכלך” את המערכת. התועלת בהכמסה עלולה לפחות – אם המחיר הוא קוד סבוך כל כך!
פתרון שבמקרים רבים יהיה אלגנטי למדי הוא שימוש ב promise בעת יצירת הקריאה האסינכרונית ותעבירו את ה promise לאובייקטים אחרים. אובייקטים אלו יוכלו להירשם לאירועים שמעניינים אותם, אבל מעבר לזה – להעביר את ה promise הלאה לאובייקטים אחרים על שמודול המקור לא מכיר. מה שנקרא “הכמסה”. – כך שאובייקטים אחרים שהם מכירים יירשמו עליו.יש פה ייתרון במספר האובייקטים המועברים. אם 2 אובייקטים שונים היו רוצים לדעת על הצלחה או כישלון של אירוע שיצרתם – הייתם צריכים להעביר 4 callbacks. במקום זה אתם יכולים להעביר promise אחד בלבד.

סיכום 
Promise הוא לא Silver Bullet, אבל במקרים רבים הוא יכול להיות כלי רב-עצמה שיתיר סיבוכיות רבה מהמערכת. היכולת לנתק את השליטה (קריאות resolve ו reject של Deferred) מצד אחד ואת התגובה לסיום הפעולה (done, fail ו always של ה promise) מצד שני מאפשרות לשמור על מודולריות גבוהה של המערכת במחיר נמוך.

אם אתם מוצאים את תבנית העיצוב של Deferred ו Promise שימושית, יש עוד פונקציות שניתן לחקור:
Deferred ו Promise יכולים לתקשר בניהם גם אודות progress (שלא כיסיתי בפוסט). לפעולת when יש פעולה “אחות” בשם then. חשובה אולי מכולן היא פעולת ה pipe שמתירה לשרשר הצלחות / כישלונות של promise וכך לחסוך קוד.

שיהיה בהצלחה!

[א] בגרסה 1.6 שונו שמות הפונקציות – אני משתמש בשמות החדשים. בגרסה 1.8 השמות הישנים (complete, success ו error) יוכרזו כמיושנים (deprecated) ולכן אני נמנע משימוש בהם.

[ב] בג’אווהקריפט בדפדפן יש כמובן thread יחיד ואין מקביליות חישוב. מצד שני יש הרבה פעולות IO אסינכרוניות שקוראות במקביל ויש אתגר תכנותי לשלוט בהן.