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