เข้าใจ JavaScript Event Loop: ไขความลับ Call Stack และการทำงานแบบ Async

9 นาที 10 views บันทึกเป็น PDF
เข้าใจ JavaScript Event Loop: ไขความลับ Call Stack และการทำงานแบบ Async

ทำไม JavaScript ถึงทำงานหลายอย่างได้โดยไม่ค้าง? มาเรียนรู้กลไก Event Loop, Call Stack และการจัดการงานแบบ Async ที่มือใหม่ต้องรู้เพื่อเขียนโค้ดให้ลื่นไหล

ทำไม JavaScript ถึงทำงานหลายอย่างพร้อมกันได้

หลายคนมักได้ยินว่า JavaScript เป็นภาษาแบบ Single-threaded (ภาษาที่ทำงานทีละคำสั่งตามลำดับ) แต่ทำไมเวลาเรากดปุ่มหรือดึงข้อมูลจากอินเทอร์เน็ต หน้าเว็บถึงไม่ค้างไปเฉยๆ คำตอบอยู่ที่กลไกที่เรียกว่า Event Loop (วงรอบการจัดการเหตุการณ์) ซึ่งทำหน้าที่เหมือนพนักงานต้อนรับที่คอยจัดคิวงานให้เรานั่นเอง

ลองจินตนาการว่าคุณเป็นพนักงานในร้านอาหารที่ต้องทำทุกอย่างคนเดียว คุณคงไม่ยืนรอให้ลูกค้ากินข้าวเสร็จก่อนถึงจะไปรับออเดอร์โต๊ะถัดไปได้ คุณต้องจดออเดอร์แล้วส่งให้ครัวทำ จากนั้นก็หันไปดูแลลูกค้าคนอื่นต่อ พอกับข้าวเสร็จแล้วค่อยนำไปเสิร์ฟ JavaScript ก็ทำงานในลักษณะเดียวกัน คือไม่ยอมเสียเวลารอคอยงานที่ใช้เวลานาน

การเข้าใจกลไกนี้จะช่วยให้คุณเลิกปวดหัวกับปัญหาที่โค้ดทำงานไม่ตรงตามลำดับที่คิดไว้ หากคุณเพิ่งเริ่มต้นเส้นทางโปรแกรมเมอร์ นี่คือพื้นฐานสำคัญที่สุดที่จะเปลี่ยนให้คุณเขียนโค้ดที่ลื่นไหลและมีประสิทธิภาพ เมื่อเข้าใจหัวใจของมันแล้ว คุณจะเห็นภาพชัดเจนว่าทำไมโค้ดบางบรรทัดถึงทำงานก่อนหรือหลังกันเสมอ

Call Stack หัวใจหลักของการทำงาน

ก่อนจะไปถึงเรื่องงานเบื้องหลัง เราต้องรู้จัก Call Stack (กองซ้อนการทำงาน) กันก่อน มันคือโครงสร้างข้อมูลที่ใช้เก็บว่าตอนนี้โปรแกรมกำลังทำฟังก์ชันไหนอยู่ โดยจะใช้วิธีที่เรียกว่า Last-In, First-Out (งานที่เข้ามาล่าสุด จะถูกทำเสร็จและเอาออกไปก่อน) เปรียบได้กับกองจานที่วางซ้อนกันนั่นเอง

เมื่อโปรแกรมเมอร์เรียกใช้ฟังก์ชัน โปรแกรมจะนำฟังก์ชันนั้นวางไว้บนสุดของกอง เมื่อฟังก์ชันทำงานเสร็จก็จะถูกหยิบออกไป ถ้าฟังก์ชันนั้นมีการเรียกฟังก์ชันอื่นต่อ ก็จะวางซ้อนทับเข้าไปอีกชั้นหนึ่ง JavaScript จะทำงานตามลำดับในกองนี้จนกว่าจะว่างเปล่า หากกองนี้แน่นเกินไปหรือมีงานค้างนานเกินไป หน้าเว็บของเราก็จะค้างหรือที่เรียกว่า Browser freezing (หน้าเว็บหยุดตอบสนอง)

