callback היא פונקציה שמוסרים לפונקציה אחרת
פונקציות ב-JavaScript הן ערכים. אפשר לשמור אותן במשתנים, לשים אותן במערכים, והכי חשוב כאן, להעביר אותן כארגומנטים. כשמעבירים פונקציה לפונקציה אחרת כדי שזו תקרא לה מאוחר יותר, הפונקציה שהעברתם היא callback.
ל-greet לא אכפת מה formatter עושה, והיא גם לא יודעת. היא פשוט קוראת לה עם שם ומשתמשת בתוצאה. אתם בוחרים את ההתנהגות כשאתם מעבירים callbacks שונים. הגמישות הזו היא כל הסיבה לקיומם של callbacks.
callbacks סינכרוניים רצים עכשיו
לא כל callback הוא אסינכרוני. הרבה ממתודות המערך שאתם כבר משתמשים בהן מבוססות על callbacks, והן מריצות את ה-callback באופן סינכרוני, לפני שהקריאה החיצונית חוזרת:
map, filter ו-reduce מקבלות כולן callback ומפעילות אותו פעם אחת לכל איבר, מיד. עד ש-map חוזרת, כל הקריאות ל-callback כבר קרו. שום דבר לא ממתין בתור.
זה דפוס פשוט של higher-order function: "הנה עבודה, הנה איך לעשות אותה, תן לי את התוצאה". בלי שום event loop.
callbacks אסינכרוניים רצים אחר כך
כשאנשים אומרים "callbacks" הם בדרך כלל מתכוונים לסוג האסינכרוני. מוסרים פונקציה ל-API שלוקח זמן, טיימר, בקשת רשת, קריאת קובץ, וה-API קורא בחזרה לפונקציה שלכם כשהעבודה מסתיימת.
סדר הפלט: before, after, ואז timer fired שנייה אחר כך. setTimeout לא עוצר את התוכנית. הוא מוסר את ה-callback לסביבת הריצה, חוזר מיד, ושאר הסקריפט ממשיך. שנייה אחר כך ה-event loop אוסף את ה-callback ומריץ אותו.
הצורה הזו של "לחזור עכשיו, לקרוא בחזרה אחר כך" היא המודל המחשבתי לכל API של callbacks אסינכרוניים ב-JavaScript, מ-addEventListener ועד ממשקי הקבצים הישנים של Node.js.
מוסכמת error-first (ב-Node.js)
לפני שהיו promises, Node.js התיישרה סביב צורה מסוימת של callback: הארגומנט הראשון הוא שגיאה (או null), והשאר הם התוצאה בפועל. עדיין תיתקלו בה בקוד ישן ובחלק מהספריות.
הקורא בודק קודם את err ויוצא מוקדם אם הוא truthy. רק אז הוא סומך על התוצאה. זו מוסכמה, השפה לא אוכפת אותה, אבל ברגע שתראו את החתימה (err, result) => ... תזהו אותה בכל מקום.
Callback Hell
הצרות מתחילות כשצעד אסינכרוני אחד תלוי בתוצאה של צעד אחר. כל callback צריך להיות מקונן בתוך הקודם, והקוד הולך ונכנס פנימה כמו מדרגות:
זו "פירמידת האבדון" המפורסמת, או callback hell. כמה דברים הופכים אותה לכואבת:
- זרימת הבקרה מזגזגת במקום להיקרא מלמעלה למטה.
- כל רמה חוזרת על אותו קוד שגרתי של
if (err) return .... - חריגה שנזרקת מ-callback אחד לא מתפשטת לחיצוניים: צריך לטפל בשגיאות בכל שכבה.
- ריפקטורינג אומר להזיח מחדש את כל הבלוק.
אפשר לשטח את זה קצת על ידי הוצאת פונקציות עם שם, אבל הבעיה המרכזית, שהרכבה של פעולות אסינכרוניות עם callbacks גולמיים מסורבלת, לא נעלמת. זו הבעיה ש-promises נועדו לפתור.
שתי מלכודות שכדאי להכיר
אל תקראו ל-callback בטעות. כשמעבירים callback, מעבירים את הפונקציה עצמה, לא את התוצאה של קריאה לה.
שימו לב ל-this. אם ה-callback הוא פונקציה רגילה שמשתמשת ב-this, הערך של this תלוי באופן שבו ה-callback נקרא, ולא במקום שבו הוגדר. arrow functions עוקפות את הבעיה כי הן יורשות את this מהסביבה העוטפת:
בדיוק מהסיבה הזו arrow functions הן בחירת ברירת המחדל ל-callbacks שנכתבים במקום.
Callbacks מול Promises
callbacks עדיין מופיעים ב-APIs סינכרוניים (map, forEach, sort), ב-event listeners (element.addEventListener("click", ...)) ובהוקים ברמה נמוכה של סביבת הריצה. לעבודה אסינכרונית שמפיקה תוצאה אחת, האקוסיסטם עבר כמעט לגמרי ל-promises.
ההשוואה המהירה:
- Callbacks: ישירים ומינימליים, אבל מתחברים זה לזה גרוע. הטיפול בשגיאות ידני בכל צעד.
- Promises: ערך שמייצג תוצאה עתידית. משרשרים אותם עם
.then(), מטפלים בשגיאות פעם אחת עם.catch(), והפירמידה נעלמת.
עדיין תצטרכו להבין callbacks: promises בנויים עליהם, והם נמצאים בכל מקום בקוד מונחה אירועים. אבל היום כמעט לא כותבים APIs אסינכרוניים חדשים עם callbacks גולמיים.
הבא בתור: Promises
promises לוקחים את הרעיון של "תעשה את זה כשההוא מוכן" ועוטפים אותו באובייקט שאפשר להעביר, לשרשר ולהרכיב. זה העמוד הבא, והוא הגשר אל async/await, הדרך שבה רוב ה-JavaScript המודרני מטפל בעבודה אסינכרונית.
שאלות נפוצות
מה זו פונקציית callback ב-JavaScript?
callback היא פונקציה שמעבירים כארגומנט לפונקציה אחרת, כדי שזו תוכל לקרוא לה מאוחר יותר. setTimeout(() => console.log('hi'), 1000) מעביר arrow function כ-callback: setTimeout שומר אותה וקורא לה כשהטיימר מסתיים. callbacks הם הדרך המקורית שבה JavaScript טיפלה ב'תעשה את זה כשההוא מוכן'.
מה ההבדל בין callbacks סינכרוניים לאסינכרוניים?
callback סינכרוני רץ מיד, במהלך הקריאה שקיבלה אותו: [1, 2, 3].map(x => x * 2) מפעיל את ה-callback שלוש פעמים לפני ש-map חוזרת. callback אסינכרוני נשמר ונקרא מאוחר יותר, אחרי שאירוע כלשהו קורה: setTimeout, fs.readFile ו-event listeners של ה-DOM עובדים כולם כך. callbacks אסינכרוניים לא חוסמים את שאר הקוד.
מה זה callback hell ואיך נמנעים ממנו?
callback hell הוא צורת הפירמידה שמתקבלת כש-callbacks אסינכרוניים תלויים זה בזה ומקוננים כמה רמות לעומק. קשה לעקוב בה אחרי זרימת הבקרה ואחרי הטיפול בשגיאות. הפתרון הוא promises עם שרשראות .then(), או עדיף, async/await: שניהם משטחים את הפירמידה בחזרה למשהו קריא.