logo

Callback Hell ב-JavaScript

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

בשפות תכנות, callback hell מתייחס בדרך כלל לדרך לא יעילה של כתיבת קוד עם שיחות אסינכרוניות. היא ידועה גם בשם פירמידת האבדון.

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

כדי להבין טוב יותר את גיהינום ה-callback ב-JavaScript, הבן תחילה את ה-callbacks ולולאות האירועים ב-JavaScript.

Callbacks ב-JavaScript

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

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

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

שקול את הדוגמה הבאה:

 function expectedResult(action, x, y){ if(action === 'add'){ return x+y }else if(action === 'subtract'){ return x-y } } console.log(expectedResult('add',20,10)) console.log(expectedResult('subtract',30,10)) 

תְפוּקָה:

 30 20 

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

אז נוכל לשכתב את הקוד בצורה טובה יותר באופן הבא:

 function add(x,y){ return x+y } function subtract(x,y){ return x-y } function expectedResult(callBack, x, y){ return callBack(x,y) } console.log(expectedResult(add, 20, 10)) console.log(expectedResult(subtract, 30, 10)) 

תְפוּקָה:

 30 20 

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

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

כדי להבין את זה טוב יותר, עלינו להבין את ה-pass-by-value וה-pass-by-reference של JavaScript.

JavaScript תומך בשני סוגים של סוגי נתונים שהם פרימיטיביים ולא פרימיטיביים. סוגי נתונים פרימיטיביים הם undefined, null, string ובוליאני, אשר לא ניתנים לשינוי, או שאנו יכולים לומר בלתי ניתנים לשינוי באופן השוואתי; סוגי נתונים לא פרימיטיביים הם מערכים, פונקציות ואובייקטים הניתנים לשינוי או שינוי.

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

באופן השוואתי, מושג ה-pass-by-value אינו משנה את הערך המקורי שלו, הזמין מחוץ לגוף הפונקציה. במקום זאת, הוא יעתיק את הערך לשני מיקומים שונים באמצעות הזיכרון שלהם. JavaScript זיהה את כל האובייקטים לפי ההפניה שלהם.

ב-JavaScript, ה-addEventListener מאזין לאירועים כגון קליק, מעבר עכבר ו-mouseout ולוקח את הארגומנט השני כפונקציה שתבוצע ברגע שהאירוע יופעל. בפונקציה זו נעשה שימוש במושג מעבר לפי התייחסות ומועבר ללא סוגריים.

שקול את הדוגמה שלהלן; בדוגמה זו, העברנו פונקציית greet כארגומנט לתוך addEventListener כפונקציית callback. הוא יופעל כאשר אירוע הקליק יופעל:

Test.html:

 Javascript Callback Example <h3>Javascript Callback</h3> Click Here to Console const button = document.getElementById(&apos;btn&apos;); const greet=()=&gt;{ console.log(&apos;Hello, How are you?&apos;) } button.addEventListener(&apos;click&apos;, greet) 

תְפוּקָה:

Callback Hell ב-JavaScript

בדוגמה שלמעלה, העברנו פונקציית greet כארגומנט לתוך addEventListener כפונקציית callback. הוא יופעל כאשר אירוע הקליק יופעל.

מספר שלם של java למחרוזת

באופן דומה, המסנן הוא גם דוגמה לפונקציית ה-callback. אם נשתמש במסנן כדי לחזור על מערך, הוא ייקח עוד פונקציית התקשרות כארגומנט כדי לעבד את נתוני המערך. שקול את הדוגמה שלהלן; בדוגמה זו, אנו משתמשים בפונקציה גדולה יותר כדי להדפיס את המספר הגדול מ-5 במערך. אנו משתמשים בפונקציה isGreater כפונקציית התקשרות חוזרת בשיטת הסינון.

 const arr = [3,10,6,7] const isGreater = num =&gt; num &gt; 5 console.log(arr.filter(isGreater)) 

תְפוּקָה:

 [ 10, 6, 7 ] 

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

כדי להבין טוב יותר את ה-Callbacks, Event לולאות ב-JavaScript, בואו נדון ב-JavaScript סינכרוני וא-סינכרוני:

JavaScript סינכרוני