มือใหม่มักพลาดตรงที่พยายามใส่โค้ดที่ทำงานหนักๆ ไว้ในฟังก์ชันเดียวโดยไม่แยกส่วน ทำให้ Call Stack เต็มและโปรแกรมค้าง การแบ่งฟังก์ชันให้เล็กลงจึงเป็นนิสัยที่ดีของโปรแกรมเมอร์มืออาชีพ เรามาดูตัวอย่างการทำงานพื้นฐานของมันกันดีกว่า

function first() {
  console.log("เริ่มทำงานที่หนึ่ง");
  second();
}

function second() {
  console.log("ทำงานที่สองต่อทันที");
}

first();
console.log("จบการทำงาน");

คำอธิบายโค้ด: บรรทัดแรกสร้างฟังก์ชัน first ที่เรียกใช้ second ภายใน บรรทัดต่อมาคือฟังก์ชัน second ที่พิมพ์ข้อความออกมา จากนั้นเราสั่งรัน first() และพิมพ์ข้อความสุดท้าย ผลลัพธ์คือโปรแกรมจะพิมพ์ข้อความตามลำดับ 1, 2, 3 ตามที่โค้ดถูกเรียกเข้าสู่กองงานจริงๆ

ผลลัพธ์ที่ได้: เริ่มทำงานที่หนึ่ง ทำงานที่สองต่อทันที จบการทำงาน

เมื่อต้องทำงานที่ใช้เวลานาน: Async Execution

งานบางอย่างเช่น การดึงข้อมูลจากฐานข้อมูลหรือการรอเวลา ไม่สามารถทำบน Call Stack ได้ เพราะจะทำให้โปรแกรมหยุดชะงัก JavaScript จึงต้องใช้ Async Execution (การทำงานแบบไม่รอคอย) โดยส่งงานเหล่านั้นไปให้ Browser หรือ Node.js จัดการแทน แล้วค่อยกลับมาเรียกผลลัพธ์เมื่อทำเสร็จ

ยกตัวอย่างการใช้คำสั่ง setTimeout (คำสั่งตั้งเวลาให้ทำงานในอนาคต) เมื่อเราสั่งให้มันทำงาน JavaScript จะไม่รอจนครบเวลา แต่จะส่งหน้าที่การนับเวลาไปให้สภาพแวดล้อมจัดการ แล้วตัวมันเองจะไปทำงานบรรทัดถัดไปทันที เมื่อครบเวลาที่กำหนด ตัวจัดการงานจะส่ง "ตั๋ว" กลับมาบอกว่าถึงเวลาทำงานแล้วนะ

ความเข้าใจผิดที่พบบ่อยคือการคิดว่าเวลา 0 วินาทีใน setTimeout จะทำให้โค้ดทำงานทันที จริงๆ แล้วมันแค่บอกว่า "ให้เอาไปต่อคิวท้ายสุดนะ" ซึ่งเป็นจุดที่มือใหม่หลายคนมักงงว่าทำไมโค้ดถึงไม่ทำงานในเสี้ยววินาทีที่สั่งไว้ ลองดูตัวอย่างที่แสดงการทำงานแบบนี้กันครับ

console.log("จุดเริ่มต้น");

setTimeout(() => {
  console.log("ทำงานหลังจากหน่วงเวลา");
}, 0);

console.log("จุดสิ้นสุด");

คำอธิบายโค้ด: บรรทัดแรกพิมพ์ข้อความเริ่ม บรรทัดถัดมาสั่ง setTimeout แม้จะตั้งเวลาเป็น 0 แต่ JavaScript จะโยนคำสั่งนี้ไปไว้ในคิวงาน (Task Queue) แทน และบรรทัดสุดท้ายทำงานทันทีโดยไม่รอให้ setTimeout ทำงานก่อน ทำให้ข้อความสุดท้ายถูกพิมพ์ออกมาก่อนข้อความใน setTimeout

