ข้ามไปยังเนื้อหา

EventEmitter

EventEmitter (จาก node:events) คือ class ที่ขับเคลื่อนแทบทุก I/O object ใน Node.js HTTP server, TCP socket, file stream, child process และไลบรารีบุคคลที่สามจำนวนมากล้วน extend EventEmitter การเข้าใจมันคือการเข้าใจว่า Node สื่อสารระหว่าง component ต่าง ๆ อย่างไร

Node.js
Output (verified on Node 24)
Hello, Alice!
Hello, Bob!
message: first

once ลงทะเบียน listener ที่ลบตัวเองอัตโนมัติหลังจากการ emit ครั้งแรก

Node.js
Output (verified on Node 24)
Connected! (fires once only)
listener count: 0

extend EventEmitter เพื่อให้ class ของคุณเองมีความสามารถแบบ event-driven

Node.js
Output (verified on Node 24)
loaded item: alpha
loaded item: beta
loaded item: gamma
all done! total: 3 items
items: alpha, beta, gamma

off เป็น alias ของ removeListener ที่เพิ่มเข้ามาใน Node 10 listenerCount ช่วยให้ตรวจสอบได้ว่ามี listener ลงทะเบียนไว้กี่ตัวสำหรับ event หนึ่ง — มีประโยชน์สำหรับตรวจจับ memory leak ที่อาจเกิดขึ้น

Node.js
Output (verified on Node 24)
count before off: 2
count after off: 1
handler2

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', ...):

Node.js
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.once(emitter, name) คืนค่า Promise ที่ resolve ด้วย argument ของ event เมื่อมีการ emit ครั้งแรก — ทางเลือกแบบครั้งเดียวของ emitter.once() ที่ใช้ callback events.on(emitter, name) คืนค่า async iterable ที่ yield ทุกการ emit เพื่อใช้กับ for await:

Node.js
Output (verified on Node 24)
once() resolved with: [ 'server' ]
tick: 1
tick: 2
tick: 3
finished iterating ticks

listener แบบ async ที่ throw จะสร้าง rejected promise ที่ EventEmitter ไม่ await — โดยดีฟอลต์มันจะกลายเป็น unhandled rejection ซึ่งทำให้ process crash บน Node สมัยใหม่ (ดีฟอลต์ --unhandled-rejections=throw) การส่ง captureRejections: true ให้ constructor จะส่ง rejection นั้นไปที่ event 'error' ของ emitter แทน ซึ่งคุณจัดการมันได้เหมือน error ทั่วไป:

Node.js
Output (verified on Node 24)
emit() returned -- the throw happens later, on the microtask queue
caught via error event: async listener failed
done

'error' เป็นชื่อ event พิเศษใน EventEmitter: ถ้ามันถูก emit และ ไม่มี listener ลงทะเบียนไว้เลย Node จะ throw error แทนที่จะทิ้งมันไปเงียบ ๆ ทำให้ process crash นี่เป็นความตั้งใจ — มันคือ event เดียวที่คุณไม่ได้รับอนุญาตให้เผลอมองข้าม

Node.js
Output (verified on Node 24)
about to emit error with no listener...
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
listener count ที่โตขึ้นเรื่อย ๆ ทำให้เกิด warning; event 'error' ที่ไม่มี listener ทำให้ process crash; captureRejections ส่ง error ของ async listener ไปในทางเดียวกัน
ทางเลือกข้อดีต้นทุน
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: trueerror ของ async listener กลายเป็น event 'error' ธรรมดาแทนที่จะ crashปิดโดยดีฟอลต์ — emitter ทุกตัวที่คุณสร้างต้อง opt-in เอง
  • “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 จาก async listener จะถูกจับโดย emitter.on('error', ...) โดยอัตโนมัติ” — ความจริง: จะเกิดขึ้นก็ต่อเมื่อ emitter ถูกสร้างด้วย captureRejections: true เท่านั้น ไม่งั้น rejection จะ unhandled และทำให้ process crash โดยไม่เกี่ยวกับ listener 'error' เลย
  • .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
  • มี async function ถูกส่งตรงเข้า .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 request
bus.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(...)

ความแตกต่างหลักระหว่าง emitter.on() กับ emitter.once() คืออะไร?
listener ของ EventEmitter ทำงานเมื่อไหร่เทียบกับ emitter.emit()?
Node.js built-in class ใดบ้างที่ extend EventEmitter?
เกิดอะไรขึ้นเมื่อโค้ดนี้รัน? const { EventEmitter } = require('node:events'); var e = new EventEmitter(); e.emit('error', new Error('boom')); console.log('after emit');
function ลงทะเบียน bus.on('saved', handler) ทุกครั้งที่มันถูกเรียก และ function นี้ถูกเรียกครั้งละหนึ่งครั้งต่อ HTTP request อาการที่สังเกตได้หลังจาก request ที่ 10 ขึ้นไปคืออะไร?
EventEmitter ถูกสร้างเป็น new EventEmitter({ captureRejections: true }) มีอะไรเปลี่ยนไปกับ listener ที่ลงทะเบียนเป็น emitter.on('work', async () => { throw new Error('x') })?