הבנת הלולאת אירועים, התקשרויות, Promises ו-Async/Await ב-JavaScript

המחבר בחר בקרן ההקלה מקוביד-19 לקבל תרומה כחלק מתוכנית הכתיבה למענה לתרומות.

הקדמה

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

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

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

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

הקטע הזה יסביר כיצד JavaScript מתמודד עם קוד אסינכרוני באמצעות לולאת האירועים. תחילה יתבצע דמיינה של לולאת האירועים בפועל, ואז ייסברו שני הרכיבים של לולאת האירועים: המחסנית והתור.קוד JavaScript שאינו משתמש ב-API רשת אסינכרונית יבצע באופן סינכרוני – אחד אחרי השני, בצורה רצופה. זה מודגם על ידי קוד הדוגמה הזה שקורא לשלוש פונקציות שכל אחת מדפיסה מספר ל־console:// מגדיר שלוש פונקציות דוגמהבקוד זה, אתה מגדיר שלוש פונקציות שמדפיסות מספרים עם console.log().

לאחר מכן, יש לכתוב קריאות לפונקציות:

הפלט יתבסס על סדר הפעולות שנקראו – first(), second(), ואז third():

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

// מגדיר שלוש פונקציות דוגמה, אך אחת מהן מכילה קוד אסינכרוני
function first() {
  console.log(1)
}

function second() {
  console.log(2)
}

function third() {
  console.log(3)
}

בקוד זה, אתה מגדיר שלושה פונקציות המדפיסות מספרים עם console.log().

לאחר מכן, כתוב קריאות לפונקציות:

// בצע את הפונקציות
first()
second()
third()

הפלט יהיה תלוי בסדר שבו קראו לפונקציות—first(), second(), ולאחר מכן third():

Output
1 2 3

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

הוסף setTimeout לפונקציה second כדי לדמות בקשה אסינכרונית:

// הגדר שלוש פונקציות דוגמא, אך אחת מהן מכילה קוד אסינכרוני
function first() {
  console.log(1)
}

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

function third() {
  console.log(3)
}

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

עכשיו קרא לפונקציות, כפי שעשית קודם:

// בצע את הפונקציות
first()
second()
third()

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

Output
1 3 2

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

ערימה

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

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

  • הוספת first() למחסנית, הרצת first() אשר מחזירה 1 לתיקייה, הסרת first() מהמחסנית.
  • הוספת second() למחסנית, הרצת second() אשר מחזירה 2 לתיקייה, הסרת second() מהמחסנית.
  • הוספת third() למחסנית, הרצת third() אשר מחזירה 3 לתיקייה, הסרת third() מהמחסנית.

הדוגמה השנייה עם setTimout נראית כך:

  • הוספת first() למחסנית, הרצת first() אשר מחזירה 1 לתיקייה, הסרת first() מהמחסנית.
  • הוספת second() למחסנית, הרצת second().
    • הוספת setTimeout() למחסנית, הרצת אינטרנט API setTimeout() אשר מתחיל טיימר ומוסיפה את הפונקציה האנונימית לתור, הסרת setTimeout() מהמחסנית.
  • הסרת second() מהמחסנית.
  • הוספת third() למחסנית, הרצת third() אשר מחזירה 3 לתיקייה, הסרת third() מהמחסנית.
  • הלולאה של האירועים בודקת את התור לכל מסרים בהמתנה ומוצאת את הפונקציה האנונימית מ- setTimeout(), מוסיפה את הפונקציה לערימה שמחזירה 2 לתוך התפריט, ואז מסירה אותה מהערימה.

באמצעות setTimeout, API אינטרנט אסיכרוני, מציג את מושג ה- תור, שהמדריך הזה יכסה בהמשך.

תור

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

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

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

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

פונקציות קלטנית

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

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

הנה דוגמה קוד תחבירית של פונקציה מסדר גבוה יותר ופונקציה קולבק:

// פונקציה
function fn() {
  console.log('Just a function')
}

// פונקציה שלוקחת פונקציה אחרת כארגומנט
function higherOrderFunction(callback) {
  // כשאתה קורא לפונקציה שהועברה כארגומנט, זה נקרא קולבק
  callback()
}

// מעברת פונקציה
higherOrderFunction(fn)

בקוד זה, אתה מגדיר פונקציה fn, מגדיר פונקציה higherOrderFunction שלוקחת פונקציה callback כארגומנט, ומעביר fn כקולבק לhigherOrderFunction.

ריצת הקוד הזה תיתן את הבא:

Output
Just a function

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

function first() {
  console.log(1)
}

function second() {
  setTimeout(() => {
    console.log(2)
  }, 0)
}

function third() {
  console.log(3)
}

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

הנה שלוש הפונקציות עם קלאבק מופעל:

// הגדר שלושה פונקציות
function first() {
  console.log(1)
}

function second(callback) {
  setTimeout(() => {
    console.log(2)

    // ביצוע פונקציה הקלאבק
    callback()
  }, 0)
}

function third() {
  console.log(3)
}

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

first()
second(third)

לאחר ריצת קובץ הקוד הזה, תקבל את הפלט הבא:

Output
1 2 3

תדפיס את הראשון 1, ואחרי שהזמן ישלים (במקרה זה, אפס שניות, אך אפשר לשנות אותו לכמות כזו) ידפיס 2 ואז 3. על ידי העברת פונקציה כקלאבק, כבר מצאת דרך לעכב את הביצוע של הפונקציה עד שה-Web API האסיכרוני (setTimeout) ישלים.

המפתח לקח כאן הוא שפונקציות הקלאבק אינן אסיכרוניות – setTimeout הוא ה-Web API האסיכרוני האחראי לטפל במשימות אסיכרוניות. הקלאבק רק מאפשר לך לקבל הודעה על כך שמשימה אסיכרונית השלמה ומטפל בהצלחה או כישלון של המשימה.

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

קללבקס מקוננים ופירמידת האומות

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

הנה הדגמה של קללבקס מקוננים:

function pyramidOfDoom() {
  setTimeout(() => {
    console.log(1)
    setTimeout(() => {
      console.log(2)
      setTimeout(() => {
        console.log(3)
      }, 500)
    }, 2000)
  }, 1000)
}

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

Output
1 2 3

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

הנה דוגמה שניתנת לריצה של "פירמידת האומות" יותר מספרית שאפשר לשחק עם:

// דוגמה לפונקציה אסימכרונית
function asynchronousRequest(args, callback) {
  // זרוק שגיאה אם לא עברו טיעונים
  if (!args) {
    return callback(new Error('Whoa! Something went wrong.'))
  } else {
    return setTimeout(
      // רק מוסיפים מספר אקראי כך שנראה כאילו הפונקציה האסימכרונית המובאת
      // חזר נתונים שונים
      () => callback(null, {body: args + ' ' + Math.floor(Math.random() * 10)}),
      500,
    )
  }
}

// בקשות אסימכרוניות מקונן
function callbackHell() {
  asynchronousRequest('First', function first(error, response) {
    if (error) {
      console.log(error)
      return
    }
    console.log(response.body)
    asynchronousRequest('Second', function second(error, response) {
      if (error) {
        console.log(error)
        return
      }
      console.log(response.body)
      asynchronousRequest(null, function third(error, response) {
        if (error) {
          console.log(error)
          return
        }
        console.log(response.body)
      })
    })
  })
}

// ביצוע
callbackHell()

בקוד זה, עליך להבטיח שכל פונקציה תשתמש באופן אפשרי ב response ובאופן אפשרי ב error, מה שהופך את הפונקציה callbackHell למבלבלת מבטיח.

הפעלת הקוד הזה תיתן לך את הבא:

Output
First 9 Second 3 Error: Whoa! Something went wrong. at asynchronousRequest (<anonymous>:4:21) at second (<anonymous>:29:7) at <anonymous>:9:13

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

בטחונות

A promise represents the completion of an asynchronous function. It is an object that might return a value in the future. It accomplishes the same basic goal as a callback function, but with many additional features and a more readable syntax. As a JavaScript developer, you will likely spend more time consuming promises than creating them, as it is usually asynchronous Web APIs that return a promise for the developer to consume. This tutorial will show you how to do both.

יצירת בטחון

אפשר לאתחל תשובה בעזרת התחביר new Promise, ועליך לאתחל אותה עם פונקציה. הפונקציה שמועברת לתשובה מכילה פרמטרים resolve ו-reject. הפונקציות resolve ו-reject מטפלות בהצלחה ובכישלון של פעולה, בהתאמה.

כתוב את השורה הבאה כדי להכריז על תשובה:

// אתחל תשובה
const promise = new Promise((resolve, reject) => {})

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

Output
__proto__: Promise [[PromiseStatus]]: "pending" [[PromiseValue]]: undefined

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

const promise = new Promise((resolve, reject) => {
  resolve('We did it!')
})

עכשיו, על ידי בדיקת התשובה, תמצא שיש לה סטטוס של fulfilled, וערך value המוגדר לערך שהעברת ל-resolve:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: "We did it!"

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

A promise can have three possible states: pending, fulfilled, and rejected.

  • Pending – המצב הראשוני לפני שהוכרע או נדחה
  • Fulfilled – פעולה מוצלחת, התשובה התגברה
  • Rejected – פעולה נכשלה, התשובה נדחתה

