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\nwosplit('\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()。