Kodokon kodokon.com

Event loop: call stack, microtask, macrotask

ทำความเข้าใจว่าเอนจินจัดลำดับการทำงานของโค้ดแบบซิงโครนัส microtask และ macrotask อย่างไรจริง ๆ

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

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

JavaScript รันโค้ดของคุณบน เธรดเดียว แต่ event loop ไม่ได้ถูกกำหนดโดย ECMAScript แต่เป็นสเปกของ HTML (และ libuv ในฝั่ง Node.js) ที่อธิบายว่ามันทำงานอย่างไร เอนจินจะเก็บ call stack สำหรับโค้ดที่กำลังรันอยู่ คิวของ macrotask (timer, I/O, event) และคิวของ microtask (Promise.prototype.then, queueMicrotask, MutationObserver) กฎทองคือ หลังจากแต่ละ macrotask คิวของ microtask จะถูก ระบายจนหมดเกลี้ยง ก่อนที่จะคืนการควบคุมกลับไป

JAVASCRIPT
console.log("script start");

setTimeout(() => console.log("timeout"), 0);

Promise.resolve()
  .then(() => console.log("promise 1"))
  .then(() => console.log("promise 2"));

console.log("script end");
// script start, script end,
// promise 1, promise 2, timeout
microtask จะรันก่อน macrotask เสมอ

สคริปต์เริ่มต้นเองก็เป็น macrotask หนึ่ง กล่าวคือ จนกว่ามันจะทำงานเสร็จ อย่างอื่นจะยังไม่รัน เมื่อ call stack ว่างเปล่า เอนจินจะระบายคิวของ microtask รวมถึง microtask ที่ถูกเพิ่มเข้ามาระหว่างการระบายด้วย รายละเอียดนี้อธิบายว่าทำไมการเรียก then สองครั้งที่ต่อกันจึงรันก่อน setTimeout(fn, 0) เพราะแต่ละ then จะจัดคิว microtask อีกตัวหนึ่ง และการควบคุมจะไม่ถูกคืนกลับไปจนกว่าคิวจะว่างเปล่า

JAVASCRIPT
setTimeout(() => console.log("macro 1"), 0);
setTimeout(() => console.log("macro 2"), 0);

queueMicrotask(() => {
  console.log("micro 1");
  queueMicrotask(() => console.log("micro 2"));
});
// micro 1, micro 2, macro 1, macro 2
microtask ที่ซ้อนอยู่ข้างในจะรันก่อน macrotask ใด ๆ

คีย์เวิร์ด await สร้างขึ้นบนกลไกนี้ มันจะ หยุดพัก ฟังก์ชัน async คืนการควบคุมกลับไปยังผู้เรียก แล้วจัดคิวการทำงานต่อในรูปของ microtask แม้แต่ await null ก็ทริกเกอร์การหยุดพักนี้ เพราะค่าจะถูกห่อไว้ใน promise ที่ resolve แล้ว นั่นคือเหตุผลว่าทำไมโค้ดที่วางไว้หลัง await จึงไม่รันแบบซิงโครนัส

JAVASCRIPT
async function main() {
  console.log("before await");
  await null;
  console.log("after await");
}

main();
console.log("sync code");
// before await, sync code, after await
await หยุดพักฟังก์ชันและจัดคิวส่วนที่เหลือใหม่

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

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

  1. สคริปต์หนึ่ง log C แบบซิงโครนัส จัดคิว A ผ่าน setTimeout(fn, 0) และ B ผ่าน Promise.resolve().then ตัวอักษรจะปรากฏในลำดับใด?
    • A, B, C
    • C, A, B
    • C, B, A
    • B, C, A
  2. คิว microtask ถูกระบายเมื่อใด?
    • หลังแต่ละ macrotask จนหมดเกลี้ยง รวมถึง microtask ที่ถูกเพิ่มระหว่างการระบาย
    • มี microtask เพียงตัวเดียวเท่านั้นที่รันระหว่าง macrotask สองตัว
    • ทุก 16 ms ก่อนที่หน้าจะถูกเรนเดอร์
    • เฉพาะเมื่อ macrotask ทั้งหมดถูกประมวลผลเสร็จแล้วเท่านั้น
  3. จริง ๆ แล้ว await ทำอะไรเบื้องหลัง?
    • มันบล็อกเธรดจนกว่า promise จะ resolve
    • มันหยุดพักฟังก์ชัน async และจัดคิวการทำงานต่อในรูปของ microtask
    • มันเปลี่ยนส่วนที่เหลือของฟังก์ชันให้เป็น macrotask
    • มันไม่ทำอะไรเลยถ้าค่าที่ await ไม่ใช่ promise