NodeJS EventEmitter

10 阅读2分钟
class MyEventEmitter {
  constructor() {
    this.events = Object.create(null);
  }

  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
    return this;
  }

  emit(event, ...args) {
    const listeners = this.events[event];
    if (!listeners) return false;

    for (const listener of [...listeners]) {
      listener(...args);
    }
    return true;
  }

  off(event, listener) {
    const listeners = this.events[event];
    if (!listeners) return this;

    this.events[event] = listeners.filter(
      (fn) => fn !== listener && fn._original !== listener
    );

    if (this.events[event].length === 0) {
      delete this.events[event];
    }

    return this;
  }

  once(event, listener) {
    const wrapper = (...args) => {
      this.off(event, wrapper);
      listener(...args);
    };

    wrapper._original = listener;
    return this.on(event, wrapper);
  }
}

EventEmitter+Promise封装

function waitForEvent(emitter, eventName){
return new Promise((resolve,reject)=>{
    function onSuccess(...args){
        cleanup()
        resolve(args)
    }
    function onError(err){
        cleanup()
        reject(err)
    } 
    function cleanup(){
        emitter.off(eventName,onSuccess)
        emitter.off('error',onError)
    }
    emitter.on(eventName,onSuccess)
    emitter.on('error'onError)
})
}

Stream生命周期完整图

stream流有:Readable,Writeable,Duplex/Transform四种流

Readable(生产数据) → Writable(消费数据)

Readable:
  start → data → data → ... → end → close

Writable:
  write → write → ... → end() → finish → close

Duplex / Transform:
  Readable + Writable 两套生命周期并行运行

其中:

  • end表示Readable流已经没有数据可读,是输入结束;
  • finish表示Wrutable流得数据已经全部写入底层系统,是输出完成
  • close表示流机器底层资源已经关闭,是流生命周期得最终结束

🔑 Readable 核心事件

事件含义
data来了一块数据
end没数据了
close资源关闭

🔑 Writable 核心事件

事件含义
drain缓冲区恢复
finish写入完成
close资源关闭

即: Node.js Stream 的生命周期分为三个阶段:
Readable 流通过 data 事件持续产出数据,并在 end 时结束;
Writable 流通过 write 接收数据,在 end() 后触发 finish 表示写入完成;
close 表示底层资源释放,是最终阶段。
在 pipe 过程中,通过 write 返回值和 drain 事件实现背压控制,使不同速度的流能够安全协作。

手写一个Transform:按行分割

const {Transform } = require('node:stream');

class LineSplitTransform extends Transform {
    constructor(options = {}){
        super({...options, readableObjectMode: true});
        thie.leftover = '';
    }
    
    _transform(chunk, encoding, callback) {
     try{
     const text = this.leftover + chunk.toString();
     const lines = text.split('\n');
     
     this.leftover = lines.pop();
     
     for(const line of lines){
     this.push(line);
     }
     callback();
     
     }catch(err){
     callback(err)
     }
    }
    
    _flush(callback){
        try{
            if(this.leftover){
            this.push(this.leftover);
            }
            callback()
        
        }catch(err){
     callback(err)
     }
    
    }
}

  • _transform() 负责持续处理

  • _flush() 负责收尾

这个例子为什么更高级

假设输入分块是这样:

hello\nwo
rld\nabc

第一次 _transform()

  • leftover + chunk 得到 hello\nwo
  • split('\n') 后是 ['hello', 'wo']
  • pop() 把最后不完整的 'wo' 留下来
  • 推出完整行 'hello'

第二次 _transform()

  • 拼上 leftover,变成 world\nabc
  • split('\n') 后是 ['world', 'abc']
  • pop()'abc' 留下来
  • 推出完整行 'world'

最后流结束时 _flush()

  • 把最后剩下的 'abc' 推出去

这就是 _flush() 的价值

为什么这里用了 this.push(),而前面例子用了 callback(null, output)

两种都可以。

方式 1:简单场景

callback(null, output);

适合:

  • 一块输入通常只对应一块输出

方式 2:复杂场景

this.push(...);
this.push(...);
callback();

适合:

  • 一块输入可能变成多块输出
  • 或者一块输入可能暂时不输出
  • 或者要自己精细控制输出节奏

像“按行拆分”这种,一块 chunk 可能拆成很多行,所以更适合 this.push()