על פעולות סינכרוניות, אסינכרוניות ו-Async/Await, Promises, Callbacks ב-JavaScript
בפוסט זה אסביר לכם על פעולות סינכרוניות, אסינכרוניות ו-Async/Await, Promises, Callbacks ב-JavaScript.
אין ספק שמדובר בנושא מעט מורכב ומבלבל אבל הפוסט הבא יעשה לכם קצת סדר.
סינכורני/אסינכרוני?
אז לפני שנתחיל לצלול לדבר על Callbacks, Promises, ו-Async/Await ב-JavaScript, בואו נתחיל מהבסיס ונבין מה זה בכלל קוד סינכרוני ומה זה קוד אסינכרוני בקצרה.
סינכורני משמעותו פעולה אחת פעם אחת בכל זמן, הפעולה הבאה לא תתחיל לפני שקודמתה תסתיים.
אסינכרוני משמעותו, מספר פעולות במקביל, כמו קריאת API למשל, פקודה אחת לא "תוקעת" פקודה אחרת אלא הכל מתבצע במקביל.
הנה פקודות סינכרוניות לדוגמה, אם נבצע מספר פקודות console.log במקביל:
console.log('Hello #1');
console.log('Hello #2');
console.log('Hello #3');
זה הפלט שנקבל:
Hello #1
Hello #2
Hello #3
כלומר, כמצופה, שורה אחר שורה, לפי הסדר, לפי סדר ההרצה.
כאשר מדברים על פעולות אסינכרוניות, לרוב לא תהיה לנו יכולות לדעת מתי הפעולה תסתיים ותחזיר לנו את המידע הרצוי, למשל, בעת קריאה ל-API מסויים שבה אנו תלויים בזמן תגובות השרת, שליפת הנתונים מממסד הנתונים והחזרתם. זה יכול לקחת 100ms וזה יכול לקחת 500ms ואפילו שנייה שלמה.
מן הסתם, נרצה לעשות שימוש במידע שמתקבל ולכן עלינו "להמתין" למידע.
האפשרות הראשונה היא להשתמש ב-callback, כלומר, פונקציה שמתקבלת כארגומנט לפונקציה אחרת, דוגמה:
callback
const greetingFunc = greetingText => console.log(`Hi ${greetingText}`);
const greetingInfoFunc = (name, dayTime, callbackFunc)=> {
const greetingText = `${name}, Good ${dayTime}!`;
callbackFunc(greetingText);
}
greetingInfoFunc('John', 'Morning', greetingFunc);
וזה מה שיתקבל:

מה שעשיתי כאן זה בסך הכל ליצור פונקציה שמדפיסה ברכה כלשהי (greetingFunc) , ופונקציה נוספת שמקבלת מידע נרחב יותר על הברכה וקוראת לפונקציה שמדפיסה את הברכה עצמה (greetingInfoFunc).
מה שמעניין בדוגמה הפשוטה הזו הוא שהפונקציה greetingInfoFunc קיבלה כארגומנט פונקציה אחרת, greetingFunc.
callback hell
אבל מה יקרה אם נתחיל להשתמש ב-callback פעם אחר פעם? נגיע לאחד מהמצבים הכי פחות רצויים – קוד מבולגן ולא קריא.
שימוש "גרוע" ב-callback נקרא callback hell, אם אתם מפתחי JavaScript, סביר להניח שאתם כבר מכירים את המונח הזה.
אז מהו callback hell? הנה דוגמה אבל אני מזהיר מראש, זה לא קל לצפייה:
getData(function(result1){
getMoreData(result1, function(result2){
getMoreData(result2, function(result3){
getMoreData(result3, function(result4){
console.log(result4);
});
});
});
});
ניתן לראות שמתקבל קוד עם כל כך הרבה הזחות (והיד עוד נטויה) ומאוד קשה להבין מה קורה שם.
מה הפיתרון? Promises.
מה זה Promises?
אז מה זה promises? כשמו כן הוא – הבטחה.
ההבטחה לבצע משהו בעתיד כאשר יקרה משהו.
promises עוזרים לנו לבצע פעולות אסינכרוניות בזו אחר זו בצורה נעימה יותר לעין.
הרבה ספריות JavaScript שאתם וודאי מכירים משתמשים ב-promises, למשל axios.
ל-promises יש 3 מצבים:
pending – מצב ראשוני והתחלתי, ההבטחה לא מומשה ולא נדחתה, היא "ממתינה".
fulfilled – ההבטחה מומשה, הפעולה שהתבצעה הסתיימה בהצלחה, ממומש על ידי resolve (בעוד רגע ארחיב על כך).
rejected – ההבטחה נכשלה, ממומש על ידי reject (בעוד רגע ארחיב גם על כך).
דוגמה לשימוש ב-promises:
const promise1 = new Promise((resolve, reject) => {
if(true) resolve('Success!');
else reject(new Error('Error!'));
});
promise1.then(res => console.log(res))
.catch(err => console.log(err.message));
ניתן לראות שה-promise קיבל פונקציה עם שני ארגומנטים, resolve ו-reject.
בתוך הפונקציה, ביצענו בדיקה טיפשית למדי והחזרנו הצלחה (resolve) ולא כישלון (reject)
לאחר מכן, עלינו להוציא את ההבטחה על פועל ולכן השתמשנו ב-then למקרה של הצלחה ו-catch במקרה של כישלון.
בואו נצלול לדוגמה מעט יותר מוחשית ואמיתית שבה אנחנו משרשרים שתי הבטחות:
const getUserData = new Promise((resolve, reject) => {
const userData = {userId: 173, name: 'John', emailConfirmation: true, emailConfirmationDate: "17/04/2020"} // getUserDataFromApi(173);
if(userData) resolve(userData);
else reject(new Error('Error'));
});
const checkEmailConfirmation = user => {
if(user.emailConfirmation) return Promise.resolve(user.emailConfirmationDate);
}
getUserData
.then(checkEmailConfirmation)
.then (res => {
console.log(res);
})
.catch( err => {
console.log(err);
});
בדוגמה זו רציתי תחילה לקבל אובייקט של user באמצעות קריאה ל-api, כמובן שבדוגמה פשוט הכנתי אובייקט כדוגמה.
אתם יכולים לראות שחיברתי בין שני ה-promises, תחילה התבצעה ה-promise הראשון בשם getUserData, ורק לאחר שחזר המידע באמצעות resolve, ה-promise השני בשם checkEmailConfirmation התבצע עם המידע שהתקבל מה-promise הראשון.
אגב, במידה והיה מתקבל reject מתישהו, למשל ב-promise הראשון, היינו מקבלים שגיאה כפי שהגדרנו ב-catch.
מה יקרה אם נרצה לבצע כמה promises ביחד ולבצע פעולה רק לאחר שכולם הסתיימו? בואו נגדיר 3 הבטחות:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello from promise #1');
}, 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello from promise #2');
}, 2000);
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Hello from promise #3');
}, 3000);
});
כל אחד מהם יסיים בזמנו מאחר ועשיתי שימוש ב-setTimeout.
הרי אם נעשה את הדבר הבא:
promise1
.then(res => console.log(res))
.catch(err => {
console.log(err);
});
promise2
.then(res => console.log(res))
.catch(err => {
console.log(err);
});
promise3
.then(res => console.log(res))
.catch(err => {
console.log(err);
});
נקבל את זה:

