การดู : 130

08/05/2026 06:52น.

ไดอะแกรมแสดงการทำงานของ JavaScript Event Loop เชื่อมโยงระหว่าง Call Stack, Web APIs และ Task Queue

Event Loop คืออะไร? สรุปกลไก JavaScript Runtime และลำดับการทำงานที่ Dev ต้องรู้

#Event Loop

#JavaScript Runtime

#Microtask

#Macrotask

#Asynchronous JavaScript

#JavaScript

#เรียนเขียนโปรแกรม

เคยสงสัยไหมครับว่า ทำไม JavaScript ที่ทำงานแบบ Single-threaded (จัดการได้ทีละงาน) ถึงสามารถรับส่งข้อมูลจาก API, ประมวลผลการพิมพ์คีย์บอร์ด และรัน Animation ลื่นๆ บนหน้าจอไปพร้อมกันได้โดยไม่เกิดอาการหน้าจอค้าง?

ความลับนี้ไม่ได้ซ่อนอยู่ในไวยากรณ์ของภาษาครับ แต่มันคือการทำงานร่วมกันอย่างเป็นระบบระหว่าง JavaScript Engine (อย่าง V8) และกลไกที่เรียกว่า Event Loop

วันนี้เราจะมาแกะรอยการทำงานแบบ Non-blocking I/O ที่ทำให้ JavaScript กลายเป็นภาษาที่ทรงพลังที่สุดตัวหนึ่งในโลกของการพัฒนาเว็บกันครับ!

Event Loop คืออะไร?

ถ้าจะนิยามให้สั้นที่สุด Event Loop คือ กลไกการจัดการลำดับการทำงาน (Orchestrator) ของ JavaScript ครับ

แม้ว่าตัวภาษา JavaScript จะทำงานแบบ Single-threaded คือมีทางเดินเดียวและทำได้ทีละอย่าง (เหมือนถนนเลนเดียว) แต่ Event Loop คือสิ่งที่เข้ามาช่วยบริหารจัดการว่างานไหนควรทำทันที และงานไหนควรส่งไปรอทำทีหลัง เพื่อให้โปรแกรมสามารถทำงานหลายอย่างพร้อมกันได้ (Asynchronous) โดยไม่เกิดอาการค้างหรือรอคำสั่งนานเกินไป

หน้าที่หลักของมันมีเพียงอย่างเดียวคือ: การเฝ้าดู (Monitoring) มันจะคอยส่องดูว่าถ้า Call Stack ว่างลงเมื่อไหร่ มันจะไปหยิบงานที่รออยู่ใน Task Queue ขึ้นมาทำต่อทันที กระบวนการนี้จะวนซ้ำไปเรื่อยๆ เป็นวงกลม จึงเป็นที่มาของชื่อคำว่า Loop นั่นเองครับ

เจาะลึก 4 ส่วนประกอบหลักที่ทำให้ JavaScript ทำงานได้

การจะเข้าใจ Event Loop เราต้องเห็นภาพรวมก่อนว่า JavaScript Runtime (ไม่ว่าจะเป็น V8 ใน Chrome หรือ Node.js) แบ่งหน้าที่การจัดการ Code ออกเป็นส่วนๆ ดังนี้ครับ:

1. Call Stack

ส่วนนี้คือที่ที่ JavaScript รันคำสั่งต่างๆ ตามลำดับ (Single Thread) โดยทำงานแบบ LIFO (Last In, First Out) เมื่อมีการเรียก Function ระบบจะดัน (Push) เข้าไปใน Stack และเมื่อทำงานเสร็จถึงจะดึง (Pop) ออกมา

2. Web APIs / Node.js APIs

นี่คือส่วนที่ช่วยให้ JavaScript ทำงานแบบ Asynchronous ได้จริง เพราะคำสั่งประเภทที่ต้องรอ เช่น setTimeout, fetch (Network Request) หรือการดักจับ Event ต่างๆ จะถูกส่งมาจัดการที่นี่แทน เพื่อไม่ให้ไปขวางการรัน Code ส่วนอื่นใน Call Stack

3. Task Queue (Callback Queue)

เมื่อคำสั่งในส่วน Web APIs ทำงานเสร็จสิ้น (เช่น ดึงข้อมูลเสร็จ หรือครบเวลาที่ตั้งไว้) ตัว Callback Function จะถูกส่งมาต่อคิวที่นี่เพื่อรอจังหวะกลับเข้าไปรันต่อ

