בחלק זה של המדריך נכיר את Promise או "הבטחה" בתרגום חופשי – אובייקט בשפת JavaScript אשר מסייע לנו לבצע פעולות אסינכרוניות
בחלק הקודם של המדריך למדנו על ההבדל שבין תכנות סינכרוני לתכנות אסינכרוני על ידי שימוש בדוגמאות קוד.
בדוגמא שבה השתמשנו, הצגנו מימוש של setTimeout וכיצד נוכל להשתמש בו על מנת להדגים מהי אסינכרוניות.
כמובן שבשפת JavaScript יש מגוון של כלים שמאפשרים לנו פעולות אסינכרוניות – EventListener הוא דוגמא אפשרית –
כאשר אנו משתמשים ב-EventListener על מנת להאזין לאירוע מסוים למשל,
הוא מאזין לאותו האירוע שהוגדר לו וממשיך את פעולתו רק כאשר חל אותו האירוע,
בזמן המתנה זו התוכנית רצה כרגיל ולא קופאת בהמתנה לאותו אירוע.
במאמר זה נכיר אובייקט מיוחד שנועד לאפשר לנו פעולות אסינכרוניות – Promise.
מחלקת Promise
Promise מסמל את ההצלחה הסופית (או כישלון) של פעולה אסינכרונית.
ראשית נתבונן בתחביר של Promise:
הבנאי (constructor) של מחלקת Promise מקבל פונקציה כפרמטר, פונקציה זו נקראת ה-Executor.
ה-Executor עצמו מקבל 2 ארגומנטים – resolve ו-reject.
הלוגיקה עצמה של ה-Promise תיכתב בתוך פונקציה זו – היא תרוץ באופן אוטומטי כשניצור את מופע ה-Promise.
הבנאי של המחלקה מחזיר לנו אובייקט Promise,
כאשר תפקידו של ה-Executor זה לנהל את הפעילות האסינכרונית.
תפקידו של אובייקט ה-Promise המוחזר שלנו יהיה להודיע לנו שההרצה התחילה, הסתיימה או נכשלה.
לאובייקט זה יש את המאפיינים הבאים:
- state – מאפיין זה יכול להחזיק אחד מתוך הערכים הבאים:
- pending – זה יהיה ה-state מרגע שה-Executor החל לרוץ ובזמן הריצה
- fulfilled – כאשר פעולת ה-Executor תמה, זה יהיה ה-state
- rejected – במקרה שה-Promise נכשל או נדחה
- result – מאפיין זה יכול להחזיק אחד מתוך הערכים הבאים:
- undefined – זה יהיה הערך כל עוד ה-state במצב pending.
- value – כאשר (value)resolve נקרא לפעולה
- error – כאשר (error)resolve נקרא לפעולה
שימו לב כי אין לנו גישה לערכים אלו, אך נוכל לבחון אותם במצב Debug.
כפי שהוסבר ה-state (מצב) של האובייקט יכול להיות pending, fulfilled או rejected,
כאשר מצב של בקשה שאושרה או נדחתה נקרא – "settled".
שימו לב לקטע הקוד הבא שבו נדגים בקשה ומצב שבו היא נקלטה והחזירה תשובה, לעומת מצב שבו הבקשה נדחית:
let resolvedPromise = new Promise(function(resolve, reject) { resolve('Success'); }); let rejectedPromise = new Promise(function(resolve, reject) { reject(new Error('rejected, sorry')); });
אם תנסו להריץ קוד זה הוא יכשל משום שזרקנו שגיאה, הוא רק בא להמחיש לנו את אופן פעולתן של המתודות resolve ו-reject –
הלא הן הפונקציות שנשלחו כארגומנטים ל-Executor.
לעומת זאת, שימו לב למצב הבא:
let promise = new Promise(function(resolve, reject) { resolve('this will be resolved'); reject(new Error("this will be ignored")); });
ה-Executor מריץ את ה-resolve או ה-reject הראשון שמופיע ב-scope הפונקציה,
במידה וקיימים עוד כאלו בתוך scope הפונקציה – ה-Executor יתעלם מקיומם ולא יריץ אותם,
ומסיבה זו בדיוק קטע הקוד האחרון ירוץ מבלי לקרוס.
Promise Handling
כעת, לאחר הבנו את הקונספט באופן כללי, נראה כיצד עלינו לטפל בבקשות.
כפי שהוסבר, תפקידו של ה-Executor הוא לבצע פעולה (אסינכרונית בד"כ),
כלומר שנוכל להיעזר בפונקציה ייעודית שמשתמשת בתוצאה של ה-Promise,
ונצטרך להודיע לה כאשר ה-Executor סיים את פעולתו. (resolve או error).
נוכל להיעזר במתודות ()then() ,catch ו-()finally על מנת ליצור חיבור בין ה-Executor לבין פונקציות ייעודיות על מנת לסנכרן אותן כאשר נשלח resolve or reject.
()then
במתודה זו נוכל להשתמש על מנת לטפל במצב של resolve או reject.
then מקבלת שני פרמטרים שהם פונקציות.
לרוב, נקרא ל-then מתוך הפונקציה הייעודית שניזונה מה-Executor,
משום שכפי שהוסבר, הפונקציה הייעודית אמורה לטפל במצב זה.
שימו לב לתחביר של then שישפוך מעט אור על הנושא:
אם היינו מעוניינים בתוצאות מוצלחות בלבד, היינו מוותרים על הפונקציה השנייה,
או ההיפך, לו היינו מעוניינים לבדוק רק מצב של כישלון היינו משמיטים את הראשונה, אך נאלצים לציין null במקום:
promise.then( null, (error) => { console.log(error); } );
כעת, לאחר שהבנו את הקונספט, נמשיך אל קטע הקוד הבא שבו ננסה לשלוח בקשת Get ל-api מסוים.
https://pokeapi.co/ הוא api) – application programming interface) שניתן לשלוח לו בקשות על מנת לקבל מידע מכל מיני סוגים על פוקימונים.
מה שאנחנו הולכים לעשות זה לבקש מידע על 20 פוקימונים.
ראשית ניצור פונקציה אשר תכיל Promise שה-Executor שלו מכיל את הקוד שמנהל את שליחת הבקשה לשרת:
function getPromise(url) { let myPromise = new Promise(function (resolve, reject) { let request = new XMLHttpRequest(); request.open("GET", url); request.onload = function () { if (request.status == 200) { resolve(request.response); } else { reject("Error"); } }; request.send(); }); return myPromise; }
כעת ניצור את הפונקציה הייעודית – פונקציה אשר תייבא לנו 20 פוקימונים במידה והבקשה לשרת החזירה לנו resolve:
const pokemonsUrl = 'https://pokeapi.co/api/v2/pokemon?limit=20'; let promise = GETPromise(pokemonsUrl); const promiseHandler = () => { promise.then( (result) => { poksArray = [...JSON.parse(result)['results']] poksArray.forEach(item => console.log(item['name'])); }, (error) => { console.log('Error'); // משום שמדובר בכתובת תקינה, זה לא יקרה }); } promiseHandler(); console.log('\n<<<hello, world!>>>\n\n')
ובכן, בואו ננתח את מה שקרה כאן עכשיו.
בתחילת הסקריפט שלנו יצרנו Promise אש ה-Executor שלו מנסה לשלוח בקשת GET לשרת כלשהו,
פונקציה זו תמתין אם כך שנשלח בקשה לשרת כדי שהיא תוכל להחזיר לנו תשובה (או סירוב).
הפונקציה הייעודית שבנינו מחזיקה משתנה שהערך שלו הוא כתובת ה-url.
השתמשנו אם כך ב-then על מנת שבמידה ויחזור לנו resolve – אז ניקח את התוצאה שקיבלנו ונדפיס אותה לקונסול.
בשורה 15 קראנו לפונקציה הייעודית, ולאחר מכן בשורה 17 הדפסנו הודעת בדיקה לקונסול,
הודעה זו היא למעשה הוכחה לטיעון כי נעשתה כאן פעולה אסינכרונית.
אם תסתכלו היטב, תבחינו שהודעה זו הודפסה לפני שקיבלנו את התוצאות מה-api של הפוקימונים – וזאת למרות שהדפסה זו היא הקריאה האחרונה בתוכנית.
שרשור Promises
הקריאה למתודה ()promise.then תמיד תחזיר לנו Promise,
כאשר הערך של ה-state יהיה pending, והערך של result יהיה undefined.
מצב זה מאפשר לנו לקרוא למתודת ה-then הבאה.
לאחר שמתודת ה-then הראשונה מחזירה לנו ערך – ה-then הבא בתור יכול לקבל אותו, וכן הלאה.
כלומר שצורת שרשור זו מעבירה למטה את אובייקט ה-Promise עד סוף השרשרת, חוליה אחת בכל פעם.
שימו לב לקטע הקוד הבא:
function promiseHandler() { promise.then(result => { let pokemon = JSON.parse(result).results[0].name; return pokemon; }).then(pokemonName => { console.log(pokemonName); }).then(_ => console.log('\n<<<hello, world!>>>\n\n')); } promiseHandler();
מה שעשינו הפעם זה לשרשר 3 מתודות then.
הראשונה החזירה לנו את האובייקט מה-api, השנייה הדפיסה את שמו לאחר שנתקבלה תשובה,
ותיקנו עוול קודם, כאשר מתודת ה-then הבאה הדפיסה לנו את הודעת הבדיקה בצורה מסונכרנת –
כך שהפלט שלה לא הופיע לפני פלט התשובה מה-api:
אם כך, then יכולה להחזיר לנו או ערך או Promise כערך.
שימו לב כי אחד מהערכים שנוכל לקבל מה-api הוא הכתובת של אותו הפוקימון.
אם נרצה להציג דוגמא יעילה יותר אז נשתמש בשרשור של then על מנת:
- שהראשון יחזיר לנו פוקימון בודד, ידפיס לנו את שמו ויחזיר את הכתובת שלו
- השני ידפיס את הכתובת של הפוקימון ויחזיר אותה
- השלישי ידפיס לנו את רשימת היכולות של אותו הפוקימון
function promiseHandler() { promise.then(result => { let pokemon = JSON.parse(result).results[0]; console.log(`pokemon name: ${pokemon.name}`) return pokemon.url; }) .then(pokemonUrl => { console.log(`pokemon url: ${pokemonUrl}`); return GETPromise(pokemonUrl); }) .then(pokemon => { console.log(`\nabilities:\n\n`); JSON.parse(pokemon).abilities.forEach(ability => console.log(ability.ability.name)) }); }
למרות מה שמצטייר מהתוצאה שקיבלנו, קריאות ל-then ברצף אינן מבטיחות לנו שרשור,
מה שגורם לשרשור הוא החיבור ביניהם באמצעות נקודה – …then.then.then.
למעשה אם נפריד בין הקריאות בסגירת וירידת שורה (;), אז נקבל שגיאה.
טיפול ב-Promises מרובים
מעבר למתודות ()then() ,catch ו-()finally יש לרשותנו 6 מתודות נוספות שבהם נוכל להשתמש:
-
()Promise.all
מתודה זו מקבלת אוסף של Promises (מערך למשל) בתור ארגומנט ומוציאה אותם לפועל במקביל:
const squirtle = 'https://pokeapi.co/api/v2/pokemon/squirtle'; const pidgeotto = 'https://pokeapi.co/api/v2/pokemon/pidgeotto'; const metapod = 'https://pokeapi.co/api/v2/pokemon/metapod'; let promise1 = GETPromise(squirtle); let promise2 = GETPromise(pidgeotto); let promise3 = GETPromise(metapod); function promiseHandler() { Promise.all([promise1, promise2, promise3]).then(function(result){ for (let pokemon of result) { console.log(JSON.parse(pokemon).name); }}); }
כפי שניתן לראות שרשרנו את then ל-Promise.all, כך שהבקשות רצו במקביל, וכארגומנטים שלחנו את הפוקימונים שלנו בתוך מערך.
-
()Promise.any
מתודה זו דומה ל-Promise.all בכך שגם היא מקבלת מערך של Promises ומריצה אותם במקביל,
רק שמתודה זו לא ממתינה שכל הבקשות יטופלו, היא מסיימת את פעולתה כאשר אחת מהבקשות תחזיר לנו resolve:
function promiseHandler() { Promise.any([promise1, promise2, promise3]).then(function(result){ console.log(JSON.parse(result)['name']); }); }
כך שאם היינו מנסים להדפיס את השמות של שלושת הפוקימונים באמצעות לולאת forEach כפי שעשינו מקודם – היינו מקבלים שגיאה,
ולכן הפעם ניגשנו ישירות אל ה-result והדפסנו את השם – הפלט היה בהתאם לבקשה הראשונה שהחזירה לנו resolve.
-
()Promise.allSettled
מתודה זו תקבל את כל ה-Promises שנשלח לה במערך,
לאחר שכולם יסיימו את ה-resolve\reject שלהם היא תחזיר לנו מערך של התוצאות.
לכל תוצאה יש מאפיין שהוא המצב (state) – (resolved) fulfilled או rejected.
במקרה שהבקשה תטופל בהצלחה – מאפיין הערך (value) יהיה התוצאה, אחרת תוחזר סיבת השגיאה:
function promiseHandler() { Promise.allSettled([promise1, promise2, promise3]).then(function(result){ for (let pokemon of result) { console.log(JSON.parse(pokemon.value).name); }}); }
כך שהתוצאה תהיה דומה לזו של ()Promise.all.
-
()Promise.race
מתודה אשר ממתינה לבקשה המהירה ביותר (זו שתחזיר resolve או reject ראשונה) ומחזירה את הערך או את השגיאה בהתאם:
function promiseHandler() { Promise.race([promise1, promise2, promise3]).then(function(result){ console.log(JSON.parse(result).name); }); }
מתודות נוספות
המתודות האחרונות שנותרו לנו לבאר הן הפשוטות ביותר:
-
()Promise.resolve() / Promise.reject
()Promise.resolve תסיים בקשה ב-resolve עם ערך שנעביר כארגומנט:
let promise = new Promise(resolve => resolve(value));
()Promise.reject תסיים בקשה ב-reject עם הודעת שגיאה שנעביר כארגומנט:
let promise = new Promise((resolve, reject) => reject(error));
שימו לב שאם היינו מזינים כתובת url אשר איננה תקינה, היינו מקבלים שגיאה.
בשפת JavaScript יש לנו דרכים להתגונן מפני מצבים מסוג זה,
כפי שנוכל לראות בחלק הבא של המדריך שיעסוק בטיפול בשגיאות באמצעות ()then() ,catch ו-()finally.