Promise.all
בדיוק בשביל זה יש לנו את Promise.all, פשוט מעבירים מערך של על ה-promises וברגע שכולם יסיימו נקבל את המידע של כולם:
Promise.all([promise1, promise2, promise3])
.then(res => console.log(res))
.catch(err => {
console.log(err);
});
וזה מה שהתקבל:

Promise.race
במידה ונרצה לקבל רק את המידע שהתקבל מה-promise הראשון, נשתמש ב-Promise.race:
Promise.race([promise1, promise2, promise3])
.then(res => console.log(res))
.catch(err => {
console.log(err);
});
Async/Await
אז מה זה Async/Await?
חשוב לציין – Async/Await לא מחליף את ה-promises, בסך הכל מדובר בצורת כתיבה נוחה יותר למימוש promises מאשר ה-then המשורשר (syntactic sugar).
מדובר על פונקציה שבתחילה אנו קובעים שהיא פונקציה מסוג async על ידי כתיבה המילה "async" לפני הכרזתה ובתוך הפונקציה, לפני מימוש של כל Promise אנו "מחכים" ל-Promise באמצעות המילה "await".
נחזור לדוגמה הקודמת שבה היו לנו 2 הבטחות אך הפעם במקום להשתמש ב-then, נשתמש ב-async/await:
const getUserData = new Promise((resolve, reject) => {
const userData = {userId: 173, name: 'John', emailConfirmation: true, emailConfirmationDate: "17/04/2020"}
if(userData) resolve(userData);
else reject(new Error('Error'));
});
const checkEmailConfirmation = user => {
if(user.emailConfirmation) return Promise.resolve(user.emailConfirmationDate);
}
async function checkUserEmailConfirmation() {
try {
const userData = await getUserData;
const userEmailConfirmation = await checkEmailConfirmation(userData);
console.log(userEmailConfirmation);
} catch(err) {
console.log(err);;
}
}
checkUserEmailConfirmation();
שימו לב לפונקציה checkUserEmailConfirmation, לפני כתיבת שמה של הפונקציה הצהרנו עלייה כפונקציה async ובתוך הפונקציה, לפני כל קריאה ל-promise, חיכינו לו על ידי await.
כמובן שיש שימוש ב-try catch בתוך הפונקציה ואם אתם לא מכירים, אני ממליץ לכם לקרוא את הפוסט שכתבתי על כך: טיפול בשגיאות ב-JavaScript באמצעות try catch
כמובן ש-async/await היא הדרך הנכונה והנפוצה כיום לממש promises מאחר והיא הכי נוחה לכתיבה ולקריאה.
סיכום
אין ספק שפעולות אסינכרוניות הן נושא די מבלבל ב-JavaScript, ההבנה של נושא זה כרוכה בהבנה של 3 נושאים די שונים אך דומים: Callbacks, Promises, Async/Await ומטרת הפוסט הייתה להסביר נושאים אלו בצורה פשוטה ומובנת עד כמה שאפשר 😉
מי שמעוניין לקרוא בהרחבה על promises, אני ממליץ לקרוא את העמוד הבא ב-MDN.
בהצלחה!
נהנת ממאמר זה? הירשם לרשימת התפוצה וקבל עדכונים על מאמרים חדשים!
רק רגע! :)
כשאני לא כותב פוסטים ב-CodeBrain אני מספק שרותי פיתוח, ייעוץ והדרכה.
אם נראה לך שאני האיש המתאים עבורך, כדאי שנדבר :)
הסבר מעולה!!! הלוואי שיהיו עוד כאלה.
תודה רבה!
הסבר בהיר ופשוט. תודה!
קצר וברור ממש נהנתי! בהצלחה!