EventEmitter
แกนหลักของ event-driven model ของ Node
หัวข้อที่มีชื่อว่า “แกนหลักของ event-driven model ของ Node”EventEmitter (จาก node:events) คือ class ที่ขับเคลื่อนแทบทุก I/O object ใน Node.js HTTP server, TCP socket, file stream, child process และไลบรารีบุคคลที่สามจำนวนมากล้วน extend EventEmitter การเข้าใจมันคือการเข้าใจว่า Node สื่อสารระหว่าง component ต่าง ๆ อย่างไร
การใช้งานพื้นฐาน: on, emit, removeListener
หัวข้อที่มีชื่อว่า “การใช้งานพื้นฐาน: on, emit, removeListener”Output (verified on Node 24)
Hello, Alice!
Hello, Bob!
message: firstonce — ยิงแค่ครั้งเดียว
หัวข้อที่มีชื่อว่า “once — ยิงแค่ครั้งเดียว”once ลงทะเบียน listener ที่ลบตัวเองอัตโนมัติหลังจากการ emit ครั้งแรก
Output (verified on Node 24)
Connected! (fires once only)
listener count: 0การสร้าง custom EventEmitter class
หัวข้อที่มีชื่อว่า “การสร้าง custom EventEmitter class”extend EventEmitter เพื่อให้ class ของคุณเองมีความสามารถแบบ event-driven
Output (verified on Node 24)
loaded item: alpha
loaded item: beta
loaded item: gamma
all done! total: 3 items
items: alpha, beta, gammaListener count และ off()
หัวข้อที่มีชื่อว่า “Listener count และ off()”off เป็น alias ของ removeListener ที่เพิ่มเข้ามาใน Node 10 listenerCount ช่วยให้ตรวจสอบได้ว่ามี listener ลงทะเบียนไว้กี่ตัวสำหรับ event หนึ่ง — มีประโยชน์สำหรับตรวจจับ memory leak ที่อาจเกิดขึ้น
Output (verified on Node 24)
count before off: 2
count after off: 1
handler2คำเตือน listener leak
หัวข้อที่มีชื่อว่า “คำเตือน listener leak”EventEmitter มีค่าดีฟอลต์เป็น สูงสุด 10 listener ต่อหนึ่ง event (defaultMaxListeners) นี่ไม่ใช่ hard limit — มันคือ heuristic tripwire: บั๊ก EventEmitter อันดับ 1 ใน AI-generated code คือ listener ที่ถูกลงทะเบียน ภายใน loop, request handler หรือ re-render สไตล์ React ซึ่งจะเพิ่ม listener ใหม่แบบเงียบ ๆ ทุกครั้งที่เรียกแทนที่จะใช้ซ้ำตัวเดิม ถ้าไม่สังเกตเห็น นั่นคือ memory leak ที่ไม่มีขอบเขต
เมื่อจำนวนเกิน max Node จะ emit process warning — ปกติจะพิมพ์ไปที่ stderr เท่านั้น เพื่อให้เห็นใน output ของ script เอง (และจัดการมันแบบ programmatic ได้ เช่น ส่งไปยัง log) ให้ listen process.on('warning', ...):
Output (verified on Node 24)
default max listeners: 10
listener count: 11
warning name: MaxListenersExceededWarning
warning message: Possible EventEmitter memory leak detected. 11 data listeners added to [EventEmitter]. MaxListeners is 10. Use emitter.setMaxListeners() to increase limitถ้า 10 listener น้อยเกินไปจริง ๆ สำหรับ use case ที่ชอบธรรม (เช่น shared event bus ที่มีผู้ subscribe อิสระจำนวนมาก) ให้เพิ่ม limit อย่างชัดเจนด้วย emitter.setMaxListeners(n) — นั่นคือการบันทึกการตัดสินใจไว้แทนที่จะปล่อยให้เกินดีฟอลต์แบบเงียบ ๆ
events.on() และ events.once() — async iteration และ promise
หัวข้อที่มีชื่อว่า “events.on() และ events.once() — async iteration และ promise”events.once(emitter, name) คืนค่า Promise ที่ resolve ด้วย argument ของ event เมื่อมีการ emit ครั้งแรก — ทางเลือกแบบครั้งเดียวของ emitter.once() ที่ใช้ callback events.on(emitter, name) คืนค่า async iterable ที่ yield ทุกการ emit เพื่อใช้กับ for await:
Output (verified on Node 24)
once() resolved with: [ 'server' ]
tick: 1
tick: 2
tick: 3
finished iterating tickscaptureRejections — ส่ง error ของ async listener ไปที่ ‘error’
หัวข้อที่มีชื่อว่า “captureRejections — ส่ง error ของ async listener ไปที่ ‘error’”listener แบบ async ที่ throw จะสร้าง rejected promise ที่ EventEmitter ไม่ await — โดยดีฟอลต์มันจะกลายเป็น unhandled rejection ซึ่งทำให้ process crash บน Node สมัยใหม่ (ดีฟอลต์ --unhandled-rejections=throw) การส่ง captureRejections: true ให้ constructor จะส่ง rejection นั้นไปที่ event 'error' ของ emitter แทน ซึ่งคุณจัดการมันได้เหมือน error ทั่วไป:
Output (verified on Node 24)
emit() returned -- the throw happens later, on the microtask queue
caught via error event: async listener failed
doneevent ‘error’ ที่ไม่มี listener จะ throw
หัวข้อที่มีชื่อว่า “event ‘error’ ที่ไม่มี listener จะ throw”'error' เป็นชื่อ event พิเศษใน EventEmitter: ถ้ามันถูก emit และ ไม่มี listener ลงทะเบียนไว้เลย Node จะ throw error แทนที่จะทิ้งมันไปเงียบ ๆ ทำให้ process crash นี่เป็นความตั้งใจ — มันคือ event เดียวที่คุณไม่ได้รับอนุญาตให้เผลอมองข้าม
Output (verified on Node 24)
about to emit error with no listener...Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart TB
Loop["listener registered inside a loop / request handler"] -->|"each call adds a NEW listener"| Count["listenerCount keeps growing"]
Count -->|"exceeds getMaxListeners() (default 10)"| Warn["MaxListenersExceededWarning"]
Emit["emitter.emit('error', err)"] -->|"listener registered?"| Handled["'error' handler runs — process keeps going"]
Emit -->|"no listener registered"| Crash["Node throws — process crashes"]
Async["async listener throws"] -->|"captureRejections: false (default)"| Unhandled["unhandled rejection — crash"]
Async -->|"captureRejections: true"| Handled Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
callback style emitter.on() | ใช้ได้ทุกที่ ไม่มี dependency ทำงานแบบ synchronous และคาดเดาได้ | รั่วง่าย — ลงทะเบียนซ้ำภายใน loop/handler เป็นบั๊กที่เงียบ |
events.once(emitter, name) (promise) | อ่านเหมือน call ที่ await ทั่วไป ประกอบกับ Promise.race/AbortSignal ได้ | ยิงแค่ครั้งเดียว — ไม่ใช่ตัวแทนของ listener on() ที่เกิดซ้ำ |
events.on(emitter, name) (async iterator) | for await อ่านจากบนลงล่างเหมือน stream เป็นมิตรกับ backpressure | ไม่มีจุดจบอัตโนมัติ — คุณต้อง break เองหรือพึ่ง event 'end'/'close' |
captureRejections: true | error ของ async listener กลายเป็น event 'error' ธรรมดาแทนที่จะ crash | ปิดโดยดีฟอลต์ — emitter ทุกตัวที่คุณสร้างต้อง opt-in เอง |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “listener ของ EventEmitter ทำงานแบบ asynchronous เหมือน DOM event” — ความจริง:
.emit()เรียกทุก listener แบบ synchronous ตามลำดับการลงทะเบียน ก่อนที่emit()จะ return - “MaxListenersExceededWarning แปลว่ามีอะไรพัง” — ความจริง: มันเป็น heuristic warning ไม่ใช่ error — code ยังทำงานต่อ แต่มันชี้บั๊ก
EventEmitterที่พบบ่อยที่สุดได้อย่างถูกต้อง: listener ที่ถูกเพิ่มทุกครั้งที่เรียก function แทนที่จะเพิ่มครั้งเดียว - “event
'error'ที่ไม่มี listener จะถูกละเว้นไปเฉย ๆ เหมือน event อื่นที่ไม่มีใครฟัง” — ความจริง:'error'ถูกจัดการเป็นกรณีพิเศษ — ถ้าไม่มี listener เลยมันจะถูก throw ทำให้ process crash - “error ที่ถูก throw จาก
asynclistener จะถูกจับโดยemitter.on('error', ...)โดยอัตโนมัติ” — ความจริง: จะเกิดขึ้นก็ต่อเมื่อ emitter ถูกสร้างด้วยcaptureRejections: trueเท่านั้น ไม่งั้น rejection จะ unhandled และทำให้ process crash โดยไม่เกี่ยวกับ listener'error'เลย
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”.on()ถูกเรียกภายใน loop, request handler, class constructor ที่ถูกเรียกต่อ request หรือ re-render path — ที่ไหนก็ตามที่รันมากกว่าหนึ่งครั้งต่ออายุของ process หรือเปล่า? แต่ละครั้งที่เรียกจะเพิ่ม listener ใหม่เอี่ยมที่ไม่เคยถูกลบ- มีอะไรที่ extend หรือห่อ
EventEmitter(custom class, socket, stream) ที่ไม่มี listener'error'เลยตลอด call chain หรือเปล่า? การ emit'error'ที่ไม่มีคนจับแค่ครั้งเดียวทำให้ทั้ง process crash - มี
asyncfunction ถูกส่งตรงเข้า.on()โดยไม่มีcaptureRejections: trueบน emitter และไม่มีtry/catchภายในหรือเปล่า? error ที่มันโยนออกมาจะกลายเป็น unhandled rejection ไม่ใช่ error ที่ถูกจับ - code เรียก
events.on(emitter, name)แล้ว iterate ด้วยfor awaitแต่ไม่เคยbreakหรือพึ่งเงื่อนไขจบเลยหรือเปล่า? นั่นคือ listener ที่ไม่เคยถูกเคลียร์ เป็น leak แบบเดียวกับการลืมremoveListener
🐛 หา bug ในโค้ดนี้
const { EventEmitter } = require('node:events');const http = require('node:http');
var bus = new EventEmitter();
var server = http.createServer(function(req, res) {// registers a new listener on every single requestbus.on('request-logged', function(url) { console.log('logged:', url);});bus.emit('request-logged', req.url);res.end('ok');});เฉลย
bus.on('request-logged', ...) ถูกเรียกภายใน request handler ดังนั้น listener ใหม่เอี่ยมจะถูกลงทะเบียนทุก request ที่เข้ามาและไม่เคยถูกลบ หลังจาก 10 request Node จะพิมพ์ MaxListenersExceededWarning; หลังจากหลายพัน request process จะรั่วไหลหน่วยความจำ — closure ของ listener ทุกตัวยังมีชีวิตอยู่ และทุก emit('request-logged', ...) ในอนาคตจะรันทั้งหมดนั้น เพิ่มงานต่อ request ทวีคูณขึ้นเรื่อย ๆ วิธีแก้คือลงทะเบียน listener ครั้งเดียว นอก request handler ก่อนที่ server.listen() จะถูกเรียก — handler ควรเรียกแค่ bus.emit(...) ไม่ใช่ bus.on(...)