פונקציות אסינכרוניות מאפשרות לכתוב קוד מבוסס-הבטחה כאילו הוא סינכרוני.
הפונקציות האסינכרוניות מופעלות כברירת מחדל ב-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);
}
}
המספר זהה של שורות אבל כל הקריאות החוזרות נעלמו. כך קל יותר לקרוא אותה, במיוחד לאנשים שלא מכירים את ההבטחות.
ערכי החזרה אסינכרוניים
פונקציות אסינכרוניות תמיד מחזירות הבטחה, גם אם משתמשים ב-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
בלולאת 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.
הפלט לא כל כך יפה, אז כדאי להיזהר מנפח יתר של קוד.
אסנכרן את כל הדברים!
אחרי שהפונקציות האסינכרוניות נוחתות בכל הדפדפנים, אפשר להשתמש בהן בכל פונקציה שמחזירה הבטחה! הם לא רק משפרים את הסדר של הקוד, אלא גם מבטיחה שהפונקציה תמיד תחזיר הובטחה.
מאוד התרגשתי לגבי פונקציות אסינכרוניות ב-2014, וזה נהדר לראות אותן נוחתות, באמת, בדפדפנים. אופס!