ผู้เขียนบทความตรวจสอบตัวอย่างของ Async/Await ใน JavaScript โดยรวมแล้ว Async/Await เป็นวิธีที่สะดวกในการเขียนโค้ดแบบอะซิงโครนัส ก่อนที่ฟีเจอร์นี้จะปรากฏขึ้น โค้ดดังกล่าวถูกเขียนโดยใช้การเรียกกลับและคำสัญญา ผู้เขียนบทความต้นฉบับเปิดเผยข้อดีของ Async/Await โดยการวิเคราะห์ตัวอย่างต่างๆ
เราเตือนคุณ: สำหรับผู้อ่าน "Habr" ทุกคน - ส่วนลด 10 rubles เมื่อลงทะเบียนในหลักสูตร Skillbox ใด ๆ โดยใช้รหัสส่งเสริมการขาย "Habr"
Skillbox แนะนำ: หลักสูตรออนไลน์เพื่อการศึกษา
“นักพัฒนาจาวา” .
โทรกลับ
Callback เป็นฟังก์ชันที่การโทรล่าช้าอย่างไม่มีกำหนด ก่อนหน้านี้มีการใช้การเรียกกลับในพื้นที่ของโค้ดที่ไม่สามารถรับผลลัพธ์ได้ทันที
นี่คือตัวอย่างของการอ่านไฟล์แบบอะซิงโครนัสใน Node.js:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
});
ปัญหาเกิดขึ้นเมื่อคุณจำเป็นต้องดำเนินการอะซิงโครนัสหลายๆ รายการพร้อมกัน ลองจินตนาการถึงสถานการณ์นี้: มีการร้องขอไปยังฐานข้อมูลผู้ใช้ Arfat คุณต้องอ่านฟิลด์ profile_img_url และดาวน์โหลดรูปภาพจากเซิร์ฟเวอร์ someserver.com
หลังจากดาวน์โหลด เราจะแปลงรูปภาพเป็นรูปแบบอื่น เช่น จาก PNG เป็น JPEG หากการแปลงสำเร็จ จดหมายจะถูกส่งไปยังอีเมลของผู้ใช้ จากนั้น ข้อมูลเกี่ยวกับเหตุการณ์จะถูกป้อนลงในไฟล์ changes.log ซึ่งระบุวันที่
ควรให้ความสนใจกับการทับซ้อนกันของการโทรกลับและ }) จำนวนมากในส่วนสุดท้ายของโค้ด เรียกว่า Callback Hell หรือ Pyramid of Doom
ข้อเสียของวิธีนี้ชัดเจน:
- รหัสนี้อ่านยาก
- นอกจากนี้ยังเป็นเรื่องยากที่จะจัดการกับข้อผิดพลาด ซึ่งมักจะทำให้โค้ดมีคุณภาพต่ำ
เพื่อแก้ไขปัญหานี้ จึงมีการเพิ่มคำสัญญาลงใน JavaScript พวกมันอนุญาตให้คุณแทนที่การซ้อนการโทรกลับแบบลึกด้วยคำว่า .then
ด้านบวกของคำมั่นสัญญาคือทำให้โค้ดอ่านได้ดีขึ้นมาก จากบนลงล่าง แทนที่จะอ่านจากซ้ายไปขวา อย่างไรก็ตาม คำสัญญาก็มีปัญหาเช่นกัน:
- คุณต้องเพิ่ม .then จำนวนมาก
- แทนที่จะลอง/จับ .catch ใช้เพื่อจัดการกับข้อผิดพลาดทั้งหมด
- การทำงานกับสัญญาหลายรายการภายในลูปเดียวนั้นไม่สะดวกเสมอไป ในบางกรณี จะทำให้โค้ดซับซ้อนขึ้น
นี่คือปัญหาที่จะแสดงความหมายของจุดสุดท้าย
สมมติว่าเรามี for loop ที่พิมพ์ลำดับตัวเลขตั้งแต่ 0 ถึง 10 ในช่วงเวลาสุ่ม (0–n วินาที) เมื่อใช้คำสัญญา คุณต้องเปลี่ยนการวนซ้ำนี้เพื่อให้ตัวเลขถูกพิมพ์ตามลำดับตั้งแต่ 0 ถึง 10 ดังนั้น หากใช้เวลา 6 วินาทีในการพิมพ์เลขศูนย์ และ 2 วินาทีในการพิมพ์เลขหนึ่ง ควรพิมพ์เลขศูนย์ก่อน จากนั้น การนับถอยหลังสำหรับการพิมพ์จะเริ่มขึ้น
และแน่นอนว่าเราไม่ได้ใช้ Async/Await หรือ .sort เพื่อแก้ไขปัญหานี้ ตัวอย่างวิธีแก้ปัญหาอยู่ตอนท้าย
ฟังก์ชันอะซิงก์
การเพิ่มฟังก์ชันอะซิงก์ใน ES2017 (ES8) ทำให้งานทำงานตามสัญญาง่ายขึ้น ฉันสังเกตว่าฟังก์ชัน async ทำงาน "เหนือ" ของสัญญา ฟังก์ชันเหล่านี้ไม่ได้แสดงถึงแนวคิดที่แตกต่างกันในเชิงคุณภาพ ฟังก์ชั่น Async มีไว้เป็นทางเลือกแทนโค้ดที่ใช้สัญญา
Async/Await ทำให้สามารถจัดระเบียบงานด้วยโค้ดอะซิงโครนัสในรูปแบบซิงโครนัสได้
ดังนั้นการรู้คำมั่นสัญญาทำให้เข้าใจหลักการของ Async/Await ได้ง่ายขึ้น
วากยสัมพันธ์
โดยปกติจะประกอบด้วยคำหลักสองคำ: async และ await คำแรกจะเปลี่ยนฟังก์ชันเป็นแบบอะซิงโครนัส ฟังก์ชั่นดังกล่าวอนุญาตให้ใช้ await ได้ ในกรณีอื่น การใช้ฟังก์ชันนี้จะทำให้เกิดข้อผิดพลาด
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
Async จะถูกแทรกไว้ที่จุดเริ่มต้นของการประกาศฟังก์ชัน และในกรณีของฟังก์ชันลูกศร จะอยู่ระหว่างเครื่องหมาย “=” และวงเล็บ
ฟังก์ชั่นเหล่านี้สามารถวางไว้ในวัตถุเป็นวิธีการหรือใช้ในการประกาศคลาส
// As an object's method
const obj = {
async getName() {
return fetch('https://www.example.com');
}
}
// In a class
class Obj {
async getResource() {
return fetch('https://www.example.com');
}
}
หมายเหตุ! โปรดจำไว้ว่าตัวสร้างคลาสและตัวรับ/ตัวตั้งค่าไม่สามารถเป็นแบบอะซิงโครนัสได้
ความหมายและกฎการดำเนินการ
ฟังก์ชัน Async โดยพื้นฐานแล้วจะคล้ายกับฟังก์ชัน JS มาตรฐาน แต่มีข้อยกเว้นอยู่
ดังนั้นฟังก์ชัน async จะส่งคืนสัญญาเสมอ:
async function fn() {
return 'hello';
}
fn().then(console.log)
// hello
โดยเฉพาะ fn ส่งคืนสตริงสวัสดี เนื่องจากนี่เป็นฟังก์ชันอะซิงโครนัส ค่าสตริงจึงถูกรวมไว้ในสัญญาโดยใช้ตัวสร้าง
นี่คือการออกแบบทางเลือกที่ไม่มี Async:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// hello
ในกรณีนี้ สัญญาจะถูกส่งกลับ "ด้วยตนเอง" ฟังก์ชันอะซิงโครนัสจะถูกห่อหุ้มด้วยสัญญาใหม่เสมอ
หากค่าที่ส่งคืนเป็นค่าดั้งเดิม ฟังก์ชัน async จะส่งคืนค่าโดยห่อค่านั้นไว้ในสัญญา ถ้าค่าตอบแทนเป็นวัตถุสัญญา ความละเอียดจะถูกส่งกลับในสัญญาใหม่
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
แต่จะเกิดอะไรขึ้นหากมีข้อผิดพลาดภายในฟังก์ชันอะซิงโครนัส?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);
หากไม่ได้รับการประมวลผล foo() จะส่งคืนสัญญาพร้อมการปฏิเสธ ในสถานการณ์นี้ Promise.reject ที่มีข้อผิดพลาดจะถูกส่งกลับแทน Promise.resolve
ฟังก์ชัน Async จะแสดงสัญญาเสมอ โดยไม่คำนึงถึงสิ่งที่ส่งคืน
ฟังก์ชันอะซิงโครนัสหยุดชั่วคราวทุกครั้งที่ await
Await ส่งผลต่อนิพจน์ ดังนั้น หากนิพจน์เป็นสัญญา ฟังก์ชันอะซิงก์จะถูกระงับจนกว่าสัญญาจะบรรลุผล หากนิพจน์ไม่ใช่ Promise นิพจน์นั้นจะถูกแปลงเป็น Promise ผ่าน Promise.resolve จากนั้นจึงดำเนินการให้เสร็จสิ้น
// utility function to cause delay
// and get random value
const delayAndGetRandom = (ms) => {
return new Promise(resolve => setTimeout(
() => {
const val = Math.trunc(Math.random() * 100);
resolve(val);
}, ms
));
};
async function fn() {
const a = await 9;
const b = await delayAndGetRandom(1000);
const c = await 5;
await delayAndGetRandom(1000);
return a + b * c;
}
// Execute fn
fn().then(console.log);
และนี่คือคำอธิบายว่าฟังก์ชัน fn ทำงานอย่างไร
- หลังจากเรียกแล้ว บรรทัดแรกจะถูกแปลงจาก const a = await 9; ใน const a = รอ Promise.resolve (9);
- หลังจากใช้ Await การดำเนินการฟังก์ชันจะถูกระงับจนกว่าจะได้รับค่า (ในสถานการณ์ปัจจุบันคือ 9)
- DelayAndGetRandom(1000) หยุดการทำงานของฟังก์ชัน fn ชั่วคราวจนกว่าจะดำเนินการเสร็จสิ้น (หลังจาก 1 วินาที) การดำเนินการนี้จะหยุดฟังก์ชัน fn เป็นเวลา 1 วินาทีอย่างมีประสิทธิภาพ
- DelayAndGetRandom(1000) ผ่านการแก้ไขจะส่งกลับค่าสุ่ม ซึ่งจากนั้นจะกำหนดให้กับตัวแปร b
- กรณีที่มีตัวแปร c ก็คล้ายกับกรณีที่มีตัวแปร a หลังจากนั้น ทุกอย่างหยุดชั่วขณะ แต่ตอนนี้ DelayAndGetRandom(1000) ไม่ได้ส่งคืนสิ่งใดเลยเนื่องจากไม่จำเป็น
- เป็นผลให้ค่าถูกคำนวณโดยใช้สูตร a + b * c ผลลัพธ์จะรวมอยู่ในสัญญาโดยใช้ Promise.resolve และส่งคืนโดยฟังก์ชัน
การหยุดชั่วคราวเหล่านี้อาจชวนให้นึกถึงเครื่องกำเนิดไฟฟ้าใน ES6 แต่ก็มีบางอย่างอยู่
การแก้ปัญหา
ทีนี้เรามาดูวิธีแก้ปัญหาที่กล่าวมาข้างต้นกันดีกว่า
ฟังก์ชัน FinishMyTask ใช้ Await เพื่อรอผลลัพธ์ของการดำเนินการ เช่น queryDatabase, sendEmail, logTaskInFile และอื่นๆ หากคุณเปรียบเทียบวิธีแก้ปัญหานี้กับวิธีที่ใช้สัญญา ความคล้ายคลึงกันจะชัดเจน อย่างไรก็ตาม เวอร์ชัน Async/Await ช่วยลดความซับซ้อนทางวากยสัมพันธ์ทั้งหมดได้อย่างมาก ในกรณีนี้ ไม่มีการเรียกกลับและลูกโซ่เช่น .then/.catch จำนวนมาก
นี่คือวิธีแก้ปัญหาผลลัพธ์ของตัวเลข มีสองตัวเลือก
const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
let pr = Promise.resolve(0);
for (let i = 1; i <= 10; i += 1) {
pr = pr.then((val) => {
console.log(val);
return wait(i, Math.random() * 1000);
});
}
resolve(pr);
});
// Implementation Two (Using Recursion)
const printNumbersRecursive = () => {
return Promise.resolve(0).then(function processNextPromise(i) {
if (i === 10) {
return undefined;
}
return wait(i, Math.random() * 1000).then((val) => {
console.log(val);
return processNextPromise(i + 1);
});
});
};
และนี่คือวิธีแก้ปัญหาโดยใช้ฟังก์ชันอะซิงก์
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}
เกิดข้อผิดพลาดในการประมวลผล
ข้อผิดพลาดที่ไม่สามารถจัดการได้จะรวมอยู่ในสัญญาที่ถูกปฏิเสธ อย่างไรก็ตาม ฟังก์ชันอะซิงก์สามารถใช้ try/catch เพื่อจัดการข้อผิดพลาดพร้อมกันได้
async function canRejectOrReturn() {
// wait one second
await new Promise(res => setTimeout(res, 1000));
// Reject with ~50% probability
if (Math.random() > 0.5) {
throw new Error('Sorry, number too big.')
}
return 'perfect number';
}
canRejectOrReturn() เป็นฟังก์ชันอะซิงโครนัสที่จะสำเร็จ (“จำนวนสมบูรณ์”) หรือล้มเหลวโดยมีข้อผิดพลาด (“ขออภัย ตัวเลขใหญ่เกินไป”)
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
เนื่องจากตัวอย่างข้างต้นคาดว่า canRejectOrReturn จะดำเนินการ ความล้มเหลวของตัวเองจะส่งผลให้เกิดการดำเนินการของ catch block ด้วยเหตุนี้ ฟังก์ชัน foo จะลงท้ายด้วย undefinition (เมื่อไม่มีการส่งคืนสิ่งใดในบล็อก try) หรือโดยตรวจพบข้อผิดพลาด ด้วยเหตุนี้ ฟังก์ชันนี้จะไม่ล้มเหลวเนื่องจาก try/catch จะจัดการฟังก์ชัน foo เอง
นี่เป็นอีกตัวอย่างหนึ่ง:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
ควรให้ความสนใจกับข้อเท็จจริงที่ว่าในตัวอย่าง canRejectOrReturn จะถูกส่งคืนจาก foo Foo ในกรณีนี้จะลงท้ายด้วยตัวเลขสมบูรณ์หรือส่งคืนข้อผิดพลาด (“ขออภัย ตัวเลขใหญ่เกินไป”) catch block จะไม่ถูกดำเนินการ
ปัญหาคือ foo ส่งคืนสัญญาที่ส่งผ่านจาก canRejectOrReturn ดังนั้นวิธีแก้ปัญหาของ foo จึงกลายเป็นวิธีแก้ปัญหาของ canRejectOrReturn ในกรณีนี้ โค้ดจะประกอบด้วยสองบรรทัดเท่านั้น:
try {
const promise = canRejectOrReturn();
return promise;
}
นี่คือสิ่งที่จะเกิดขึ้นหากคุณใช้ await และ return ร่วมกัน:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'error caught';
}
}
ในโค้ดด้านบน foo จะออกได้สำเร็จโดยตรวจพบทั้งจำนวนสมบูรณ์และข้อผิดพลาด จะไม่มีการปฏิเสธที่นี่ แต่ foo จะกลับมาพร้อมกับ canRejectOrReturn ไม่ใช่โดยไม่ได้กำหนด มาตรวจสอบสิ่งนี้ด้วยการลบบรรทัด return await canRejectOrReturn() ออก:
try {
const value = await canRejectOrReturn();
return value;
}
// …
ข้อผิดพลาดและข้อผิดพลาดทั่วไป
ในบางกรณี การใช้ Async/Await อาจทำให้เกิดข้อผิดพลาดได้
รอจนลืม.
สิ่งนี้เกิดขึ้นค่อนข้างบ่อย - คีย์เวิร์ด await จะถูกลืมก่อนสัญญา:
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}
อย่างที่คุณเห็นไม่มีการรอหรือคืนโค้ด ดังนั้น foo จะออกโดยไม่ได้กำหนดเสมอโดยไม่มีการดีเลย์ 1 วินาที แต่คำสัญญาจะสำเร็จ หากเกิดข้อผิดพลาดหรือการปฏิเสธ ระบบจะเรียก UnhandledPromiseRejectionWarning
ฟังก์ชั่น Async ในการโทรกลับ
ฟังก์ชัน Async มักใช้ใน .map หรือ .filter เป็นการเรียกกลับ ตัวอย่างคือฟังก์ชัน fetchPublicReposCount(username) ซึ่งส่งคืนจำนวนที่เก็บที่เปิดอยู่บน GitHub สมมติว่ามีผู้ใช้สามคนที่เราต้องการเมตริก นี่คือรหัสสำหรับงานนี้:
const url = 'https://api.github.com/users';
// Utility fn to fetch repo counts
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}
เราต้องการบัญชี ArfatSalman, octocat, norvig ในกรณีนี้เราทำ:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
ควรให้ความสนใจกับ Await ในการโทรกลับ .map นี่คืออาร์เรย์ของคำสัญญา และ .map เป็นการเรียกกลับโดยไม่ระบุชื่อสำหรับผู้ใช้แต่ละรายที่ระบุ
การใช้ await สม่ำเสมอมากเกินไป
ลองใช้โค้ดนี้เป็นตัวอย่าง:
async function fetchAllCounts(users) {
const counts = [];
for (let i = 0; i < users.length; i++) {
const username = users[i];
const count = await fetchPublicReposCount(username);
counts.push(count);
}
return counts;
}
ที่นี่หมายเลข repo จะถูกวางไว้ในตัวแปรการนับ จากนั้นหมายเลขนี้จะถูกเพิ่มลงในอาร์เรย์การนับ ปัญหาเกี่ยวกับโค้ดคือจนกว่าข้อมูลของผู้ใช้รายแรกจะมาถึงจากเซิร์ฟเวอร์ ผู้ใช้รายต่อมาทั้งหมดจะอยู่ในโหมดสแตนด์บาย ดังนั้นจะมีการประมวลผลผู้ใช้เพียงรายเดียวในแต่ละครั้ง
ตัวอย่างเช่นหากใช้เวลาประมาณ 300 มิลลิวินาทีในการประมวลผลผู้ใช้หนึ่งรายดังนั้นสำหรับผู้ใช้ทั้งหมดจะใช้เวลาหนึ่งวินาทีแล้ว เวลาที่ใช้เป็นเส้นตรงขึ้นอยู่กับจำนวนผู้ใช้ แต่เนื่องจากการได้รับจำนวน repo ไม่ได้ขึ้นอยู่กับแต่ละกระบวนการ กระบวนการจึงสามารถขนานกันได้ สิ่งนี้ต้องทำงานร่วมกับ .map และ Promise.all:
async function fetchAllCounts(users) {
const promises = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
return Promise.all(promises);
}
Promise.all ได้รับอาร์เรย์ของสัญญาเป็นอินพุตและส่งคืนสัญญา อย่างหลัง หลังจากที่สัญญาทั้งหมดในอาเรย์เสร็จสมบูรณ์หรือเมื่อการปฏิเสธครั้งแรกเสร็จสิ้น อาจเกิดขึ้นได้ว่าพวกเขาทั้งหมดไม่ได้เริ่มพร้อมกัน - เพื่อให้แน่ใจว่าจะสตาร์ทพร้อมกัน คุณสามารถใช้ p-map
ข้อสรุป
ฟังก์ชั่น Async มีความสำคัญมากขึ้นสำหรับการพัฒนา สำหรับการใช้งานฟังก์ชั่น async แบบปรับตัวคุณควรใช้
Skillbox แนะนำ:
- หลักสูตรภาคปฏิบัติ
"นักพัฒนามือถือ PRO" .- สมัครเรียนออนไลน์
“นักวิเคราะห์ข้อมูลหลาม” .- หลักสูตรภาคปฏิบัติสองปี
"ฉันเป็นนักพัฒนาเว็บ PRO" .
ที่มา: will.com