בואו נבין מהן התכונות של שפת תכנות סינכרונית. לתכנות סינכרוני יש את התכונות הבאות:

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

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

פַּשְׁטוּת: לעתים קרובות, התכנות הסינכרוני נחשב קל להבנה מכיוון שאנו יכולים לחזות את סדר זרימת הביצוע שלו. באופן כללי, זה ליניארי וקל לחזות אותו. טוב לפתח את היישומים הקטנים בשפות אלה מכיוון שהם יכולים להתמודד עם סדר הפעולות הקריטי.

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

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

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

אבל כשאנשים מדברים על JavaScript, זו תמיד הייתה תשובה תמוהה בין אם היא סינכרונית או אסינכרונית.

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

מכאן שפונקציית ה-callback נקראת גם חסימת callbacks, שכן היא חוסמת את ביצוע פונקציית האב שבה היא הופעלה.

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

כעת, בואו נבין את ה-JavaScript האסינכרוני.

JavaScript אסינכרוני

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

 function greet(){ console.log(&apos;greet after 1 second&apos;) } setTimeout(greet, 1000) 

מהדוגמה שלמעלה, הפונקציה setTimeout לוקחת התקשרות חוזרת וזמן באלפיות שניות כארגומנטים. ההתקשרות חזרה מופעלת לאחר הזמן שהוזכר (כאן 1s). בקיצור, הפונקציה תחכה 1 שניות לביצוע שלה. עכשיו, תסתכל על הקוד שלהלן:

 function greet(){ console.log(&apos;greet after 1 second&apos;) } setTimeout(greet, 1000) console.log(&apos;first&apos;) console.log(&apos;Second&apos;) 

תְפוּקָה:

 first Second greet after 1 second 

מהקוד לעיל, הודעות היומן לאחר setTimeout יבוצעו תחילה בזמן שהטיימר יעבור. מכאן שהתוצאה היא שנייה אחת ולאחר מכן הודעת הברכה לאחר מרווח זמן של שנייה אחת.

ב-JavaScript, setTimeout היא פונקציה אסינכרונית. בכל פעם שאנו קוראים לפונקציה setTimeout, היא רושמת פונקציית callback (greet במקרה זה) שתבוצע לאחר ההשהיה שצוינה. עם זאת, זה לא חוסם את ביצוע הקוד הבא.

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

בינתיים, לולאת האירועים ב-JavaScript מטפלת במשימות האסינכרוניות. במקרה זה, הוא ממתין עד שהעיכוב שצוין (שנייה) יעבור, ולאחר זמן זה הוא קולט את פונקציית ה-callback (greet) ומבצע אותה.

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

עלינו להבין את מחסנית השיחות ואת תור ההתקשרות חזרה כדי לטפל באירועים האסינכרוניים ב-JavaScript.

שקול את התמונה הבאה:

Callback Hell ב-JavaScript

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

זיכרון הערימה אחראי להקצאת הזיכרון עבור אובייקטים ופונקציות בזמן ריצה בכל פעם שהם נחוצים.

כעת, מנועי הדפדפן שלנו מורכבים ממספר ממשקי API באינטרנט כגון DOM, setTimeout, console, אחזור וכו', והמנוע יכול לגשת לממשקי API אלה באמצעות אובייקט החלון הגלובלי. בשלב הבא, לולאות אירועים מסוימות ממלאות את התפקיד של שומר סף שבוחר בקשות פונקציה בתוך תור ההתקשרות חזרה ודוחף אותן לערימה. פונקציות אלה, כגון setTimeout, דורשות זמן המתנה מסוים.

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

Callback Hell תרחישים

