Kodokon kodokon.com

อะซิงโครนัสเชิงลึก: promise, async/await, การจัดการข้อผิดพลาด

ใช้ประโยชน์จาก event loop, ทำให้ promise ของคุณทำงานคู่ขนานอย่างถูกต้อง และทำให้การจัดการข้อผิดพลาดแบบอะซิงโครนัสเชื่อถือได้

11 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

โมเดลการทำงานอาศัยคิว macrotask (setTimeout, I/O) และคิว microtask (.then, await, queueMicrotask) หลังจากแต่ละงาน เอนจินจะระบายคิว microtask ให้หมด ก่อนจะไปทำงานต่อ ผลที่ตามมาโดยตรง: promise ที่ resolve แล้วจะทำงานก่อน setTimeout(fn, 0) เสมอ และเชนของ microtask ที่ยาวเกินไปก็สามารถทำให้การเรนเดอร์ค้างได้แน่นอนพอ ๆ กับลูปแบบซิงโครนัส

JAVASCRIPT
console.log("start");
setTimeout(() => console.log("macrotask"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("end");
// start, end, microtask, macrotask
microtask ทำงานก่อน macrotask ที่ค้างอยู่ใด ๆ

async/await คือน้ำตาลเคลือบ (syntactic sugar) ที่อยู่บน promise แต่มันซ่อนกับดักด้านประสิทธิภาพไว้: await แต่ละตัวภายในลูปจะ ทำให้การทำงานเป็นลำดับ หากคำร้องขอเป็นอิสระต่อกัน จงเริ่มมันทั้งหมดก่อน แล้วค่อยรอทั้งชุดด้วย Promise.all สำหรับคำร้องขอ 100 มิลลิวินาทีสามรายการ เวอร์ชันแบบลำดับใช้เวลา 300 มิลลิวินาที ส่วนเวอร์ชันคู่ขนานใช้ 100 มิลลิวินาที

JAVASCRIPT
const fetchUser = (id) => new Promise((res) => {
  setTimeout(() => res({ id }), 100);
});
async function loadSequential(ids) {
  const users = [];
  for (const id of ids) {
    users.push(await fetchUser(id)); // ~100 ms each
  }
  return users;
}
async function loadParallel(ids) {
  return Promise.all(ids.map(fetchUser)); // ~100 ms total
}
ผลลัพธ์เหมือนกัน แต่ความหน่วงถูกหารด้วยจำนวนการเรียก

จงเลือกตัวรวม (combinator) ที่ถูกต้องตามสัญญาความล้มเหลวที่คุณต้องการ: Promise.all reject เมื่อล้มเหลว ครั้งแรก (ทั้งหมดหรือไม่มีเลย), allSettled รอทุกตัวและอธิบายผลลัพธ์แต่ละอัน, race เอาการเสร็จสิ้นครั้งแรก (สำเร็จหรือล้มเหลว มีประโยชน์สำหรับ timeout), any เอาความสำเร็จครั้งแรกและจะ reject ก็ต่อเมื่อทุกอย่างล้มเหลวเท่านั้น สำหรับแดชบอร์ดที่ต้องแสดงผลลัพธ์บางส่วน allSettled มักเป็นตัวเลือกที่ถูกต้องเสมอ

JAVASCRIPT
const tasks = [
  Promise.resolve("profile loaded"),
  Promise.reject(new Error("network unavailable")),
];
const results = await Promise.allSettled(tasks);
for (const r of results) {
  if (r.status === "fulfilled") {
    console.log("success:", r.value);
  } else {
    console.error("failure:", r.reason.message);
  }
}
ทุกผลลัพธ์ถูกอธิบาย ไม่มีความล้มเหลวใดซ่อนความล้มเหลวอื่น

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. console.log แบบซิงโครนัส, คอลแบ็ก Promise.resolve().then, และคอลแบ็ก setTimeout(fn, 0) ทำงานตามลำดับใด?
    • ซิงโครนัส, setTimeout, then
    • ซิงโครนัส, then, setTimeout
    • then, ซิงโครนัส, setTimeout
  2. คุณยิงคำร้องขออิสระสิบรายการ วิธีใดลดความหน่วงรวมได้มากที่สุด?
    • หนึ่ง await ต่อหนึ่งคำร้องขอภายในลูป for...of
    • สร้าง promise ทั้งหมด แล้ว await Promise.all
    • เชน .then ต่อกันทีละอัน
    • ใช้ setTimeout เพื่อเว้นระยะห่างระหว่างการเรียก
  3. ทำไม try { doAsync(); } catch (e) {} ที่ไม่มี await จึงจับการ reject จาก doAsync ไม่ได้?
    • เพราะ catch ใช้งานไม่ได้ภายในฟังก์ชัน async
    • เพราะการ reject เกิดขึ้นหลังจากบล็อก try ได้ออกไปแล้ว เนื่องจาก promise ไม่เคยถูก await
    • เพราะ promise จับได้ด้วย .catch เท่านั้น