פונקציות אסינכררוניות מאפשרות לכתוב קוד מבוסס-הבטחה כאילו הוא סינכרוני.
פונקציות אסינכררוניות מופעלות כברירת מחדל ב-Chrome, ב-Edge, ב-Firefox וב-Safari, והן פשוט נהדרות. הם מאפשרים לכתוב קוד מבוסס-הבטחה כאילו הוא סינכרוני, אבל בלי לחסום את ה-thread הראשי. הם הופכים את הקוד האסינכרוני פחות "חכם" לקריא יותר.
הפונקציות האסינכרוניות פועלות כך:
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
} catch (rejectedValue) {
// …
}
}
אם משתמשים במילות המפתח async
לפני הגדרת פונקציה, אפשר להשתמש ב-await
בתוך הפונקציה. כשמבצעים await
של הבטחה, הפונקציה מושהית בדרך שלא חוסמת עד שההבטחה נשמרת. אם ההתחייבות מתקיימת, הערך מוחזר. אם ההבטחה נדחתה, הערך שנדחתה יידחה.
תמיכה בדפדפנים
דוגמה לרישום ביומן של אחזור
נניח שאתם רוצים לאחזר כתובת URL ולרשום את התשובה כטקסט. כך זה נראה בעזרת הבטחות:
function logFetch(url) {
return fetch(url)
.then((response) => response.text())
.then((text) => {
console.log(text);
})
.catch((err) => {
console.error('fetch failed', err);
});
}
והנה אותו הדבר כשמשתמשים בפונקציות אסינכרוניות:
async function logFetch(url) {
try {
const response = await fetch(url);
console.log(await response.text());
} catch (err) {
console.log('fetch failed', err);
}
}
מספר השורות זהה, אבל כל הקריאות החוזרות הוסרו. כך קל יותר לקרוא את הקוד, במיוחד למי שלא מכיר היטב את ה-promises.
ערכים שמוחזרים בפעולות אסינכרוניות
פונקציות אסינכררוניות תמיד מחזירות הבטחה, גם אם משתמשים ב-await
וגם אם לא. ההבטחה הזו מתקבלת עם הערך שהפונקציה האסינכרונית מחזירה, או נדחית עם הערך שהפונקציה האסינכרונית גורמת להשלכה שלו. לכן, עם:
// wait ms milliseconds
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function hello() {
await wait(500);
return 'world';
}
...קריאה לפונקציה hello()
מחזירה הבטחה שממלאת את "world"
.
async function foo() {
await wait(500);
throw Error('bar');
}
…קריאה ל-foo()
מחזירה הבטחה שנדחית עם Error('bar')
.
דוגמה: העברה בזמן אמת של תגובה
היתרונות של פונקציות אסינכרניות גדלים בדוגמאות מורכבות יותר. נניח שרוצים להעביר תשובה בסטרימינג תוך רישום ביומן של קטעי הקוד, ולהחזיר את הגודל הסופי.
הנה זה עם הבטחות:
function getResponseSize(url) {
return fetch(url).then((response) => {
const reader = response.body.getReader();
let total = 0;
return reader.read().then(function processResult(result) {
if (result.done) return total;
const value = result.value;
total += value.length;
console.log('Received chunk', value);
return reader.read().then(processResult);
});
});
}
תראו לי את ג'ייק, 'בעל ההבטחות', ארצ'יבלד. רואים איך אני קורא ל-processResult()
בתוך עצמו כדי להגדיר לולאה אסינכרונית? כתיבת שגרמה לי להרגיש חכמה מאוד. אבל כמו רוב הקודים 'החכמים', צריך להביט בו במשך זמן רב כדי להבין מה הוא עושה, כמו אחת מהתמונות של 'עין הקסם' משנות ה-90.
ננסה שוב עם פונקציות אסינכרוניות:
async function getResponseSize(url) {
const response = await fetch(url);
const reader = response.body.getReader();
let result = await reader.read();
let total = 0;
while (!result.done) {
const value = result.value;
total += value.length;
console.log('Received chunk', value);
// get the next result
result = await reader.read();
}
return total;
}
כל ה'חכמה' נעלמה. הלולאה האסינכרונית שגרמה לי להרגיש כל כך מרוצים הוחלפה בלולאת while מהימנה ומשעממת. הרבה יותר טוב. בעתיד, תוכלו להשתמש במחזורים אסינכרונים, שיאפשרו להחליף את הלולאה while
בלולאה מסוג for-of, וכך להפוך את הקוד ליותר מסודר.
תחבירים אחרים של פונקציות אסינכררוניות
כבר הראיתי לכם את async function() {}
, אבל אפשר להשתמש במילות המפתח async
עם תחביר של פונקציות אחרות:
פונקציות חץ
// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
const response = await fetch(url);
return response.json();
});
שיטות של אובייקטים
const storage = {
async getAvatar(name) {
const cache = await caches.open('avatars');
return cache.match(`/avatars/${name}.jpg`);
}
};
storage.getAvatar('jaffathecake').then(…);
שיטות של כיתות
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jaffathecake').then(…);
זהירות! הימנעו מרצף אירועים ארוך מדי
גם אם אתם כותבים קוד שנראה סינכרוני, חשוב לא לפספס את ההזדמנות לבצע דברים במקביל.
async function series() {
await wait(500); // Wait 500ms…
await wait(500); // …then wait another 500ms.
return 'done!';
}
השלמת התהליך שלמעלה נמשכת 1,000 אלפיות השנייה, ואילו:
async function parallel() {
const wait1 = wait(500); // Start a 500ms timer asynchronously…
const wait2 = wait(500); // …meaning this timer happens in parallel.
await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
return 'done!';
}
השלמת הפעולה שלמעלה נמשכת 500 אלפיות שנייה, כי שתי ההמתנות מתרחשות בו-זמנית. נבחן דוגמה מעשית.
דוגמה: הפלט של אחזור ברצף
נניח שאתם רוצים לאחזר סדרה של כתובות URL ולרשום אותן בהקדם האפשרי, בסדר הנכון.
נשימה עמוקה – כך זה נראה עם הבטחות:
function markHandled(promise) {
promise.catch(() => {});
return promise;
}
function logInOrder(urls) {
// fetch all the URLs
const textPromises = urls.map((url) => {
return markHandled(fetch(url).then((response) => response.text()));
});
// log them in order
return textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise).then((text) => console.log(text));
}, Promise.resolve());
}
כן, זה נכון, אני משתמש ב-reduce
כדי לשרשר רצף של הבטחות. אני כל כך חכם. אבל זו קצת קוד חכם מדי, עדיף להימנע ממנו.
עם זאת, כשממירים את הקוד שלמעלה לפונקציה אסינכררונית, קל ליפול לסדר כרונולוגי מוגזם:
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
function markHandled(...promises) {
Promise.allSettled(promises);
}
async function logInOrder(urls) {
// fetch all the URLs in parallel
const textPromises = urls.map(async (url) => {
const response = await fetch(url);
return response.text();
});
markHandled(...textPromises);
// log them in sequence
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
פתרון עקיף לתמיכה בדפדפנים: גנרטורים
אם אתם מטרגטים דפדפנים שתומכים בגנרטורים (כולל הגרסה האחרונה של כל דפדפן ראשי), אתם יכולים להשתמש ב-polyfill לפונקציות אסינכררוניות.
Babel יעשה זאת בשבילכם, כאן יש דוגמה באמצעות Babel REPL
- שימו לב כמה דומה הקוד שעבר טרנספיילציה. הטרנספורמציה הזו היא חלק מהגדרת ברירת המחדל של es2017 ב-Babel.
אני ממליץ על גישת ההעברה, כי אפשר להשבית אותה ברגע שדפדפני היעד יתמכו בפונקציות אסינכרוניות, אבל אם אתם באמת לא רוצים להשתמש בטרנספילר, תוכלו להשתמש ב-polyfill של Babel ולהשתמש בו בעצמכם. במקום:
async function slowEcho(val) {
await wait(1000);
return val;
}
…צריך לכלול את ה-polyfill ולכתוב:
const slowEcho = createAsyncFunction(function* (val) {
yield wait(1000);
return val;
});
שימו לב שצריך להעביר מחולל (function*
) ל-createAsyncFunction
, ולהשתמש ב-yield
במקום ב-await
. חוץ מזה, זה עובד באותו אופן.
פתרון: יצירה מחדש
אם אתם מטרגטים דפדפנים ישנים, Babel יכול גם להמיר גנרטורים, מה שמאפשר לכם להשתמש בפונקציות אסינכרוניות עד ל-IE8. כדי לעשות זאת, צריך את הגדרת ברירת המחדל es2017 של Babel וגם את הגדרת ברירת המחדל es2015.
הפלט לא יפה, לכן חשוב להיזהר מקוד מיותר.
אסינכרוניזציה של הכול!
אחרי שפונקציות אסינכררוניות יהיו זמינות בכל הדפדפנים, כדאי להשתמש בהן בכל פונקציה שמחזירה הבטחה (promise). הם לא רק עוזרים לשמור על קוד מסודר, אלא גם מוודאים שהפונקציה תמיד תחזיר הבטחה.
התלהבתי מאוד מהפונקציות האסינכרוניות בשנת 2014, ועכשיו נהדר לראות שהן מגיעות לדפדפנים. יש!