הבטחות ב JavaScript
מודל הפיתוח האסינכרוני של JavaScript לא פעם מתנגש עם האופן בו היינו רוצים לכתוב קוד, ועלול להוביל לקטעי קוד מבולגנים וקשים לתחזוקה. גירסת ES6 הוסיפה לדפדפנים אובייקט שנקרא Promise המיועד לפשט קוד אסינכרוני. אובייקט זה נתמך בכל הדפדפנים המודרניים ולכן פרק זה יוקדש להבטחות ולבעיות אותן הן פותרות.
1. מהי הבטחה
הבטחה היא אובייקט המייצג פעולה אסינכרונית הנמצאת כרגע בתהליך ביצוע. פעולה זו תסתיים בעתיד בהצלחה או בכשלון, ובאמצעות אובייקט Promise אנו יכולים ״להעביר״ את אותה פעולה בין מקומות שונים בתוכנית. דבר זה מאפשר לנו להגדיר את קוד הטיפול בפעולה במקום שונה מהמקום בו התחלנו לבצע את הפעולה, ועדיין להשאיר את הקוד קריא.
אמחיש את העניין באמצעות דוגמא פשוטה. ניקח בתור פעולה אסינכרונית את הפונקציה setTimeout. הקוד הבא למשל ידפיס את ההודעה start ו-5 שניות לאחר מכן ידפיס את ההודעה hello:
console.log('--- start');
setTimeout(function() {
console.log('hello');
}, 5000);
הפעולה האסינכרונית בדוגמא היא המתנה, וקוד הטיפול בפעולה מוגדר במקום בו התחלנו את הפעולה.
שכתוב הקוד כך שישתמש בפונקציה חיצונית לא ישנה מהותית את חלוקת התפקידים בקוד. ניתן לראות שגם במצב כזה החיבור בין הפונקציה החיצונית לאירוע מתבצע מאותו קטע הקוד שיוזם את הפעולה.
function after_timeout() {
console.log('hello');
}
console.log('--- start');
setTimeout(after_timeout, 5000);
הבטחה מאפשרת שינוי בחלוקת התפקידים באמצעות הפרדה בין ייזום פעולה אסינכרונית לבין הגדרת קוד הטיפול בפעולה זו. הבטחה הינה אובייקט העוטף פעולה אסינכרונית ומאפשר העברת הפעולה עצמה למקום אחר בתוכנית. אציג תחילה את הקוד המשתמש בהבטחה ולאחר מכן נפרט כיצד מוגדר אובייקט ההבטחה:
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve();
}, 5000);
});
p.then(function() {
console.log('hello');
});
הקוד שלנו מחולק הפעם לשני חלקים: תחילה אנו מגדירים אובייקט Promise ושומרים אותו במשתנה p. רק בחלק השני אנו מחברים לאובייקט זה את קוד הטיפול, ומבקשים להדפיס את ההודעה hello לאחר סיום הפעולה. הבטחות הולכות לעזור לנו במקומות בהם הקוד בחלק השני נמצא במרחק ניכר בתוכנית מהקוד בחלק הראשון.
2. איך פועלת הבטחה
מבחינה טכנית אובייקט הבטחה מקבל בבנאי פונקציה לביצוע, המופעלת מיד בעת יצירת האובייקט. בתוך גוף הפונקציה יש לנו שני משתנים להיעזר בהם כדי לדווח את התוצאה לעולם החיצון, בשניהם שמורות פונקציות. אנו נקרא לפונקציה resolve כדי לדווח על הצלחה בביצוע הפעולה האסינכרונית, ולפונקציה reject כדי לדווח על כשלון בביצוע פעולה זו.
כך למשל נבצע טעינה אסינכרונית של תמונה בתור הבטחה, עם אפשרות לדווח הצלחה או כשלון:
var img = new Image();
img.src = 'http://cdn.nextshark.com/wp-content/uploads/2013/10/success_kid.jpg';
var p = new Promise(function(resolve, reject) {
img.addEventListener('load', resolve);
img.addEventListener('error', reject);
});
החלק השני הוא השימוש בהבטחה. כאן יש לנו שתי פונקציות באמצעותן אנו מגדירים קוד טיפול בפעולה: הפונקציה then מאפשרת להגדיר קוד שיופעל במידה והפעולה האסינכרונית הצליחה, והפונקציה catch מאפשרת להגדיר קוד טיפול למקרה שהפעולה האסינכרונית נכשלה. כך זה נראה בקוד.
p.then(function() {
console.log('Image load OK');
});
p.catch(function() {
console.log('error');
});
3. דוגמא לשימוש בהבטחות: המתנה למספר פעולות אסינכרוניות.
השאלה הראשונה שרבים שואלים כשנתקלים ברעיון ההבטעות היא מה הערך של מנגנון זה עבורי, כלומר איפה בקוד אפשר להשתמש בהבטחות כדי לפשט קוד מורכב. בפרק זה אציג שתי דוגמאות לשימוש בהבטחות מהעולם האמיתי, תחילה נראה כיצד להמתין למספר פעולות אסינכרוניות, ולאחר מכן נדבר על שרשור טיפול במספר פעולות אסינכרוניות.
בתוכנית הבאה אנו טוענים מספר תמונות, ורק לאחר שכל התמונות נטענו בהצלחה נרצה להציג הודעה למשתמש שאנו מוכנים להמשיך:
כדי לדעת אם כל התמונות נטענו על הקוד לשמור מונה ולבדוק בכל סיום טעינה האם התמונה שנטענה היא האחרונה, תהליך המסרבל את קוד הטיפול. פתרון מבוסס הבטחות ייצור הבטחה לכל טעינת תמונה, ובאמצעות הפונקציה Promise.all נוכל לקבל הבטחה חדשה המייצגת איחוד של כל ההבטחות גם יחד. היא תתממש כאשר כל ההבטחות התממשנה, ותתבדה בהבטחה הראשונה שתדווח כשלון. כך נראה הקוד:
מה שהרווחנו בקוד הוא היכולת להפריד בין הגדרת הפעולה לבין תוצאת הפעולה, כלומר ניתוק ההחלטה לאיזה פונקציות לקרוא מקוד ביצוע הפעולה.
4. דוגמא לשימוש בהבטחות: שרשור אירועים.
המקום השני בו הבטחות מתגלות כיעילות הוא באפשרות ״לחשוב״ כמו בקוד סינכרוני. כך למשל נראה קוד סינכרוני המבקש שם משתמש באמצעות prompt ומדפיס הודעת ברוכים הבאים על המסך:
הבעייה מתחילה כשאנו הופכים את הדיאלוג מ prompt לתיבת טקסט מבוססת HTML אנו נאלצים גם להפוך את הקוד לאסינכרוני. בניגוד ל prompt שכאילו עוצר את ריצת קוד ה JavaScript ורק לאחר הכנסת פרטי המשתמש עובר לשורה הבאה, במודל עבודה אסינכרוני אנו מציגים למשתמש את הדיאלוג מבוסס ה HTML וממתינים. ברגע שהמשתמש יסיים להכניס את הפרטים נוכל להתקדם להצגת ההודעה. כך נראה הקוד:
שימו לב שבמקום פונקציה אחת המתארת את זרימת האירועים יש לנו כעת שתי פונקציות, והקשר ביניהן רופף. אפשר בהחלט לדמיין אחת מהן בקובץ אחד והשנייה בקובץ אחר, מה שמקשה מאוד על הבנת הקוד.
הבטחות מאפשרות לנו לחזור למודל הסינכרוני שאתו התחלנו את התרגיל. אנו נעטוף את הדיאלוג האסינכרוני שלנו באובייקט הבטחה וכך נוכל לכתוב את הקוד הבא בכפתור ההתחלה:
btn_start.addEventListener('click', function() {
wait_for_user_input().then(function(name) {
result.innerHTML = 'Hello! ' + name;
});
});
זה כבר מאוד מזכיר את הפתרון הראשון עם פקודת ה prompt. השימוש בהבטחה מאפשר לנתק בין הגדרת והפעלת דיאלוג אסינכרוני, לבין טיפול בקלט שמגיע באמצעות דיאלוג זה. כך נראה קוד הפתרון המלא:
5. סיכום וקריאת המשך
הבטחות הן תבנית עיצוב קוד המאפשרת לנתק בין ייזום פעולה אסינכרונית לבין קוד הטיפול בתוצאת פעולה זו. הבטחות אינן רעיון מושגי חדש, אך התמיכה של דפדפנים באוביקטי הבטחה התחילה רק מגרסאות חדשות יחסית, כחלק מהמעבר לפיתוח Web Applications. הבטחות מאפשרות לשמור על מבנה קוד נקי במערכות מורכבות הכוללות ירושה וחלוקת תפקידים בין מחלקות.
ניתן למצוא חומר לקריאה נוספת על הבטחות בקישורים הבאים:
- הבטחות ב ES6:
http://www.html5rocks.com/en/tutorials/es6/promises/ - תיעוד API של הבטחות מתוך MDN:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise - כיצד לממש בעצמכם אובייקט הבטחה ב JavaScript:
http://www.mattgreer.org/articles/promises-in-wicked-detail/
דוגמת קלט מהמשתמש שהוצגה במדריך:
תיעוד מלא על הבטחות ב MDN:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise