async/await הוא פשוט promises בתחפושת
async/await הוא לא מודל מקביליות חדש. זו שכבת תחביר מעל promises שמאפשרת לכתוב קוד ש_נראה_ סדרתי גם כשהוא אסינכרוני. אותו מנגנון, צורה הרבה יותר ידידותית.
הנה אותה משימה כתובה בשתי הדרכים:
שתי הפונקציות מחזירות promise. שתיהן עושות בדיוק את אותו הדבר. גרסת ה-async נקראת מלמעלה למטה בלי שרשרת .then, וזה כל הקסם.
async מסמן שפונקציה מחזירה promise
שימו async לפני function, לפני arrow function או לפני מתודה, ושני דברים קורים:
- הפונקציה תמיד מחזירה promise. כל מה שעושים לו
returnהופך לערך שה-promise מסתיים בו. - מותר להשתמש בתוכה ב-
await.
שימו לב ש-result הוא לא המחרוזת: הוא promise שמסתיים במחרוזת. אף ש-greet לא מכילה await ולא עבודה אסינכרונית, מילת המפתח async עדיין עוטפת את ערך ההחזרה ב-promise. אם הפונקציה זורקת שגיאה, ה-promise נדחה.
await עוצר עד שה-promise מסתיים
בתוך פונקציית async, await somePromise עוצר את הפונקציה עד שה-promise מתממש, ואז מוסר לכם את הערך. אם ה-promise נדחה, await זורק שגיאה.
שימו לב לסדר הפלט. "started countdown" מודפס לפני "2", כי await עוצר רק את פונקציית ה-async, לא את שאר התוכנית. ה-event loop ממשיך לרוץ; countdown פשוט ממשיכה מאוחר יותר, כשכל promise של wait מתממש.
אפשר לעשות await לכל דבר שדומה ל-promise. גם await 42 חוקי: ערכים שאינם promises נעטפים ב-Promise.resolve(42) ומתממשים מיד.
טיפול בשגיאות עם try/catch
עם promises רגילים משרשרים .catch(). עם async/await, promise שנדחה הופך לחריגה שנזרקת, ואפשר לתפוס אותה בדרך הרגילה:
try/catch אחד מכסה כל await שבתוכו. תקלות רשת, שגיאות בפענוח JSON ופקודות throw שלכם, כולן נוחתות באותו catch. זה שדרוג אמיתי לעומת שרשראות .then/.catch מקוננות.
דבר אחד לשים לב אליו: fetch נדחה רק בשגיאות רשת, לא ב-HTTP 4xx/5xx. אתם בודקים את res.ok בעצמכם וזורקים שגיאה, דפוס שתראו כל הזמן בקוד אמיתי.
אל תעשו await בתוך לולאה כשאין צורך
זו המלכודת הנפוצה ביותר של async/await. await סדרתי בתוך לולאה אומר שכל איטרציה מחכה לקודמת:
sequential לוקחת בערך 900ms. parallel לוקחת בערך 300ms. כלל האצבע: אם המשימות לא תלויות זו בתוצאות של זו, התחילו את כולן ואז עשו await Promise.all. עשו await אחת אחרי השנייה רק כשהקריאה הבאה באמת צריכה את התוצאה הקודמת.
לאוספים, Promise.all(items.map(async (x) => ...)) הוא הניב המקובל. for...of רגילה עם await בפנים רצה באופן סדרתי: לפעמים זה מה שרוצים (הגבלת קצב, שמירה על סדר), אבל בדרך כלל לא.
שילוב של async/await עם promises רגילים
לא חייבים לבחור צד. פונקציות async מחזירות promises, ו-await עובד על כל promise, כך שאפשר לשלב ביניהם בחופשיות:
שני הסגנונות ניתנים להחלפה. השתמשו ב-await כשהקוד נקרא טוב יותר מלמעלה למטה; השתמשו ב-.then כשרוצים משהו חד פעמי מהיר או כשעובדים מחוץ להקשר אסינכרוני.
await ברמה העליונה (ב-ES Modules)
בעבר היה צריך לעטוף await בפונקציית async, כי await לא היה מותר ברמה העליונה של סקריפט. זה השתנה: בתוך ES module (קובץ .mjs או <script type="module">), אפשר היום לעשות await ישירות ברמה העליונה:
// in an ES module
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await res.json();
console.log(user.name);
await ברמה העליונה מעכב את סיום הטעינה של המודול עד שה-promise מסתיים, וכל מי שמייבא את המודול מחכה גם הוא. זה נוח לטעינת קונפיגורציה ולייבוא דינמי, אבל השתמשו בזה במשורה: await איטי ברמה העליונה חוסם את כל מי שמייבא את המודול.
בקבצי CommonJS או בסקריפטים רגילים בתוך הדף, זה עדיין נכשל עם SyntaxError. הפתרון הקלאסי הוא פונקציה אסינכרונית שנקראת מיד:
דברים שמכשילים אנשים
סיור מהיר במלכודות הרגילות:
- שוכחים
async. שימוש ב-awaitבפונקציה רגילה הוא שגיאת תחביר. הפתרון הוא להוסיףasync, או לקרוא לפונקציית העזר האסינכרונית עם.then. - שוכחים לעשות
awaitלתוצאה.const data = getJSON(url);נותן לכם promise, לא את הנתונים. אם תשתמשו בו כאילו הוא הערך, תראו[object Promise]מופיע בפלט. - דחיות בלי טיפול. פונקציה אסינכרונית שמפעילים ושוכחים (
doWork();) תבלע שגיאות בשקט, אלא אם מוסיפים.catchאו עושים להawaitבתוךtry/catch. forEachעם callbacks אסינכרוניים.array.forEach(async (x) => await something(x))לא מחכה לשום דבר:forEachמתעלמת מה-promises שמוחזרים. השתמשו ב-for...ofעםawait, או ב-Promise.all(array.map(...)).
הריצו את זה: "finished?" מודפס לפני כל "done", כי broken חוזרת בלי לחכות. fixed מחכה לכולם, ורק אז מדפיסה "finished!" אחרונה.
מתי לבחור ב-async/await
ברירת המחדל היא async/await לכל קוד שמבצע יותר מצעד אסינכרוני אחד ברצף, או שצריך טיפול בשגיאות בסגנון try/catch. הישארו עם promises רגילים לשורות בודדות פשוטות, לקוד ספרייה שמחזיר promise בלי לחכות בעצמו לשום דבר, או כשבאמת צריך צירופים כמו Promise.race או .finally() בשרשרת.
כשמשתמשים בו נכון, async/await הופך קוד אסינכרוני לקריא כמו מתכון: עשו את זה, אחר כך את זה, אחר כך את זה. ה-event loop ממשיך לעשות את שלו, אתם רק מפסיקים לחשוב ב-callbacks.
הבא בתור: ה-fetch API
רוב הדוגמאות כאן השתמשו ב-fetch כמייצג של "משהו אסינכרוני". שווה להכיר אותו כמו שצריך: איך בקשות ותגובות עובדות, טיפול ב-JSON, הגדרת headers, ולמה fetch לא נדחה בשגיאות HTTP. זה העמוד הבא.
שאלות נפוצות
מה עושה async/await ב-JavaScript?
async/await הוא תחביר לעבודה עם promises שמאפשר לכתוב קוד אסינכרוני כאילו הוא סינכרוני. async מסמן שפונקציה מחזירה promise, ו-await עוצר בתוך הפונקציה הזו עד שה-promise מסתיים, ואז נותן לכם את הערך שהתקבל. מתחת לפני השטח אלה עדיין promises, רק קלים יותר לקריאה.
אפשר להשתמש ב-await מחוץ לפונקציה async?
ברמה העליונה של ES module, כן: זה נקרא await ברמה העליונה. בתוך פונקציות רגילות או בסקריפטים של CommonJS, לא: await מחוץ לפונקציית async הוא שגיאת תחביר. הפתרון הוא בדרך כלל לעטוף את הקוד בפונקציית async ולקרוא לה, או להפוך את הקובץ ל-ES module.
איך מטפלים בשגיאות עם async/await?
עטפו את הקריאות שממתינים להן ב-try/catch. כל promise שנדחה ושעושים לו await הופך לחריגה שנזרקת, ובלוק ה-catch יכול לטפל בה. למשימות רקע שלא עושים להן await, הוסיפו .catch() ל-promise המוחזר כדי שדחיות לא יישארו בלי טיפול.
האם await חוסם את כל התוכנית?
לא. await עוצר רק את פונקציית ה-async הנוכחית. ה-event loop ממשיך לרוץ: טיימרים מופעלים, משימות אסינכרוניות אחרות מתקדמות, והממשק נשאר תגובתי. הקוד הקורא מקבל בחזרה promise ממתין וממשיך מיד.