ผลลัพธ์ที่ได้: จุดเริ่มต้น จุดสิ้นสุด ทำงานหลังจากหน่วงเวลา

Microtasks คิวพิเศษที่ต้องเคลียร์ก่อน

ในระบบของ JavaScript จะมีคิวพิเศษที่เรียกว่า Microtasks Queue (คิวงานย่อย) ซึ่งมีความสำคัญสูงกว่าคิวงานปกติ งานที่อยู่ในคิวนี้ เช่น Promise (วัตถุที่ใช้จัดการงานที่สำเร็จหรือล้มเหลวในอนาคต) จะถูกดึงมาทำทันทีที่ Call Stack ว่างเปล่า ก่อนที่จะไปเริ่มงานในคิวปกติ

ลองนึกภาพว่าคุณกำลังต่อแถวซื้อกาแฟ แต่มีลูกค้ากลุ่มพิเศษที่ถือบัตร VIP เดินเข้ามา แทรกคิวเข้าไปทำรายการก่อนคนอื่นๆ Microtasks คือบัตร VIP นั้นเอง การที่มันมีลำดับความสำคัญสูง ทำให้เรามั่นใจได้ว่างานที่เกี่ยวข้องกับข้อมูลสำคัญจะถูกจัดการให้เสร็จสิ้นก่อนจะไปทำงานอื่นที่อาจจะช้ากว่า

สำหรับคนที่กำลังฝึกเขียนโค้ด การจดจำลำดับความสำคัญนี้ช่วยให้คุณคาดเดาผลลัพธ์ได้แม่นยำขึ้น โดยเฉพาะเวลาเขียนโปรแกรมที่ต้องเชื่อมต่อ API (ช่องทางแลกเปลี่ยนข้อมูลระหว่างโปรแกรม) ถ้าคุณเข้าใจว่า Promise จะถูกรันก่อน setTimeout เสมอ คุณจะแก้บั๊กที่เกี่ยวกับลำดับการทำงานได้ไวขึ้นมาก

Promise.resolve().then(() => {
  console.log("ทำงานจาก Microtask");
});

setTimeout(() => {
  console.log("ทำงานจาก Task ปกติ");
}, 0);

คำอธิบายโค้ด: เราใช้ Promise.resolve() เพื่อสร้างงานในคิว Microtasks และใช้ setTimeout สร้างงานในคิวปกติ แม้จะสั่งพร้อมกัน แต่ Event Loop จะตรวจสอบคิวพิเศษก่อนเสมอ ทำให้ข้อความจาก Promise แสดงออกมาก่อนเสมอ

ผลลัพธ์ที่ได้: ทำงานจาก Microtask ทำงานจาก Task ปกติ

สรุปขั้นตอนการทำงานของ Event Loop

หัวใจของ Event Loop คือการวนลูปตรวจสอบงานอยู่ตลอดเวลา โดยมีขั้นตอนดังนี้: 1. ทำงานใน Call Stack ให้หมด 2. เคลียร์งานทั้งหมดใน Microtasks Queue 3. ทำงานหนึ่งอย่างใน Task Queue (เช่น setTimeout) แล้ววนกลับไปทำข้อ 1 ใหม่เสมอ

การเข้าใจกลไกนี้ไม่ได้มีไว้เพื่อให้คุณสอบผ่าน แต่เพื่อให้คุณเขียนโค้ดที่ "คาดเดาได้" หากคุณกำลังทำโปรเจกต์ดึงข้อมูลจาก API มาแสดงผลบนหน้าเว็บ การรู้ว่าข้อมูลจะมาถึงเมื่อไหร่และจะถูกสั่งให้แสดงผลตอนไหน จะช่วยให้หน้าเว็บของคุณไม่มีอาการกระตุกหรือข้อมูลแสดงผลผิดพลาด