כעת, דיברנו על שיחות חוזרות, סינכרוניות, אסינכרוניות ונושאים רלוונטיים אחרים לגיהינום החזרה. בואו נבין מה זה גיהינום של התקשרות חוזרת ב-JavaScript.

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

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

 getArticlesData(20, (articles) =&gt; { console.log(&apos;article lists&apos;, articles); getUserData(article.username, (name) =&gt; { console.log(name); getAddress(name, (item) =&gt; { console.log(item); //This goes on and on... } }) 

בדוגמה שלמעלה, getUserData לוקח שם משתמש שתלוי ברשימת המאמרים או שצריך לחלץ תגובת getArticles שנמצאת בתוך המאמר. ל-getAddress יש גם תלות דומה, אשר תלויה בתגובה של getUserData. המצב הזה נקרא גיהינום של התקשרות חוזרת.

ניתן להבין את העבודה הפנימית של גיהנום ההתקשרות חזרה באמצעות הדוגמה הבאה:

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

בואו נבין את ההבטחות ב-JavaScript וכיצד הן יוצרות פעולות אסינכרוניות, מה שמאפשר לנו להימנע מכתיבת התקשרויות מקוננות.

JavaScript מבטיח

ב-JavaScript, הבטחות הוצגו ב-ES6. זהו אובייקט עם ציפוי תחבירי. בשל ההתנהגות הא-סינכרונית שלו, זוהי דרך חלופית להימנע מכתיבת ה-callbacks עבור פעולות אסינכרוניות. כיום, ממשקי API כמו fetch() מיושמים באמצעות ה-promising, המספק דרך יעילה לגשת לנתונים מהשרת. זה גם שיפר את קריאות הקוד ומהווה דרך להימנע מכתיבת התקשרויות מקוננות.

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

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

בדרך כלל, להבטחות יש את שלושת המצבים הבאים:

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

התחביר להבטחות:

 let newPromise = new Promise(function(resolve, reject) { // asynchronous call is made //Resolve or reject the data }); 

להלן דוגמה לכתיבת ההבטחות:

זו דוגמה לכתיבת הבטחה.

 function getArticleData(id) { return new Promise((resolve, reject) =&gt; { setTimeout(() =&gt; { console.log(&apos;Fetching data....&apos;); resolve({ id: id, name: &apos;derik&apos; }); }, 5000); }); } getArticleData(&apos;10&apos;).then(res=&gt; console.log(res)) 

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

אסינכרון/המתנה ב-JavaScript

זוהי דרך נוספת להימנע משימוש בהתקשרויות מקוננות. Async/ Await מאפשר לנו להשתמש בהבטחות בצורה הרבה יותר יעילה. אנו יכולים להימנע משימוש ב-.then() או .catch() בשרשור שיטות. שיטות אלו תלויות גם בפונקציות ההתקשרות חזרה.

ניתן להשתמש ב-Async/Await במדויק עם Promise כדי לשפר את ביצועי האפליקציה. זה פתר את ההבטחות באופן פנימי וסיפק את התוצאה. כמו כן, שוב, זה קריא יותר מהשיטות () או catch().

אנחנו לא יכולים להשתמש ב-Async/Await עם פונקציות החזרה רגילות. כדי להשתמש בה, עלינו להפוך פונקציה לא-סינכרונית על ידי כתיבת מילת מפתח אסינכרונית לפני מילת המפתח פונקציה. עם זאת, פנימית היא משתמשת גם ב-chaining.

להלן דוגמה ל-Async/Await:

 async function displayData() { try { const articleData = await getArticle(10); const placeData = await getPlaces(article.name); const cityData = await getCity(place) console.log(city); } catch (err) { console.log(&apos;Error: &apos;, err.message); } } displayData(); 

כדי להשתמש ב-Async/Await יש לציין את הפונקציה עם מילת המפתח async, ואת מילת המפתח await יש לכתוב בתוך הפונקציה. האסינכרון יפסיק את ביצועו עד שההבטחה תיפתר או תדחה. זה יחודש כאשר ההבטחה תטופל. לאחר פתרון, הערך של הביטוי await יישמר במשתנה המחזיק אותו.

סיכום:

בקצרה, אנו יכולים להימנע מהתקשרות חוזרת מקוננת על ידי שימוש בהבטחות ובסנכרון/המתנה. מלבד אלה, אנו יכולים לעקוב אחר גישות אחרות, כגון כתיבת הערות, ופיצול הקוד לרכיבים נפרדים יכול גם להיות מועיל. אבל, בימינו, המפתחים מעדיפים את השימוש ב-async/await.

סיכום:

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

כדי להבין טוב יותר את גיהינום החזרה ב-JavaScript.

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

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