שפת javascript היא שפה שבבסיסה פועלת על thread אחד בלבד, אך מסוגלת לבצע פעולות סינכרוניות ואסינכרוניות כאחת. אם במשפט הקודם היה איזשהו מושג שאתם לא מכירים, אני ממליץ לכם לעבור לקרוא את הפוסט שלי על event loop שם אני מסביר את כל המושגים האלה לעומק.
הבעיה בקוד שמשלב גם פעולות סינכרוניות וגם פעולות אסינכרוניות היא שלפעמים הקוד יכול להסתבך. בעבר היה נהוג להשתמש ב call back בשביל להתנהל עם קוד אסינכרוני (call back = פונקציה שמקבלת פונקציה כפרמטר ויכולה להפעיל את הפונקציה שהיא קיבלה בשלב מאוחר יותר) אבל כל מי שהתנסה call backs יודע שמהר מאוד עלולים להגיע למה שמתכנתים אוהבים לכנות "call back hell". בלאגן שלם שקשה מאוד לעקוב אחריו ולהבין מה קורה ומתי. מה שמסבך משמעותית את תהלי הפיתוח ומקשה עליו.
Promise
למרבה המזל בשנת 2012 נכנס לתוך javascript שחקן חדש ושמו "promise". בעזרת הpromise ניתן לכתוב קוד אסינכרוני בצורה הרבה יותר נוחה וקלה להבנה.
בואו ניתן דוגמא.
כך היינו כותבים פונקציה אסינכרונית המשתמשת ב call back: (השתמשתי בtimeout כמדמה אסינכרוניות)
function functionWithCallBacK (callback){
console.log("entered function")
setTimeout(callback, 2000)
}
functionWithCallBacK(()=>{
console.log("hello")
})
אם הייתם מריצים את הקוד הזה הייתם רואים מודפס בקונסול "entered function" ורק אחרי שתי שניות הפונקציה functionWithCallBack היתה מפעילה את ה call back שהיא קיבלה, ורק אז הייתם רואים מודפס בקונסול "hello".
שימו לב שאנחנו חייבים להעביר את הcall back כפרמטר לתוך הפונקציה הראשונה על מנת שנוכל להריץ את הקוד רק אחרי שמסתיימת הפעולה האסינכרונית (כמו קריאה לשרת לדוגמא).
בעולם אמיתי היה מאוד קשה לנהל את הקוד בצורה כזו, כאשר פונקציות יכולות לקבל הרבה פרמטרים, והכל הופך לסלט אחד גדול.
עכשיו שימו לב איך היינו מגיעים לאותה תוצאה בעזרת promise:
const promise = new Promise((resolve, reject) => {
setTimeout(resolve, 2000)
})
בתוך מסגרת של Promise ישנן שתי פונקציות שזמינות לנו: resolve ו- reject.
"resolve" – היא פונקציה שמפעילים אותה כדי לסיים את ה- promise בהצלחה, ו "reject" היא פונקציה שמפעילים אותה כדי להכשיל את ה-promise.
כדי להפעיל את ה-promise כל שיש לעשות הוא:
const readFile = new Promise((resolve, reject)=> {
setTimeout(()=>{
const file = /* asynchronous action like reading a file*/
resolve(file)
},2000)
})
readFile.then((file) => {
console.log("we got the file!")
})
"then" היא מתודה הקיימת בתוך promise, ומופעלת כאשר ה-promise הסתיים בהצלחה. ז"א שאם בתוך ה-promise הופעלה מתודת resolve, אזי מחוץ ל-promise תופעל מתודת then .
שימו לב ש-then מקבלת כארגומנט את התוצאה של ה-promise.
מה קורה אם Promise נכשל?
אם לדוגמא ניסינו לקרוא קובץ, והקריאה נכשלה. במקרה כזה לא נפעיל את resolve אלא דווקא את reject, ואז מתודת then לא תופעל.
ל-promise יש עוד מתודה, ושמה "catch".
catch נדלקת כאשר הpromie נכשל ומופעלת בתוכו מתודת reject.
כמו then, גם catch מקבל כארגומנט את מה שמעביר לו reject וכך אנחנו מקבלים בתוך catch את הerror שהתרחש בתוך הpromise:
const readFile = new Promise((resolve, reject)=> {
const error = ''/* asynchronous action fail*/
reject(error)
})
readFile.catch((error) => {
console.log("we got an error...")
})
אחרי שה-promise מסתיים, לטוב או לרע (בין אם הסתיים כ-reject או כ-resolve) נדלקת עוד פונקציה אחת נוספת, והיא "finally".
finaly היא עוד פונקציה שאנחנו יכולים להפעיל בכל סיום של promis. לדוגמא:
const readFile = /*a new promise*/
readFile.then((file) => {
console.log("we got the file!")
}).finally(()=>{
console.log("promise has ended")
})
הכוח האמיתי של promise
בעיני היכולת המשמעותית ביותר ש-promise מביאה למשחק היא היכולת להריץ מספר פעולות אסינכרוניות במקביל.
תחשבו על מצב שבו אנחנו צריכים לפנות לעשר שרתים שונים במקביל כדי למשוך מהם את הנתונים הנדרשים בשביל האפליקציה שלנו. אם היינו פונים קודם לשרת מספר 1, מחכים לתשובה, ואז פונים לשרת מספר 2, מחכים לתשובה, וכן הלאה, כל התהליך הזה היה לוקח המון המון זמן…
בעזרת promise אנחנו יכולים בקלות לבצע מספר פניות מקבילות לשרתים שונים. יש כמה דרכים לבצע את זה, ואנחנו נתחיל ב- promise.all.
const serverUrls = [
"server url 1",
"server url 2",
"server url 3",
"server url 4",
"server url 5",
]
const promises = Promise.all(serverUrls.map(url => {
// call the server url and return the value
}))
promises.then(results =>{
results.forEach(res =>{
console.log(res)
})
})
בדוגמא הזו יש מערך של חמש כתובות של שרתים שונים, ובעזרת promise.all אנחנו עוברים על כל המערך ובכל פעם פונים לשרת אחר, ומחזירים promise כתשובה.
"promises" אם כן, יהיה מערך שכל אחד מהאלמנטים בתוכו הוא promise.
מתודת then תופעל רק כאשר כל הקריאות לשרת יסתיימו, ובתוך promises יהיו חמש "promis".
המשתנה "results" שנמצא בתוך ה-then, יהיה מערך של חמש התשובות שהגיעו מהשרתים השונים.
החיסרון הגדול ביותר של שיטת promise.all היא שאם אחת מהפעולות האסינכרוניות שהיא מבצעת, למשל קריאה לאחד השרתים, נכשלת, אזי כל הpromise.all נכשל. ז"א אנחנו לא נקבל תשובה מאף אחד מהשרתים.
אך למרבה המזל, לpromise יש גם מתודה שקוראים לה Promise.allSettled:
const serverUrls = [
"server url 1",
"server url 2",
"server url 3",
"server url 4",
"server url 5",
]
const promises = Promise.allSettled(serverUrls.map(url => {
// call the server url and return the value
}))
promises.then(results =>{
results.forEach(res =>{
console.log(res)
})
})
כאשר משתמשים ב promise.allSettled, גם אם אחת הפעולות האסינכרוניות נכשלת, שאר הפעולות מוגנות ותלויות רק בעצמן.
ז"א ש-results יהיה מערך המכיל את כל התוצאות של כל הפעולות האסינכרוניות שהיה לנו בתוך promise.allSettled, פעולה שהצליחה יהיה לה סטטוס fulfilled ופעולה שנכשלה יהיה לה סטטוס rejected.
זהו זה, עד כאן להיום לגבי promises, ואף על פי שיש עוד איזה מתודה או שניים לדבר עליהן, נסתפק בזה לבנתיים.
מקווה שנהנתם 🙂