4. Event Loop

ส่วนนี้ทำหน้าที่เป็นตัวประสานงานเพียงอย่างเดียวครับ คือคอยตรวจสอบสถานะของ Call Stack ถ้า Stack ว่างเมื่อไหร่ มันจะดึงคิวงานจาก Task Queue ขึ้นไปประมวลผลทันที กระบวนการนี้เกิดขึ้นซ้ำๆ ตลอดเวลาที่โปรแกรมทำงาน

ลำดับการทำงาน (Execution Flow) ทีละขั้นตอน

ลองมาดู Code ชุดนี้ที่มักจะถูกนำไปใช้ทดสอบไหวพริบในบทสัมภาษณ์กันบ่อยๆ ครับ:

JavaScript

console.log("Start");

setTimeout(() => {
  console.log("Timeout 0s");
}, 0);

Promise.resolve().then(() => {
  console.log("Promise");
});

console.log("End");

สิ่งที่เกิดขึ้นเบื้องหลังแบบ Step-by-Step:

  1. console.log("Start"): ถูกดันเข้า Call Stack และแสดงผล "Start" ออกมาทันที จากนั้นก็ Pop ออกจาก Stack

  2. setTimeout (0ms): เมื่อทำงานใน Stack มันจะส่ง Callback ไปให้ Web APIs จัดการนับเวลา แม้จะตั้งไว้ 0 วิ แต่มันต้องถูกส่งไปต่อคิวที่ Macrotask Queue (หรือ Task Queue) เสมอ

  3. Promise.resolve(): ส่วนของ .then() จะถูกส่งไปที่ Microtask Queue ซึ่งเป็นคิวที่มีลำดับความสำคัญสูงกว่าคิวปกติ

  4. console.log("End"): ทำงานใน Call Stack เป็นลำดับสุดท้ายของ Code ชุดหลัก และแสดงผล "End"

ช่วงเวลาตัดสินของ Event Loop:

เมื่อ Call Stack ว่างลง Event Loop จะเริ่มทำงานตามกฎลำดับความสำคัญ (Priority) ดังนี้:

  • ตรวจสอบ Microtask Queue ก่อน: มันจะจัดการงานในคิวนี้ให้หมดเกลี้ยง ดังนั้น Promise จึงถูกดึงกลับมาแสดงผลก่อน

  • ตรวจสอบ Macrotask Queue (Task Queue): เมื่อ Microtask ว่างแล้ว มันถึงจะไปหยิบงานจากคิวปกติมาทำ นั่นคือ Timeout 0s

ผลลัพธ์ที่ได้ (Output):

Plaintext

Start
End
Promise
Timeout 0s

Microtask vs Macrotask: ใครสำคัญกว่ากัน?

ในระบบของ Event Loop คิวงานไม่ได้มีแค่แถวเดียวครับ แต่มันถูกแยกออกเป็น 2 ประเภทใหญ่ๆ ตามลำดับความสำคัญ (Priority) ดังนี้:

1. Microtask Queue (คิวสิทธิพิเศษ)

เป็นคิวที่ Event Loop จะให้ความสำคัญเป็นอันดับแรก หลังจากที่ Call Stack ทำงานปัจจุบันเสร็จสิ้น ระบบจะวิ่งมาเช็คคิวนี้ทันที และต้องทำให้หมดเกลี้ยงก่อนที่จะขยับไปทำอย่างอื่น

  • คำสั่งที่ใช้คิวนี้: Promises (พวก .then, .catch, .finally), MutationObserver และใน Node.js คือ process.nextTick

2. Macrotask Queue (คิวปกติ)

หรือที่บางครั้งเรียกว่า Task Queue เป็นคิวสำหรับงานทั่วไปที่รอได้

  • คำสั่งที่ใช้คิวนี้: setTimeout, setInterval, setImmediate, I/O Tasks รวมถึงการดักจับเหตุการณ์จาก User เช่น การคลิก (Click Events)

ลำดับการทำงานที่เกิดขึ้นจริง (The Cycle)

เพื่อให้เห็นภาพลำดับความสำคัญ ลองดูวงจรที่ Event Loop ทำงานใน 1 รอบ (Tick) ครับ:

  1. จัดการงานใน Call Stack จนว่าง

  2. จัดการงานทั้งหมดใน Microtask Queue จนหมด (ถ้าในระหว่างทำ มี Microtask ใหม่เกิดขึ้น มันจะทำต่อทันทีจนกว่าจะว่างจริงๆ)

  3. (เฉพาะใน Browser) ทำการ Render หน้าจอใหม่ (ถ้าถึงรอบการวาด)

  4. หยิบงานชิ้นเดียวจาก Macrotask Queue ขึ้นไปทำใน Call Stack

  5. วนกลับไปเริ่มข้อ 1 ใหม่

Pro Tip: นี่คือเหตุผลที่ setTimeout(..., 0) ไม่เคยทำงานใน 0 วินาทีจริงๆ เพราะมันต้องรอให้ Microtask (เช่นการจัดการข้อมูลจาก API) และรอบการ Render ทำงานให้เสร็จสิ้นก่อน ตัวมันถึงจะได้สิทธิ์ถูกหยิบขึ้นมา

ทำไม Developer ถึงต้องใส่ใจเรื่อง Event Loop?

การเข้าใจกลไกนี้ไม่ใช่แค่เพื่อเอาไปตอบคำถามสัมภาษณ์งานครับ แต่มันคือหัวใจสำคัญในการเขียนโปรแกรมให้มีประสิทธิภาพ โดยเฉพาะในประเด็นเรื่อง Blocking the Event Loop หรืออาการหน้าจอค้าง (UI Freeze) ที่ส่งผลเสียต่อ User Experience โดยตรง

อย่าทำแบบนี้: การยึดครอง Call Stack

เนื่องจาก JavaScript มีเพียง Thread เดียวในการประมวลผล Code และจัดการ UI หากคุณมีคำสั่งที่ใช้เวลาคำนวณนานเกินไปค้างอยู่ใน Call Stack จะทำให้ Event Loop ไม่สามารถไปหยิบงานอื่น (เช่น การคลิก หรือการวาดหน้าจอ) มาทำได้เลย

JavaScript

// ตัวอย่าง: การประมวลผลข้อมูลขนาดใหญ่ที่ขวางทางงานอื่น
function heavyTask() {
  const start = Date.now();
  while (Date.now() - start < 5000) {
    // Loop นรกที่ค้างอยู่ใน Stack นาน 5 วินาที
    // ในช่วงเวลานี้ User จะกดอะไรไม่ได้เลย หน้าเว็บจะค้าง (Not Responding)
  }
  console.log("งานหนักเสร็จแล้ว!");
}

แนวทางการแก้ไขเพื่อประสิทธิภาพสูงสุด

หากคุณเจองานที่ต้องคำนวณหนักๆ (CPU Intensive) มี 2 ทางเลือกที่แนะนำครับ:

  1. ใช้ Web Workers (สำหรับ Browser): แยกการคำนวณที่ซับซ้อนออกไปรันใน Background Thread อื่นที่ไม่ใช่ Main Thread วิธีนี้จะช่วยให้หน้าจอของคุณยังคงตอบสนอง (Responsive) ได้ตามปกติ แม้จะมีการคำนวณหนักอยู่เบื้องหลังก็ตาม

  2. การแบ่งงาน (Task Partitioning): หากไม่สามารถใช้ Web Workers ได้ ให้แบ่งงานใหญ่ๆ ออกเป็นส่วนเล็กๆ แล้วใช้ setTimeout หรือ requestAnimationFrame เพื่อ Yield หรือคืนสิทธิ์การควบคุมกลับไปให้ Event Loop ได้มีโอกาสไปจัดการงาน UI หรือ Event อื่นๆ บ้าง ก่อนจะกลับมาทำงานส่วนที่เหลือต่อ

FAQ: คำถามที่พบบ่อยเกี่ยวกับ Event Loop

1. ถ้าตั้ง setTimeout เป็น 0 (ms) ทำไมมันถึงไม่รันทันที?

คำตอบ: เพราะ setTimeout เป็น Macrotask ครับ แม้จะตั้งเวลาไว้ 0 มิลลิวินาที แต่มันทำได้เพียงแค่ส่งงานไปต่อคิว ใน Macrotask Queue เท่านั้น ตามกฎของ Event Loop มันต้องรอให้ Call Stack ว่าง และรอให้ Microtask Queue (เช่น Promise) ทำงานจนหมดเกลี้ยงก่อน มันถึงจะมีสิทธิ์ถูกดึงกลับมาทำงานครับ

2. Node.js กับ Browser มี Event Loop เหมือนกันไหม?

คำตอบ: หลักการพื้นฐานเหมือนกันครับ แต่ไส้ในทำงานต่างกันเล็กน้อย Browser ใช้ libevent (เป็นส่วนหนึ่งของ Chrome) ส่วน Node.js ใช้ library ที่ชื่อว่า libuv เพื่อจัดการงานที่เป็น I/O นอกจากนี้ใน Node.js จะมีคิวพิเศษเพิ่มเติมอย่าง process.nextTick ที่รันก่อน Microtask อื่นๆ อีกด้วยครับ

3. เราสามารถเพิ่ม Thread ให้ JavaScript ได้ไหม?

คำตอบ: ตัว JavaScript Engine หลักยังคงทำงานแบบ Single Thread ครับ แต่เราสามารถทำสิ่งที่เรียกว่า Parallel Programming ได้ผ่าน Web Workers (ใน Browser) หรือ Worker Threads (ใน Node.js) ซึ่งเป็นการแยก Thread ออกไปประมวลผลต่างหากโดยไม่กวน Main Thread ครับ

4. ทำไมการใช้ Promise ถึงลื่นกว่าการเขียน Loop ยาวๆ?

คำตอบ: เพราะ Promise ทำงานแบบ Asynchronous ผ่าน Microtask Queue ครับ มันช่วยให้เราสามารถสั่งงานทิ้งไว้แล้วไปทำอย่างอื่นต่อได้ (Non-blocking) ต่างจากการเขียน Loop ยาวๆ ใน Call Stack ที่จะยึดครอง Thread ไว้เพียงคนเดียวจนกว่าจะเสร็จ (Blocking)

5. Event Loop มีโอกาสหยุดทำงานไหม?

คำตอบ: มีครับ ถ้าเกิดสิ่งที่เรียกว่า Infinite Loop ใน Call Stack (เช่น while(true) {}) Event Loop จะถูก Block อย่างสมบูรณ์และไม่มีโอกาสได้ไปหยิบงานจาก Queue ไหนมาทำเลย ผลก็คือโปรแกรมจะ Crash หรือหน้าเว็บค้างนั่นเองครับ


สรุป: หัวใจสำคัญของ Event Loop

สรุปสั้นๆ ให้เห็นภาพวงจรชีวิตของ JavaScript ที่รันอยู่ทุกวินาที:

  1. รันงานหลัก: จัดการงานใน Call Stack ให้เสร็จสิ้น

  2. เก็บกวาดงานด่วน: เคลียร์งานใน Microtask Queue (เช่น Promise) ให้หมดเกลี้ยง

  3. อัปเดตหน้าจอ: Browser ทำการ Render หน้าจอ (ถ้าถึงรอบ)

  4. ดึงงานคิวถัดไป: Event Loop หยิบงานจาก Macrotask Queue (เช่น setTimeout) ขึ้นมาทำ

  5. วนซ้ำ: กลับไปเริ่มที่ข้อ 1 ใหม่ตลอดการทำงาน

กลไกนี้เองที่ทำให้ JavaScript กลายเป็นภาษาที่มีประสิทธิภาพสูงในด้าน I/O และสามารถจัดการงานจำนวนมากได้พร้อมกันอย่างลื่นไหล แม้จะมีเพียง Thread เดียวก็ตาม

หวังว่าบทความนี้จะช่วยให้เพื่อนๆ ชาว Superdev เข้าใจเบื้องหลังความแรงแบบ Non-blocking ของ JavaScript ได้ชัดเจนขึ้นนะครับ และถ้าไม่อยากพลาดบทความเทคนิคเจาะลึก หรืออัปเดตเทคโนโลยีใหม่ๆ ที่จะช่วยให้การเขียนโปรแกรมของคุณสนุกและโปรยิ่งกว่าเดิม...

ฝากกดติดตามพวกเราได้ที่ Superdev Academy ในทุกช่องทางนะครับ!

  • 🔵 Facebook: Superdev Academy Thailand (อัปเดตข่าวสารและบทความใหม่)

  • 🎬 YouTube: Superdev Academy Channel (ติวเข้มแบบวิดีโอ)

  • 📸 Instagram: @superdevacademy (เกร็ดความรู้สั้นๆ และเบื้องหลังการทำงาน)

  • 🎬 TikTok: @superdevacademy (Tips & Tricks ฉบับย่อยง่าย)

  • 🌐 Website: superdevacademy.com (คลังบทความและคอร์สเรียนฉบับเต็ม)