מה הבעיה?
נתחיל בתיאור קצר של הבעיה אותה Promise בא לפתור, למעשה JavaScript היא שפה המושתתת על Singe-thread מה שבעצם אומר שכל פעולה שמתבצעת תתקע את שאר הקוד עד לכשתסתיים ולשם כך אנו משתמשים ב- callbacks ו- Promises שעוזרים לנו ליצור אסינכורניות בקוד ולקרוא לחלקים ממנו ברגע שבו פעולה כלשהי תסתיים ובינתיים להמשיך עם שאר הקוד.
אז מה זה JavaScript Promise?
Promise מאפשר לנו להמתין לפעולות שאנחנו לא בהכרח יכולים לדעת מתי יסתיימו והאם יצליחו או יכשלו. למשל, נסו לדמיין פונקציה שמקבלת תמונה ואמורה לעבד אותה, דבר שלכאורה יכול לגזול זמן ומשאבים מהמכונה עליה הקוד רץ, בעבר היינו מעבירים לאותה פונקציה פונקציות נוספות שמשמשות כ- callbacks למצבים של הצלחה או כישלון של הקוד, אך עם Promise נוכל לעשות זאת בצורה יותר אלגנטית וברורה, למעשה בכל קריאה ל- Promise נשתמש במתודות .then() & .catch() לכשה- Promise יצליח או יכשל בהתאמה.
דוגמא נוספת ויותר נפוצה לשימוש ב- Promise היא המתנה לקריאה מרוחקת לשרת, כלומר, במידה ויוזר נרשם למערכת והרגע שלחנו את פרטיו לשרת במטרה שישמרו בבסיס הנתונים, לא נרצה לתת לו הרגשה של חוסר ודאות במיוחד בעידן בו UI/UX הוא מדד מאוד חשוב לשביעות רצון הגולש, ולכן נציג לו מעין ספינר או אינדיקטור אחר שמצביע על כך שמתבצעת כרגע פעולה וברגע שהיא תסתיים נוביל אותו ליעד הבא שלו או שנודיע לו שחלה שגיאה ועליו לנסות שוב במצב בו הקריאה נכשלה.
שימו לב Promise לא נתמך בדפדפן אקספלורר
איך זה נראה תכלס?
נוכל לחזור לדוגמא הקודמת שלנו על פונקציה שמבצעת עיבוד תמונה לפני עידן ה Promises
function generateImage(img, onSuccess, onFailure) {
// Do some stuff with the img parameter
...
if (newImg) {
onSuccess(newImg);
} else {
onFailure('Send failure message');
}
}
generateImage(image, doOnSuccessFunc, doOnFailFunc);
לאחר שנשתמש ב- Promise זה יראה כך:
function generateImage(img) {
return new Promise((resolve, reject) => {
// Do some stuff with the img parameter
...
if (newImg) {
resolve(newImg);
} else {
reject('Send failure message');
}
})
}
generateImage(image)
.then((result) => {
// do somthing on success
})
.catch((result) => {
// do somthing on failure
});
מה הבטוחות?
- הפונקציות שנעביר ל- then או catch יקראו גם לאחר ההצלחה או הכישלון של הפעולה האסינכרונית שעליה אחראי אותו Promise.
- ניתן להוסיף כמה וכמה callbacks דרך פונקציית then, הם יקראו לפי הסדר שבו כתבנו אותם.
- הפונקציות שהעברנו ל- then יקראו רק לאחר הסיום ריצה של ה- event loop.
שרשור Promises
כמו שניתן לראות ההפרדה שנוצרת לנו בין הצלחה וכישלון מאפשרת להפריד את הלוגיקה שאמורה לקרות בשני המצבים ולנהל אותם בצורה נכונה ונוחה יותר, זה בא עוד יותר לידי ביטוי בדוגמא הבאה שממחישה פרמידת callbacks שגורמת לקוד להסתעף ולהסתבך ובעצם להיות קשה יותר להבנה וניהול.
function doStuff(param, callback) { /* ... */ }
function doMoreStuff(param, callback) { /* ... */ }
function doEvenMoreStuff(param, callback) { /* ... */ }
function generateImage(img, onSuccess, onFailure) {
doStuff(param, function() {
doMoreStuff(param2, function() {
doEvenMoreStuff(param3, function() {
...
if (newImg) {
onSuccess(newImg);
} else {
onFailure('Send failure message');
}
})
})
})
}
generateImage(image, doOnSuccessFunc, doOnFailFunc);
בעולם של Promises בהנחה שכל פונקציה פנימית מחזירה Promise ניתן לשרשר פונצקיות then שחוזרות מכל Promise וזה נראה כך:
function doStuff(param) { return new Promise((resolve, reject) => { /* ... */ }) }
function doMoreStuff(param) { return new Promise((resolve, reject) => { /* ... */ }) }
function doEvenMoreStuff(param) { return new Promise((resolve, reject) => { /* ... */ }) }
function generateImage(img, onSuccess, onFailure) {
return new Promise((resolve, reject) => {
doStuff(param)
.then(() => {
return doMoreStuff(param2);
})
.then(() => {
return doEvenMoreStuff(param3);
})
.then(() => {
// Do something on final success
resolve()
})
.catch(() => {
// Do something on any failure
reject()
});
});
}
generateImage(image)
.then(() => { /* do something when finished successfully */ });
.catch(() => { /* do something when error occured */ });
פונקציית finally
פונקציה נוספת ומאוד שימושית הניתנת לקריאה לאחר החזרת Promise היא .finally() שבעצם עוזרת לנו לבצע פעולה כאשר ה-Promise שלנו סיים, או במצב של הצלחה או כישלון, וזה נראה כך:
generateImage(image)
.then(() => { /* do something when the call was successfull */ });
.catch(() => { /* do something when error occured */ });
.finally(() => { /* do something when the promise was finished with success or faliure */ });
מה זה async/await ואיך זה קשור ל- Promise?
אז למעשה הכרנו לראשונה את התחביר שנקרא async/await ב- ECMAScript 2017, כך שהוא מתקמפל ל- Promise כדי שדפדפנים בגירסאות ישנות שכן תומכים ב- promises יוכלו להבין מה ניסינו לעשות.
לצורך ההדגמה נוכל לבחון את הפונקציות הבאות:
function getPromise(foo) {
return new Promise((resolve, reject) => {
if (foo === true) {
resolve('Promise was resolved');
} else {
reject('Promise was rejected');
}
});
}
async function asyncFunc() {
let message;
try {
// will set message to 'Promise was resolved' in case of success
message = await getPromise(true);
} catch (errorMessage) {
// will set message to 'Promise was rejected' in case of failure
message = errorMessage;
}
// will log the recieved message
console.log(message);
}
כמו שכבר ראינו בדוגמאות קודמות, נתונה לנו הפונקציה getPromise שמחזירה Promise, בנוסף יש עוד פונקציה שקוראת לה וממתינה לתשובה שחוזרת, במידה וה- Promise רץ בהצלחה נקבל את ה- message שמודיע על הצלחה, במצב של כישלון נקבל את הודעת השגיאה ולתוך הבלוק של ה-catch, לבסוף נדפיס את משתנה ה- message שלנו.
למעשה את אותה פונקציה היינו מממשים בצורה הבאה לפני עידן ה- async/await:
function asyncFunc() {
let message = null;
getPromise(true)
.then(msg => message = msg)
.catch(errorMessage => console.error(errorMessage))
.finally(errorMessage => console.log(message));
}
בחלק השני של סדרת המאמרים על Promise נרחיב יותר על פונקציות נוספות בהם נוכל להשתמש כדי לייעל את השימוש ב- promises.