สรุปสั้นๆ สำหรับการทำงานจริง: ถ้าคุณต้องการให้โค้ดทำงานทันที ให้ใส่ไว้ใน Call Stack ปกติ ถ้าต้องการให้ทำงานหลังจากงานหลักเสร็จสิ้นแต่ต้องเร่งด่วน ให้ใช้ Promise หรือ async/await (คำสั่งที่ช่วยให้เขียนโค้ดแบบรอคอยได้ง่ายขึ้น) และถ้าต้องการทำงานที่ไม่เร่งด่วน เช่น การหน่วงเวลา ให้ใช้ setTimeout ครับ

สรุป

เราได้เรียนรู้แล้วว่า JavaScript ไม่ได้ทำงานแค่บรรทัดต่อบรรทัดแบบทื่อๆ แต่มีระบบจัดการคิวที่ชาญฉลาด การแยกแยะระหว่าง Call Stack, Microtasks และ Task Queue คือกุญแจสำคัญที่ทำให้คุณก้าวข้ามจากมือใหม่ไปสู่การเขียนโปรแกรมระดับสูงได้

ลองนำความรู้นี้ไปปรับใช้ในการทำโปรเจกต์เล็กๆ ของตัวเอง เช่น การดึงรายชื่อผู้ใช้งานจากฐานข้อมูลจำลอง แล้วลองสั่ง console.log แทรกไว้ตามจุดต่างๆ เพื่อดูว่าลำดับการทำงานเป็นไปตามที่เราเรียนรู้มาไหม การฝึกสังเกตด้วยตัวเองแบบนี้จะช่วยให้คุณจำได้แม่นยำกว่าการท่องจำทฤษฎี

สุดท้ายนี้ โปรแกรมเมอร์ที่เก่งไม่ได้วัดกันที่จำคำศัพท์ได้กี่คำ แต่วัดกันที่การเข้าใจว่าโค้ดที่ตัวเองเขียนทำงานอย่างไรภายใต้ฝากระโปรงรถ ขอให้สนุกกับการทดลองและอย่ากลัวที่จะทำบั๊ก เพราะทุกบั๊กที่คุณแก้ได้ คือโอกาสในการเรียนรู้ที่ทำให้คุณเก่งขึ้นอย่างก้าวกระโดดครับ


ที่มา: JavaScript Event Loop Explained: Call Stack, Microtasks, and Async Execution — DEV Community

แชร์บทความ

Facebook X LINE

บทความที่เกี่ยวข้อง

เทคนิคเขียนแอป Flutter สำหรับ Meta Smart Glasses ให้ลื่นไหลและมีประสิทธิภาพ

เทคนิคเขียนแอป Flutter สำหรับ Meta Smart Glasses ให้ลื่นไหลและมีประสิทธิภาพ

เรียนรู้วิธีเขียนแอป Flutter เชื่อมต่อ Meta Smart Glasses ให้ทำงานเร็ว ไม่กระตุก ด้วยการวางสถาปัตยกรรมโค้ดและการจัดการข้อมูลแบบมือโปรที่มือใหม่ทำตามได้จริง

ที่มา: DEV Community

1 hour ago 10 นาที
0 views
เปรียบเทียบ WebSocket, SSE และ Polling เลือกวิธีทำระบบ Real-Time ให้เหมาะกับงาน

เปรียบเทียบ WebSocket, SSE และ Polling เลือกวิธีทำระบบ Real-Time ให้เหมาะกับงาน

อยากทำระบบ Real-Time แต่ไม่รู้จะเลือกใช้ Polling, SSE หรือ WebSocket ดี? มาดูวิธีเลือกใช้ให้เหมาะกับงาน เพื่อให้แอปของคุณทำงานลื่นไหลและประหยัดทรัพยากรเซิร์ฟเวอร์

ที่มา: DEV Community

4 hours ago 10 นาที
4 views