JavaScript
Async JavaScript
Non-Blocking I/O
เข้าใจแนวคิด Non-Blocking I/O — การทำงานแบบไม่ต้องรอของ JavaScript ผ่าน Call Stack, Web APIs และ Callback Queue ที่ทำให้ JavaScript ทำงานแบบ asynchronous ได้แม้จะเป็น single-threaded
Blocking I/O — เมื่อโปรแกรมหยุดรอ
ลองนึกภาพร้านอาหารที่มีพนักงานเสิร์ฟคนเดียว — ทุกครั้งที่มีลูกค้าสั่งอาหาร พนักงานต้องเดินไปบอกครัว แล้ว**ยืนรอที่ครัว**จนกว่าอาหารจะเสร็จ ระหว่างที่รอ — ลูกค้าโต๊ะอื่นไม่มีใครมารับออเดอร์เลย ลูกค้าต้องนั่งรอ พนักงานก็ยืนรอ ทุกอย่างหยุดนิ่ง นี่คือ **Blocking I/O** — เมื่อโปรแกรมต้องทำงานที่ใช้เวลานาน (เช่น อ่านไฟล์, เรียก API, รอผู้ใช้คลิก) — โค้ดบรรทัดถัดไป**ต้องรอ**จนกว่างานนั้นจะเสร็จ **ทั้งโปรแกรมหยุดนิ่งระหว่างรอ** ใน JavaScript — โค้ดแบบ synchronous (ทำงานทีละบรรทัดตามลำดับ) ถ้ามี operation ที่ใช้เวลานาน — โปรแกรมจะ **block** คือค้างทั้งหน้าเว็บ — ผู้ใช้คลิกอะไรไม่ได้เลย
Blocking I/O เปรียบเหมือนพนักงานเสิร์ฟที่ยืนรอครัว — ทุกอย่างหยุดนิ่งระหว่างรอ
console.log("1: เริ่มทำงาน");
alert("นี่คือ blocking dialog — โปรแกรมหยุดตรงนี้"); // ⛔ block ทั้งหน้าจอ
console.log("2: บรรทัดนี้ไม่ทำงานจนกว่าผู้ใช้จะกด OK");
// alert() block ทุกอย่าง — คลิกอะไรไม่ได้เลย, scroll ไม่ได้, พิมพ์ไม่ได้**ปัญหาของ Blocking I/O**: - UI ค้าง — ผู้ใช้คลิกอะไรไม่ได้ — เหมือนหน้าเว็บพัง - เสียเวลา — CPU ว่างระหว่างรอ I/O — แทนที่จะทำงานอื่นต่อ - ประสบการณ์ผู้ใช้แย่ — โดยเฉพาะในเว็บแอปที่ต้องรอ network, ไฟล์, หรือฐานข้อมูล
- **Blocking I/O** คือการที่โปรแกรมหยุดรอจนกว่า operation จะเสร็จ — โค้ดบรรทัดถัดไปไม่ทำงาน
- เปรียบเหมือนพนักงานเสิร์ฟที่ยืนรอครัว — ระหว่างรอไม่มีใครรับออเดอร์ใหม่
- `alert()` เป็นตัวอย่าง blocking ใน JavaScript — หยุดทุกอย่างจนกว่าผู้ใช้จะกด OK
- Blocking ทำให้ UI ค้าง — ผู้ใช้รู้สึกว่าเว็บพัง — ประสบการณ์แย่มาก
Non-Blocking I/O — ทำงานโดยไม่ต้องรอ
กลับไปที่ร้านอาหาร — คราวนี้ร้านแจก **pager (เครื่องสั่น)** ให้พนักงานเสิร์ฟ — เมื่อลูกค้าสั่งอาหาร: 1. พนักงานรับออเดอร์และส่งให้ครัว 2. ครัวให้ pager แก่พนักงาน 3. **พนักงานไปรับออเดอร์โต๊ะอื่นต่อทันที** — ไม่ต้องยืนรอ 4. เมื่อครัวทำอาหารเสร็จ — pager สั่น — พนักงานกลับมารับอาหารไปเสิร์ฟ นี่คือ **Non-Blocking I/O** — แทนที่จะหยุดรอ — เรา**ลงทะเบียนงานไว้** แล้วไปทำงานอื่นต่อ — เมื่อมีผลลัพธ์ (อาหารเสร็จ) — ระบบจะเรียกเรากลับมา
Non-Blocking I/O — พนักงานเสิร์ฟส่งออเดอร์แล้วไปทำงานอื่นต่อ ไม่ต้องยืนรอ — ระบบจะเรียกกลับมาเมื่ออาหารเสร็จ
**JavaScript กับ Non-Blocking**: JavaScript engine เองเป็น **single-threaded** — มีแค่ 1 call stack — ทำงานได้ทีละอย่างเท่านั้น — แต่เบราว์เซอร์และ Node.js มีกลไกพิเศษที่ทำให้ JavaScript **ไม่ต้องรอ** operation ที่ใช้เวลานาน: - เบราว์เซอร์มี **Web APIs** (`setTimeout`, `fetch`, `DOM events`) - Node.js มี **libuv** (thread pool สำหรับ file I/O, network) - กลไกเหล่านี้ทำงาน**เบื้องหลัง** — JavaScript ทำงานอื่นต่อได้ทันที - เมื่อทำงานเบื้องหลังเสร็จ — ผลลัพธ์ถูกส่งเข้า **Callback Queue** — รอให้ JavaScript มารับไปทำงานต่อ
- **Non-Blocking** = ส่งงานให้ระบบจัดการ แล้วไปทำงานอื่นต่อ — ไม่ต้องยืนรอ
- เปรียบเหมือน pager ในร้านอาหาร — พนักงานไม่ต้องยืนรอที่ครัว
- JavaScript เป็น single-threaded — แต่ runtime (เบราว์เซอร์/Node.js) มีกลไกทำงานเบื้องหลัง
- Web APIs และ libuv ทำงานแทน JavaScript — JavaScript กลับมาทำงานต่อเมื่อมีผลลัพธ์
Call Stack + Web APIs + Callback Queue — จักรกลเบื้องหลัง Non-Blocking
เบื้องหลังความสามารถในการทำงานแบบ non-blocking ของ JavaScript มี 3 องค์ประกอบที่ทำงานร่วมกัน: 1. **Call Stack** — ส่วนที่ JavaScript engine ใช้ทำงาน — เก็บ function call ที่กำลังทำงาน — ทำงานทีละอย่าง (single-threaded) — เมื่อ function เสร็จจะถูก pop ออกจาก stack 2. **Web APIs** — API ที่เบราว์เซอร์เตรียมไว้ให้ (`setTimeout`, `fetch`, `addEventListener`, etc.) — ทำงาน**นอก** Call Stack — เมื่อ Web API ทำงานเสร็จ — callback ที่เราส่งไปจะถูกย้ายเข้า **Callback Queue** 3. **Callback Queue** (หรือ Task Queue) — แถวคอยของ callback function ที่รอให้ Call Stack ว่าง — คิวนี้ทำงานแบบ FIFO (First-In-First-Out) — callback ไหนมาก่อนได้ทำงานก่อน
JavaScript Runtime Model — 3 องค์ประกอบ: Call Stack (ทำงาน), Web APIs (รอเบื้องหลัง), Callback Queue (เข้าแถวรอ)
| องค์ประกอบ | หน้าที่ | ตัวอย่าง |
|---|---|---|
| **Call Stack** | ทำงาน JavaScript ทีละอย่าง — function ไหนเรียกก่อนอยู่ล่าง — function ล่าสุดอยู่บน — ทำงานบนสุดก่อน (LIFO) | `console.log()`, `function()` ทั่วไป, การคำนวณ |
| **Web APIs** | API ที่เบราว์เซอร์ให้ — ทำงานนอก Call Stack — เมื่อเสร็จแล้วส่ง callback เข้า Callback Queue | `setTimeout()`, `fetch()`, `addEventListener()`, `setInterval()` |
| **Callback Queue** | แถวคอยของ callback — รอให้ Call Stack ว่าง — FIFO: callback ไหนมาก่อนได้ทำงานก่อน | callback ของ `setTimeout`, `fetch().then()`, event handlers |
**Event Loop** — คือตัวเช็กตลอดเวลาว่า: 1. Call Stack ว่างไหม? 2. ถ้า Call Stack **ว่าง** และมี callback ใน Callback Queue → ดึง callback ตัวแรกสุดออกจากคิว → ใส่เข้า Call Stack เพื่อทำงาน 3. วนกลับไปข้อ 1 กลไกนี้ทำให้ JavaScript **ดูเหมือน** ทำงานหลายอย่างพร้อมกัน — ทั้งที่จริง ๆ แล้วทำงานทีละอย่าง — แต่ไม่เคยหยุดรอ
- **Call Stack**: สมองของ JavaScript — ทำงานทีละอย่าง — single-threaded
- **Web APIs**: มือและขาของ JavaScript — ทำงานเบื้องหลัง — browser/node เป็นคนจัดการ
- **Callback Queue**: ห้องรอของ callback — เข้าแถวรอ Call Stack ว่าง
- **Event Loop**: ผู้ประสานงาน — เช็ก Call Stack → ถ้าว่าง → ย้าย callback จากคิวเข้า stack → วนซ้ำ
setTimeout — เห็น Non-Blocking ด้วยตาตัวเอง
`setTimeout` เป็นวิธีที่ง่ายที่สุดในการ**เห็น**การทำงานแบบ non-blocking — มันให้ JavaScript เลื่อนการทำงานของ callback ไปในอนาคต โดยที่โค้ดบรรทัดต่อมาทำงานต่อทันที **วิธีอ่าน `setTimeout(callback, delay)`**: - `callback` — function ที่อยากให้ทำงานในอนาคต - `delay` — ระยะเวลา (มิลลิวินาที) ที่ต้องรอก่อน callback จะถูกใส่เข้า Callback Queue **สำคัญ**: `delay` ไม่ได้แปลว่า callback จะทำงานเป๊ะ ๆ หลังจากเวลานั้น — แต่มันคือเวลา**น้อยที่สุด**ที่ต้องรอ — ถ้า Call Stack ยังไม่ว่าง — callback ต้องรอต่อ
console.log("1: เริ่ม");
setTimeout(() => {
console.log("2: setTimeout callback — ทำงานหลังจาก 0ms");
}, 0);
console.log("3: จบ");
// Output:
// "1: เริ่ม"
// "3: จบ"
// "2: setTimeout callback — ทำงานหลังจาก 0ms"
// แม้ delay = 0ms — "2" ก็ยังแสดงหลัง "3"
// เพราะ callback ต้องรอใน Callback Queue จนกว่า Call Stack จะว่าง**เกิดอะไรขึ้น — Step by Step**: 1. `console.log("1: เริ่ม")` → ใส่ Call Stack → ทำงาน → แสดง "1" → pop ออกจาก stack 2. `setTimeout(cb, 0)` → ใส่ Call Stack → **Web API รับ timer ไป** — ตั้งเวลา 0ms — setTimeout จบทันที → pop ออกจาก stack 3. `console.log("3: จบ")` → ใส่ Call Stack → ทำงาน → แสดง "3" → pop ออกจาก stack 4. Call Stack **ว่างแล้ว** 5. Web API: ผ่านไป 0ms → ส่ง callback เข้า Callback Queue 6. Event Loop เห็น: Call Stack ว่าง + Callback Queue มี callback → ย้าย callback เข้า Call Stack 7. Callback ทำงาน → แสดง "2" → pop ออก นี่คือเหตุผลที่ "2" แสดงหลัง "3" — callback ต้องรอให้ Call Stack ว่างก่อนเสมอ — ถึงแม้ delay จะเป็น 0ms ก็ตาม
console.log("A: เริ่ม");
setTimeout(() => {
console.log("B: setTimeout 0ms ตัวที่ 1");
}, 0);
setTimeout(() => {
console.log("C: setTimeout 0ms ตัวที่ 2");
}, 0);
setTimeout(() => {
console.log("D: setTimeout 0ms ตัวที่ 3");
}, 0);
console.log("E: จบ");
// Output:
// "A: เริ่ม"
// "E: จบ"
// "B: setTimeout 0ms ตัวที่ 1"
// "C: setTimeout 0ms ตัวที่ 2"
// "D: setTimeout 0ms ตัวที่ 3"
// ทุก callback ได้ delay 0ms เท่ากัน
// Web API ส่งเข้า Callback Queue ตามลำดับที่ setTimeout ถูกเรียก
// Event Loop ดึง callback ออกมาทำงานตามลำดับ FIFO| ลำดับ | เกิดอะไรขึ้น | Call Stack | Callback Queue |
|---|---|---|---|
| 1 | `console.log("A")` | ✅ "A" | ว่าง |
| 2 | `setTimeout(cb1, 0)` — ลงทะเบียนกับ Web API | ✅ setTimeout → pop | ว่าง |
| 3 | `setTimeout(cb2, 0)` — ลงทะเบียนกับ Web API | ✅ setTimeout → pop | ว่าง |
| 4 | `setTimeout(cb3, 0)` — ลงทะเบียนกับ Web API | ✅ setTimeout → pop | ว่าง |
| 5 | `console.log("E")` | ✅ "E" | ว่าง |
| 6 | Web API ส่ง cb1 → Callback Queue | ว่าง | 📥 cb1 |
| 7 | Event Loop: Stack ว่าง → ดึง cb1 เข้า Stack → ทำงาน | ✅ "B" → pop | 📥 cb2, cb3 |
| 8 | Event Loop: Stack ว่าง → ดึง cb2 เข้า Stack → ทำงาน | ✅ "C" → pop | 📥 cb3 |
| 9 | Event Loop: Stack ว่าง → ดึง cb3 เข้า Stack → ทำงาน | ✅ "D" → pop | ว่าง |
- `setTimeout(callback, delay)` — เลื่อน callback ไปทำงานในอนาคต — โค้ดบรรทัดถัดไปทำงานต่อทันที
- `delay` คือเวลา**อย่างน้อย**ที่ต้องรอ — ไม่ใช่เวลาที่ callback จะทำงานเป๊ะ ๆ
- Callback ต้องรอใน Callback Queue จนกว่า Call Stack จะว่าง — แม้ delay = 0 ก็ต้องรอ
- ลำดับของ callback ในคิวเป็น FIFO — callback ไหนลงทะเบียนก่อนได้เข้าก่อน
Non-Blocking APIs ในชีวิตจริง
setTimeout เป็นแค่จุดเริ่มต้น — ในงานจริงมี API ที่ใช้ non-blocking pattern อีกมาก: **ฝั่งเบราว์เซอร์**: - `fetch()` — ดึงข้อมูลจาก server — ไม่ block UI ระหว่างรอ response - `addEventListener()` — รอให้ผู้ใช้คลิก, พิมพ์, scroll — แล้วเรียก callback - `setInterval()` — ทำงานซ้ำทุกช่วงเวลา — เหมือน setTimeout แต่เรียกซ้ำ - `requestAnimationFrame()` — ทำงานก่อน browser repaint ครั้งถัดไป — ใช้ทำ animation **ฝั่ง Node.js**: - `fs.readFile()` — อ่านไฟล์แบบไม่ block — โปรแกรมทำงานต่อระหว่างอ่าน - `http.createServer()` — รับ request พร้อมกันได้เป็นพันโดยไม่ต้องสร้าง thread ใหม่ ทั้งหมดนี้ใช้หลักการเดียวกัน — **ส่งงานให้ runtime → ทำงานต่อ → เมื่อเสร็จ callback ถูกเรียก**
| API | ใช้ทำอะไร | ทำงานที่ใด | Non-Blocking อย่างไร |
|---|---|---|---|
| `setTimeout(cb, ms)` | หน่วงเวลาเรียก callback | Browser / Node.js | ลงทะเบียน timer กับ Web API → JavaScript ทำงานต่อ → timer หมด → callback เข้าคิว |
| `fetch(url)` | ดึงข้อมูลจาก server | Browser / Node.js | ส่ง request ผ่าน browser network layer → JavaScript ทำงานต่อ → response กลับมา → Promise resolve |
| `addEventListener(event, cb)` | รอ event จากผู้ใช้ | Browser | ลงทะเบียน listener → JavaScript ทำงานต่อ → ผู้ใช้คลิก → callback เข้าคิว |
| `fs.readFile(path, cb)` | อ่านไฟล์จาก disk | Node.js | ส่งงานให้ libuv thread pool → JavaScript ทำงานต่อ → อ่านเสร็จ → callback เข้าคิว |
| `setInterval(cb, ms)` | ทำงานซ้ำทุกช่วงเวลา | Browser / Node.js | เหมือน setTimeout แต่ callback ถูกใส่เข้าคิวซ้ำทุก ms |
// === fetch() — non-blocking ===
console.log("เริ่ม fetch...");
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => {
console.log("ข้อมูลมาถึงแล้ว:", data);
});
console.log("โค้ดบรรทัดนี้ทำงานทันที — ไม่ต้องรอ fetch เสร็จ");
// Output:
// "เริ่ม fetch..."
// "โค้ดบรรทัดนี้ทำงานทันที — ไม่ต้องรอ fetch เสร็จ"
// "ข้อมูลมาถึงแล้ว: ..." (มาทีหลัง)
// === addEventListener() — non-blocking ===
console.log("ลงทะเบียน event listener...");
document.querySelector("#btn").addEventListener("click", () => {
console.log("ผู้ใช้คลิกปุ่ม!");
});
console.log("โค้ดบรรทัดนี้ทำงานต่อทันที — ไม่ต้องรอให้ผู้ใช้คลิก");
// ปุ่มพร้อมรับคลิก แต่โปรแกรมไม่หยุดรอ
// เมื่อผู้ใช้คลิกเมื่อไหร่ — callback ถึงจะทำงาน- **fetch()**: ดึงข้อมูลจาก server โดยไม่ block UI — ใช้ `.then()` หรือ `await` รับผลลัพธ์
- **addEventListener()**: ลงทะเบียนรอ event — เมื่อเกิด event callback ถึงทำงาน — ระหว่างนี้โปรแกรมทำงานปกติ
- **fs.readFile()** (Node.js): อ่านไฟล์โดยไม่ block server — รับ request อื่นต่อได้ระหว่างรอ
- **หลักการเดียวกันทั้งหมด**: ส่งงาน → ทำงานต่อ → เมื่อเสร็จ → callback ถูกเรียก
Blocking vs Non-Blocking — เทียบกันชัด ๆ
| มิติ | Blocking I/O | Non-Blocking I/O |
|---|---|---|
| **การทำงาน** | หยุดรอจนกว่า operation จะเสร็จ — โค้ดบรรทัดถัดไปไม่ทำงาน | ส่งงานแล้วไปทำงานอื่นต่อ — เมื่อเสร็จ callback ถูกเรียก |
| **Call Stack** | มี function รออยู่ใน stack — stack ไม่ว่าง | Call Stack ว่างเร็ว — พร้อมรับงานใหม่ |
| **UI** | ค้าง — ผู้ใช้คลิกอะไรไม่ได้ — scroll ไม่ได้ | ตอบสนองปกติ — ผู้ใช้ใช้งานต่อได้ระหว่างรอ |
| **เปรียบเทียบ** | พนักงานเสิร์ฟยืนรอที่ครัว | พนักงานรับ pager แล้วไปรับออเดอร์โต๊ะอื่นต่อ |
| **ตัวอย่าง** | `alert()`, `while(true){}`, synchronous file read | `setTimeout()`, `fetch()`, `addEventListener()`, async file read |
| **เมื่อไหร่ควรใช้** | งานที่ต้องเสร็จก่อนจึงจะทำงานต่อได้ (เช่น config ตอนเริ่มโปรแกรม) | งานที่ใช้เวลานานและไม่อยาก block UI หรือ server |
Blocking (ซ้าย): โปรแกรมหยุดรอ — vs — Non-Blocking (ขวา): โปรแกรมทำงานอื่นต่อระหว่างรอ
- **Blocking I/O** — โปรแกรมหยุดรอ — งานอื่นไม่เกิด — UI ค้าง — เหมาะกับงานที่ต้องเสร็จก่อนจริง ๆ
- **Non-Blocking I/O** — ส่งงานแล้วไปต่อ — งานอื่นเกิดขึ้นระหว่างรอ — UI ลื่น — เป็นหัวใจของ JavaScript async
- JavaScript ใช้ Non-Blocking I/O เป็นหลัก — การเข้าใจเรื่องนี้เป็นรากฐานของ Callbacks, Promises, Async/Await ที่จะเรียนต่อไป
สิ่งที่ต้องจำ — Recap บทนี้
- **Blocking I/O** = โปรแกรมหยุดรอ operation — โค้ดบรรทัดถัดไปไม่ทำงาน — UI ค้าง — `alert()` คือตัวอย่าง
- **Non-Blocking I/O** = ส่งงานให้ runtime จัดการ — โปรแกรมทำงานอื่นต่อ — เมื่อเสร็จ callback ถูกเรียก
- **JavaScript เป็น single-threaded** — มี Call Stack เดียว — แต่เบราว์เซอร์และ Node.js มี Web APIs/libuv ที่ทำงานเบื้องหลัง
- **Callback Queue** = แถวคอยของ callback — รอให้ Call Stack ว่าง — FIFO: มาก่อนได้ทำงานก่อน
- **Event Loop** = ตัวเช็กว่า Call Stack ว่าง → ดึง callback จากคิว → ใส่ stack → วนซ้ำ — ทำให้ JS ดูเหมือนทำงานหลายอย่างพร้อมกัน
- **`setTimeout(cb, 0)`** = เห็น non-blocking ชัดที่สุด — callback ถูกเลื่อนไปทำงานหลัง — แม้ delay = 0
- **`fetch()`, `addEventListener()`, `fs.readFile()`** = ใช้หลักการเดียวกัน — ส่งงาน → ทำงานต่อ → callback เมื่อเสร็จ