פונקציות אסינכררוניות מאפשרות לכתוב קוד מבוסס-הבטחה כאילו הוא סינכרוני.
פונקציות אסינכררוניות מופעלות כברירת מחדל ב-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.
מומלץ להשתמש בגישה של תרגום מקודש (transpiling), כי אפשר פשוט להשבית אותה ברגע שדפדפני היעד תומכים בפונקציות אסינכררוניות. עם זאת, אם אתם באמת לא רוצים להשתמש בתרגם מקודש, תוכלו להשתמש ב-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
. חוץ מזה, הוא פועל באופן זהה.
פתרון עקיף: regenerator
אם אתם מטרגטים דפדפנים ישנים יותר, Babel יכול גם להמיר גנרטורים, וכך מאפשר לכם להשתמש בפונקציות אסינכררוניות עד ל-IE8. כדי לעשות זאת, צריך את הגדרת ברירת המחדל es2017 של Babel וגם את הגדרת ברירת המחדל es2015.
הפלט לא יפה, לכן חשוב להיזהר מקוד מיותר.
אסנכרן את כל הדברים!
אחרי שהפונקציות האסינכרוניות נוחתות בכל הדפדפנים, אפשר להשתמש בהן בכל פונקציה שמחזירה הבטחה! הם לא רק עוזרים לשמור על קוד מסודר, אלא גם מוודאים שהפונקציה תמיד תחזיר הבטחה.
התלהבתי מאוד מהפונקציות האסינכרוניות בשנת 2014, ועכשיו נהדר לראות שהן מגיעות לדפדפנים. אופס!