אני יודע שasync נשמע כמו להקת בנים עלובה משנות ה-90, אבל האמת היא שמדובר באחד מהפיצ׳רים שהכי מחכים להם בתקן ES2017 החדש. כזכור – אפשר להשתמש בו כבר היום עם babel. הוא ממש ממש חדש ודנדש ואין אותו אפילו ב-node 8. שלא לדבר על דפדפנים (נכון לכתיבת השורות האלו).
מדובר בפיצ׳ר קצת קשה להבנה, אז אשמח אם לא תדלגו לדוגמאות אלא תקראו את המאמר הזה מההתחלה ועד הסוף. אני מבטיח שזה ישתלם לכם כי זה פיצ׳ר ממש ממש גאוני. מוכנים? שופופו:
כולנו מכירים promises, נכון? מי שלא מכיר – זה הזמן להזכר בes2015. מדובר בדרך מאוד מאוד נוחה לעבוד עם קוד שלוקח לו זמן להתבצע אחר כך. למשל קריאה מ-API או קריאה ממסד נתונים או מערכת קבצים. כאן למשל אני אדגים באמצעות קוד שפשוט ממתין כמה שניות עד שהוא חוזר:
function someService() {
return new Promise(function(resolve, reject) {
setTimeout(ev => {
resolve('Promise result123');
}, 500);
});
}
console.log('start of code');
someService().then(result => {
console.log(result);
});
console.log('end of code');
//Will print:
//"start of code"
//"end of code"
//"Promise result123"
מה יש לנו פה? משהו שמתכנת js מנוסה אמור להכיר. someService היא פונקציה שבאופן עקרוני מחזירה promise אחרי חצי שניה. כרגע מדובר במשהו מזויף, אבל זו יכולה להיות קריאת API או קריאה לקובץ. מה שחשוב הוא שאני משתמש בה באמצעות then. שימוש סטנדרטי של promise ואני אראה שבאמת ברגע שה-promise מתממשת, מה שהיא מחזירה יודפס.
אם הקוד הזה נראה לכם לא מובן בגלל כל ה-=> וה-let, זה הזמן להתחיל לעבוד עם ES6 כי זה הסינטקס שעובדים איתו.
עם async אני יכול לעשות אותו הדבר, רק בהרבה פחות קוד. שימו לב לקוד הזה:
function someService() {
return new Promise(function(resolve, reject) {
setTimeout(ev => {
resolve('Promise result123');
}, 500);
});
}
async function main() {
let result = await someService();
console.log('result' + result);
}
console.log('start of code');
main();
console.log('end of code');
//Will print:
//"start of code"
//"end of code"
//"Promise result123"
מה שמעניין וחשוב כאן הוא ה-main. במקום להשתמש ב-then, אנחנו משתמשים ב-await. בלי resolve ובלי בלגנים! מה שעושים הוא להגדיר פונקציה מסוימת כאסינכרונית. את זה אנחנו עושים באמצעות הכנסת המילה השמורה async לפני ה-function. מהרגע הזה אנחנו יכולים להשתמש ב-await בלי תסבוכות ובלגנים. ה-await בעצם קורה ברגע שההבטחה מתממשת. כלומר ברגע שיש לנו resolve בלי צורך לכתוב then!
הנה דוגמה ממש חיה – שימו לב שכאן הפעלתי את ה-babel עם ה-async plugin וגם את ה -polyfill:
See the Pen Async example by Ran Bar-Zik (@barzik) on CodePen.
אם אנחנו רוצים דוגמה מעט יותר מוחשית, אפשר להסתכל על הקוד הזה:
function jokeService() {
return new Promise(function(resolve, reject) {
var request = new XMLHttpRequest();
request.open('GET', 'https://api.icndb.com/jokes/random');
request.onload = function() {
if (request.status == 200) {
resolve(JSON.parse(request.response)); // we got data here, so resolve the Promise
} else {
reject(Error(request.statusText)); // status is not 200 OK, so reject
}
};
request.onerror = function() {
reject(Error('Error fetching data.')); // error occurred, reject the Promise
};
request.send(); //send the request
});
}
async function main() {
let result = await jokeService();
document.write(result.value.joke);
}
main();
אפשר להתחיל לנתח את jokeService אבל היא לא מאוד מעניינת. בגדול פונקציה אסינכרונית שקוראת ל-API כלשהו – מה שחשוב היא שהיא מחזירה לנו promise. אני יכול לקרוא לה ובאמצעות then לחלץ ממנה את התוצאה. אבל למה? אני יכול לעשות את זה באופן יותר אינטואטיבי ויותר קל. אני אצור פונקציה ואשתמש בתוצאה של jokeService ברגע שהיא תהיה מוכנה בקוד רגיל באמצעות שימוש ב-async\wait! כמה קל, נעים ופשוט! await פשוט מחליף את then. זה הכל! זה הביג דיל!
הנה הדוגמה החיה שאפשר לשחק איתה:
See the Pen Async example – joke service by Ran Bar-Zik (@barzik) on CodePen.
async היא קלה לשימוש גם כאשר יש לי הבטחות מרובות:
function jokeService() {
return new Promise(function(resolve, reject) {
var request = new XMLHttpRequest();
request.open('GET', 'https://api.icndb.com/jokes/random');
request.onload = function() {
if (request.status == 200) {
resolve(JSON.parse(request.response)); // we got data here, so resolve the Promise
} else {
reject(Error(request.statusText)); // status is not 200 OK, so reject
}
};
request.onerror = function() {
reject(Error('Error fetching data.')); // error occurred, reject the Promise
};
request.send(); //send the request
});
}
function fortuneService() {
return new Promise(function(resolve, reject) {
var request = new XMLHttpRequest();
request.open('GET', 'https://helloacm.com/api/fortune/');
request.onload = function() {
if (request.status == 200) {
resolve(request.response); // we got data here, so resolve the Promise
} else {
reject(Error(request.statusText)); // status is not 200 OK, so reject
}
};
request.onerror = function() {
reject(Error('Error fetching data.')); // error occurred, reject the Promise
};
request.send(); //send the request
});
}
async function main() {
let joke = await jokeService();
let fortune = await fortuneService();
document.write(joke.value.joke + fortune);
}
main();
כאן יש לי שני שירותים – jokeService ו-fortuneService. שניהם אסינכרוניים כי הם הולכים לרשת ומביאים לי תוצאה מ-API. במקום לשבור את הראש על all ו-then, אני משתמש ב-async וב-await. תראו כמה זה קל ונעים והכי חשוב- אינטאוטיבי. רק כששני השירותים מוכנים, הם מחזירים את התוצאות והקוד בתוך הפונקציה האסינכרונית יכול לרוץ.
הנה הדוגמה החיה:
See the Pen Async example – multiple services by Ran Bar-Zik (@barzik) on CodePen.
אלו שיותר מנוסים ב-promises יראו כמעט מייד את הבעיה פה. הבעיה היא שבניגוד ל-then, אין לנו יכולת להחליט מה יקרה אם ה-promise ייכשל. הרי במימוש רגיל של promise, ה-then מקבל שתי פונקציות – אחת שמחליטה מה קורה אם ההבטחה מתממשת והשניה מה קורה אם לא. אני אדגים למי ששכח:
function someService() {
return new Promise(function(resolve, reject) {
setTimeout(ev => {
reject('Promise rejected');
}, 500);
});
}
someService().then(
result => {}, //Will never happened
err => {console.log(err)} //Promise rejected
)
הנה הדוגמה החיה:
See the Pen Rejected promise by Ran Bar-Zik (@barzik) on CodePen.
מה יש לנו פה? יש לי את someService שתמיד עושה reject. כשאני קורא לו, אני מעביר ב-then שתי פונקציות. הראשונה שתפעל אם הכל תקין והשניה שתפעל במקרה של reject. כיוון ש-someService היא ילד קקה, היא תמיד תעשה reject ותמיד נקבל הדפסה של שגיאה. איך אני מממש דבר כזה עם async ו-await? מאוד פשוט. עם try catch! כיוון ש-async חוזרת בעצם לסינטקס הרגיל של ג׳אווהסקריםט, אין שום בעיה להשתמש בסינטקס הרגיל של השפה!
function someService() {
return new Promise(function(resolve, reject) {
setTimeout(ev => {
reject('Promise rejected :P');
}, 500);
});
}
async function main() {
let result;
try {
result = await someService();
console.log(result); //Will never happen
} catch(err) {
document.write(err); //"Promise rejected :P"
}
}
main();
ממש ממש קל, לא? הנה הדוגמה החיה למי שרוצה לשחק:
See the Pen Rejected async by Ran Bar-Zik (@barzik) on CodePen.
כפי שאםפשר לראות, למרות ש-async נראה מאיים ומפחיד קצת, הוא בעצם מפשט מאוד את החיים ומקל עליהם. אין שום סיבה שלא להשתמש בו כבר היום ולהמיר את המימוש של הpromises לקוד יותר אלגנטי. לא לשכוח שה-promises לא נעלמות לשום מקום ואנחנו משתמשים בהן ליצירת הפונקציות האסינכרוניות שאנו קוראים להם.
כמו כן, נכון לעכשיו, אין דרך מספיק טובה לעשות אמולציה ל promise.all.
ה-async, בגדול, הוא סוכר סינטקסטי מעל הגנרטורים. כלומר המנגנון שמפעיל אותו הוא זה של הגנרטורים והוא עובד בדיוק כמוהם. ה-await הוא בעצם ה-yield. אם תסתכלו על הקוד ש-babel עובר עליו תראו שהוא עושה את הטרנספורמציה הזו מ-async לגנרטורים. אבל זה באמת לא נדרש לשימוש ראשוני והבנה בסיסית של async.
8 תגובות
יפה מאד!
פיצ'ר שקיים כבר מזמן בשפות נורמליות אחרות (כמו C# למשל..),
ובאמת הגיע הזמן שיכנס גם לJS..
תודה על ההשקעה והכתיבה בעברית!!
Node 7.6.0 תומך בasyncגם ללא harmony flag
צודק! תודה 🙂
כרגיל, הסבר ברור ומובן גם לדברים שקודם היו נראים מסובכים.
תודה רבה
תודה רן על המאמר המושקע
עדכון: לא רק none תומכת בזה, גם פיירפוקס וכרום כבר תומכים ב async
ואגב, השימוש ב async בריבוי משימות לא מוצלח כמו השימוש ב Promise.all, כיון שבאסינכרונית הפונקציה השניה מתחילה לרוץ רק אחרי שהראשונה הסתיימה, אבל ב Promise.all שתי הפונקציות רצות במקביל, וזה חוסך זמן
תודה! אין לי אלא להסכים עם מה שכתבת.
מה שאתה צריך לעשות זה לדוגמא במקרה שלך במקום:
let joke = await jokeService();
let fortune = await fortuneService();
לכתוב:
let [joke, fortune] = await Promise.all(jokeService(), fortuneService());
תודה!
ההסבר ברור ופשוט.