לאחר השלמה או דחייה, תשובה מגובשת.

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

צריכת הבטחה

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

ככה תחזיר ותישאיר את value של הבטחה הדוגמא:

promise.then((response) => {
  console.log(response)
})

הבטחה שיצרת היתה עם [[PromiseValue]] של We did it!. ערך זה הוא מה שיעבור לפונקצית האנונימית כ response:

Output
We did it!

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

הקוד הבא מדמה נתונים שהוחזרו מבקשה אסינכרונית כבטחה:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => resolve('Resolving an asynchronous request!'), 2000)
})

// הודעת התוצאה
promise.then((response) => {
  console.log(response)
})

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

עכשיו אחרי שני שניות, זה יתמתת את ערך ההבטחה וזה יישוחרר ב then:

Output
Resolving an asynchronous request!

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

// לקשר הבטחה
promise
  .then((firstResponse) => {
    // החזר ערך חדש למימוש הבא
    return firstResponse + ' And chaining!'
  })
  .then((secondResponse) => {
    console.log(secondResponse)
  })

התגובה המתמתת בשני then תישתחרר את ערך ההחזרה:

Output
Resolving an asynchronous request! And chaining!

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

להתמודדות עם שגיאות

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

פונקציה זו getUsers תעביר דגל להבטחה, ותחזיר את ההבטחה:

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // מטפלים בפתרון ובדחיית API אסימכוני
    }, 1000)
  })
}

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

function getUsers(onSuccess) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      // מטפלים בפתרון ובדחיית API אסימכוני
      if (onSuccess) {
        resolve([
          {id: 1, name: 'Jerry'},
          {id: 2, name: 'Elaine'},
          {id: 3, name: 'George'},
        ])
      } else {
        reject('Failed to fetch data!')
      }
    }, 1000)
  })
}

לתוצאה המוצלחת, אתה מחזיר אובייקטים JavaScript המייצגים נתוני משתמש דוגמא.

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

הרץ את הgetUser בפקודה עם onSuccess מוגדר לfalse, באמצעות השיטה then עבור המקרה המוצלח והשיטה catch עבור השגיאה:

// רוץ את פונקציית ה-getUsers עם הדגל השגוי כדי לעורר שגיאה
getUsers(false)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

מאחר והשגיאה נכשלה, הthen ידועך והcatch יטפל בשגיאה:

Output
Failed to fetch data!

אם תחליף את הדגל וresolve במקום זאת, הcatch יתעלם, והנתונים יחזרו במקום זאת:

// רוץ את פונקציית ה-getUsers עם הדגל הנכון כדי לפתור בהצלחה
getUsers(true)
  .then((response) => {
    console.log(response)
  })
  .catch((error) => {
    console.error(error)
  })

זה ייצא את נתוני המשתמש:

Output
(3) [{…}, {…}, {…}] 0: {id: 1, name: "Jerry"} 1: {id: 2, name: "Elaine"} 3: {id: 3, name: "George"}

לצורך ההתייחסות, הנה טבלה עם שיטות המטפל על אובייקטים Promise:

Method Description
then() Handles a resolve. Returns a promise, and calls onFulfilled function asynchronously
catch() Handles a reject. Returns a promise, and calls onRejected function asynchronously
finally() Called when a promise is settled. Returns a promise, and calls onFinally function asynchronously

אחד מ-API-ים הרשת הכי שימושיים ונפוצים שמחזירים הבטחה הוא Fetch API, שמאפשר לך לבצע בקשת משאב אסינכרונית דרך הרשת. fetch הוא תהליך משולב שנדרש לשרשור then. דוגמה זו מדגימה איך לגשת ל-API של GitHub כדי לאחזר נתוני משתמש, ובנוסף לטפל בשגיאות פוטנציאליות:

// לאחזר משתמש מ-API של GitHub

הבקשה fetch נשלחת ל-URL של https://api.github.com/users/octocat, שמחכה אסינכרונית לתגובה. ה-then הראשון מעביר את התגובה לפונקציה אנונימית שמעצבת את התגובה כ-נתוני JSON, ואז מעביר את ה-JSON ל-then שני שמפעיל את הנתונים ללוג. ההצהרה של catch מקבלת כל שגיאה ומפעילה אותה ללוג.הרצת קוד זה תחזיר את התוצאה הבאה:

זהם הנתונים שביקשו מ-https://api.github.com/users/octocat, בתבנית JSON.

