אסינכרוניות בג'אווה סקריפט - Callbacks

Published on
Authors
  • avatar
    Name
    Yonatan Snir

הקדמה

פני שנתחיל חשוב להבין בכלל מה זה תכנות אסינכרוני. תכנות אסינכרוני מאפשר לנו לכתוב פונקציות שאינן חוסמות את ריצת הקוד (Non Blocking). רגע… מה זה Blocking בעצם?

A function or a code-block is blocking if it has to wait for anything to complete

אתן דוגמה: יש לכם אפליקציית מזג אוויר בפלאפון. כשאתם פותחים את האפליקציה, האפליקציה עולה לפניכם ובינתיים בחלק שאתם אמורים לראות את מזג האוויר מוצגת אנימציה שכותבת לכם "טוען". רק אחרי שהאפליקציה קיבלה את נתוני מזג האוויר מהשרת האנימציה מפסיקה ומספר המעלות מוצג. זה בדיוק אסינכרוניות. האפליקציה עלתה בלי לחכות לנתונים שיגיעו מהשרת. אם הקוד היה סינכרוני או Blocking, האפליקציה הייתה עולה רק אחרי שהנתונים היו מגיעים מהשרת, וזאת חווית שימוש לא טובה שאנחנו מעדיפים להימנע ממנה. עוד דוגמה טובה אפשר לראות באתר של פייסבוק. כשאתם נכנסים לפייסבוק האתר קודם כל עולה ומוצג לפניכם ותוך כדי גלישה, ברקע, ממשיך כל הזמן להיטען ולמלא את המקומות החסרים. אינכם צריכים לחכות שכל האתר יטען במלואו. הכל קורה "תוך כדי תנועה" לחווית משתמש מירבית.

כשמשתמש גולש באפליקציה (או באתר) שלכם, אתם לא רוצים לתת לו לחכות יותר מדיי ולתקוע את האפליקציה עד שכל הפונקציות והקריאות יסיימו לרוץ. העדיפות תמיד היא להציג לו את המסך ותוך כדי לטעון לו את הנתונים במקומות הרלוונטים.

במדריך הזה אשתמש ב setTimeout על מנת לדמות קריאה לשרת שלוקחת זמן. setTimeout זאת פונקציה אסינכרונית. היא ממשיכה לרוץ בלי לעצור את כל הקוד ולא צריך לחכות שהיא תסתיים.

אסינכרוניות בעזרת Callbackss

יש לי דף עם האלמנט <div id="content"></div> . אני רוצה לבצע קריאה לשרת ולהכניס את המידע שאקבל לתוך האלמנט (במקרה שלנו נניח שהשרת מחזיר לנו מחרוזת כלשהי). עיינו בקוד הבא:

let data;
setTimeout(function () {
	data = 'This is the content';
}, 2000);

const content = document.getElementById('content');
content.innerHTML = 'Content is: ' + data;

אם תנסו להריץ את הקוד תקבלו undefined. בואו ננסה להסביר.

אנחנו מצהירים על משתנה. לאחר מכן מבצעים "קריאה לשרת". הקוד ממשיך (setTimeout זאת פונקציה אסינכרונית, זוכרים?) אנחנו בוחרים את האלמנט לפי id ומכניסים לתוכו מחרוזת "Content is: " עם המשתנה שלנו והמידע שהוא מכיל מהשרת. אבל… המידע עדיין לא הגיע המשתנה ריק ואנחנו מדפיסים אותו. לכן אנחנו מקבלים undefined. מה עושים?

ג'אווה סקריפט היא בין שפות התכנות היחידות שמאפשרות להעביר פונקציות בתור ארגומטרים לפונקציות אחרות. היכולת העוצמתית הזאת נותנת לנו המון כוח בתכנות פונקציונלי ו HOF, אבל גם בתכנות אסינכרוני. בעזרת היכולת המדהימה הזאת אנחנו יכולים לכתוב פונקציות שישמשו בתור "קריאה אחורה" או בלשון העם קולבק.

נתחיל בלכתוב פונקציית callback. הפונקציה הזאת תשמש אותנו על מנת להחזיר את הערך שנקבל מהשרת.

function MyCallback(str){
    content.innerHTML += str;
}
// Or with ES6+
const MyCallback(str) => content.innerHTML += str;

הפונקציה הזאת תקבל ארגומנט בשם str שהוא בעצם המחרוזת שהשרת יחזיר לנו. את המחרוזת הזאת אנחנו נוסיף לאלמנט שבדום.

עכשיו מה שנותר לנו זה לעטוף את הקריאה לשרת שלנו בפונקציה ולשלוח לה את ה-callback בתור ארגומנט, ואת המידע שחזר לשלוח בקולבק. אין טוב ממראה עיניים:

function getData(callback) {
	setTimeout(() => callback('Our data'), 2000);
}
getData(MyCallback);

הקוד המלא:

const content = document.getElementById('content');
content.innerHTML = 'Content is: ';

function MyCallback(str) {
	content.innerHTML += str;
}

function getData(callback) {
	setTimeout(() => callback('Our data'), 2000);
}

getData(MyCallback);

אז מה עשינו פה? אתחלנו את האלמנט שלנו עם הטקסט "Content is:". כתבנו פונקציה MyCallback שתפקידה היה לקבל מחרוזת ולהוסיף את המחרוזת לאלמנט שלנו בדום. בשלב הבא, כתבנו את הפונקציה getData שמקבלת פונקציה בתור ארגומנט (callback) מבצעת קריאה לשרת, ואת המידע שחזר עוטפת בפונקציה שבארגומנט. לבסוף, קראנו לפונקציה שלנו שמבצעת קריאה לשרת ושלחנו לה בתור ארגומנט את פונקציית הcallback שכתבנו בהתחלה.

מה שקורה בפועל זה שהתבצעה קריאה לפונקציה getData עם הקולבק שנשלח בתוכה. התבצעה "קריאה לשרת" שמעבד את הנתונים ואמור להחזיר לנו את הנתונים אחרי 2 שניות. את הנתונים שהשרת מחזיר אנו עוטפים בפונקציה MyCallback ששלחנו בתור ארגומנט. הנתונים נכנסו בתור הארגומנט str של MyCallback. וכש-MyCallback רצה היא מדפיסה את ה-str שהיא קיבלה לתוך האלמנט שלנו.

עוד אופציה, במקום להפריד את הפונקציות יכולנו ישירות לכתוב את ה-MyCallback בתור ה-getData והתוצאה הייתה אותה תוצאה:

getData(function (str) {
	content.innerHTML += str;
});
// Or with ES6+
getData((str) => (content.innerHTML += str));

מדהים הא? אז למה היום כמעט ולא כותבים ככה אסינכרוניות? מסיבה פשוטה. נניח ויש לכם כמה פונקציות אסינכרוניות אחת בתוך השנייה. בקלות אפשר להגיע למצב הזה:

עכשיו דמיינו שהקוד פה בתמונה טיפה יותר מסובך, ואולי יש בו עוד איזה 2-3 פונקציות פנימיות. לזה קוראים Callback Hell ואני בטוח שאני לא צריך להסביר למה. מה הפיתרון או איך נמנעים ממצב כזה? קוראים את החלק הבא של המדריך 😁

החלק הבא של המדריך יעסוק ב Promise. חידוש של ES6 שמאפשר לנו לכתוב פונקציות אסינכרוניות בצורה ברורה וקלה יותר וכך להימנע מ-Callback Hel.