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