החלק הזה של המדריך הראה שהבטחות משלבות הרבה שיפורים לטיפול בקוד אסינכרוני. אך, בעוד ששימוש ב-then לטיפול בפעולות אסינכרוניות הוא יותר קל לעקיפה מהפירמידה של הקולבקס, חלק מהמפתחים עדיין מעדיפים פורמט סינכרוני לכתיבת קוד אסינכרוני. כדי לטפל בצורך זה, ECMAScript 2016 (ES7) הכניס פונקציות async ומילת המפתח await כדי לשפר את עבודת הבטחות.
fetch('https://api.github.com/users/octocat')
  .then((response) => {
    return response.json()
  })
  .then((data) => {
    console.log(data)
  })
  .catch((error) => {
    console.error(error)
  })

הבקשה של fetch נשלחת לכתובת ה-https://api.github.com/users/octocat של GitHub, שמחכה אסיכרונית לתגובה. הפעולה then הראשונה מעבירה את התגובה לפונקציה נוספת שמעצבת את התגובה כ-נתוני JSON, ואז מעבירה את ה-JSON לפעולה then השנייה שמדפיסה את הנתונים לתיקייה. הפעולה catch מדפיסה כל שגיאה לתיקייה.

הריצה של הקוד הזה תיצור את התוצאות הבאות:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

זהו הנתונים שנבקשו מ-https://api.github.com/users/octocat, המוצגים בפורמט JSON.

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

פונקציות אסינכרוניות עם async/await

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

אתה יכול ליצור פונקציית async על ידי הוספת מילת המפתח async לפני פונקציה:

// צור פונקציית async
async function getUser() {
  return {}
}

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

נסה זאת על ידי רישום קריאה לפונקציית getUser:

console.log(getUser())

זה ייתן את הדברים הבאים:

Output
__proto__: Promise [[PromiseStatus]]: "fulfilled" [[PromiseValue]]: Object

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

getUser().then((response) => console.log(response))

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

תקבל את הדברים הבאים כאשר תפעיל את התוכנית:

Output
{}

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

עם ידע זה, תוכל לשכתב את בקשת Fetch מהחלק האחרון באמצעות async/await כדלקמן:

// טיפול בשאילתות fetch עם async/await
async function getUser() {
  const response = await fetch('https://api.github.com/users/octocat')
  const data = await response.json()

  console.log(data)
}

// ביצוע פונקציה אסינכרונית
getUser()

האופרטורים await כאן מבטיחים שהנתונים (data) לא יועברו ללוג לפני שהבקשה תמלא אותם בנתונים.

כעת הנתונים הסופיים (data) ניתנים לטיפול בתוך פונקציית getUser, ללא צורך בשימוש ב־then. זהו פלט הלוג של data:

Output
login: "octocat", id: 583231, avatar_url: "https://avatars3.githubusercontent.com/u/583231?v=4" blog: "https://github.blog" company: "@github" followers: 3203 ...

הערה: בסביבות רבות, שימוש ב־async הוא נחוץ לשימוש ב־await—אך, גרסאות חדשות של דפדפנים ושל Node מאפשרות שימוש ב־await ברמה העליונה, מה שמאפשר לעבור על יצירת פונקציה אסינכרונית שתקיף את ה־await.

לבסוף, מכיוון שאתה טופל ב־promise המוחלט בתוך הפונקציה האסינכרונית, תוכל גם לטפל בשגיאה בתוך הפונקציה. במקום להשתמש בשיטת catch עם then, תשתמש בתבנית try/catch לטיפול בשגיאה.

הוסף את הקוד המודגש הבא:

// טיפול בהצלחה ובשגיאות עם async/await
async function getUser() {
  try {
    // טיפול בהצלחה ב־try
    const response = await fetch('https://api.github.com/users/octocat')
    const data = await response.json()

    console.log(data)
  } catch (error) {
    // טיפול בשגיאה ב־catch
    console.error(error)
  }
}

התוכנית תדלג כעת לבלוק ה־catch אם תקבל שגיאה ותלוג את השגיאה הזו לקונסול.

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

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

סיכום

מכיוון ש- Web APIs מספקות נתונים בצורה אסינכרונית לעיתים קרובות, למידה כיצד להתמודד עם תוצאות פעולות אסינכרוניות היא חלק הכרחי מהיותך מפתח JavaScript. במאמר זה, למדת כיצד סביבת האירוח משתמשת בלולאת האירועים כדי לטפל בסדר הביצוע של הקוד עם הערימה ו-התור. ניסית גם דוגמאות לשלוש דרכים להתמודד עם הצלחה או כישלון של אירוע אסינכרוני, עם פונקציות התקשרות חזרה (callbacks), הבטחות (promises), ותחביר async/await. לבסוף, השתמשת ב- Fetch Web API כדי לטפל בפעולות אסינכרוניות.

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

Source:
https://www.digitalocean.com/community/tutorials/understanding-the-event-loop-callbacks-promises-and-async-await-in-javascript