← readable.pause() writable.cork() →

Node.js readable.unshift():把已消费的数据放回缓冲区

著
原创 2026-10-17 Node.js 已有人查阅

一、unshift()的定位:消费侧的“回退”工具

可读流的内部缓冲区是先进先出结构。push()从生产者侧向尾部追加数据,read()从头部消费数据。unshift()的语义是在这两者之外增加一个反向操作:把已经交出去的数据重新放回头部。

官方文档对这一用途的表述很直接:当消费代码从源中“乐观地拉取”了超出所需的数据时,unshift()可以把多余部分还回去,让后续消费者或读取逻辑继续处理。典型场景是解析带有分隔符的协议头——你按块读取,在某个块中发现了\n\n边界,但该块在边界之后还有正文内容。此时把正文部分unshift回缓冲区,流的下一次读取就能从正文开始。

这个定位决定了unshift()不是日常流操作的高频方法。它的使用前提是:你已经通过read()或'data'事件拿到了数据,但决定不全部消费。

二、规范约束与异常边界

2.1参数类型

非对象模式下,chunk必须是string、Buffer、TypedArray、DataView或null。对象模式流中可以是任意JavaScript值。encoding参数仅在chunk为字符串时有效,必须是合法的Buffer编码(如'utf8'、'ascii')。

2.2'end'事件后调用会抛错

这是硬性约束:stream.unshift(chunk)不能在'end'事件发射之后调用,否则抛出运行时错误。原因在于'end'表示流已进入终止状态,内部读取状态机不再接受新的缓冲区写入。如果你的代码在异步回调中调用unshift(),而这个回调的执行时机可能晚于'end',需要显式检查stream.readableEnded或stream.destroyed。

2.3null的特殊语义

传入null时,unshift()的行为与push(null)相同:发出EOF信号。但有一个细节:EOF信号被放在缓冲区末端,而非头部,且缓冲区中已有的数据仍会被正常消费。这与“在头部插入一个结束标记”的直觉不同。实际项目中不应依赖unshift(null)来控制流结束,它的语义边界比push(null)更微妙。

三、最容易被忽略的风险:在_read中调用unshift()

官方文档用了一段专门文字警告这个场景:

与push不同,stream.unshift(chunk)不会通过重置流的内部读取状态来结束读取过程。如果在读取过程中(即自定义流的_read实现内部)调用readable.unshift(),可能产生意外结果。

原因在于unshift()不触发读取状态机的重置,而push()会。在_read回调内部,流正处于“正在获取数据”的状态。此时调用unshift()把数据塞回头部,状态机并不知道这次写入,可能导致后续_read的调用时机出现偏差。文档给出的缓解方式是紧接着调用一次push('')来重置状态,但更稳妥的做法是避免在_read内调用unshift()。

这条约束的实际意义:如果你在写自定义Readable实现,需要“把多读的数据还回去”,应该重新审视_read的逻辑边界,而不是依赖unshift()做状态修补。

四、与push()的职责对比

维度 push(chunk) unshift(chunk)
操作位置 缓冲区尾部 缓冲区头部
调用侧 生产者(_read实现内) 消费者(read()或'data'之后)
读取状态影响 会重置内部读取状态 不重置读取状态
典型用途 向流中注入数据 把多消费的数据还回流
'end'后调用 同样受限 抛运行时错误

两者的选择由数据流向决定。push()是“我有了新数据,加进去给消费者”。unshift()是“我拿多了,还回去”。在自定义流实现中误用unshift()代替push(),会破坏_read的调用节奏。

五、实践中的替代方案:优先考虑Transform流

官方文档在unshift()的说明中附带了一条建议:经常使用stream.unshift()的开发者应考虑改用Transform流。

这条建议的工程含义是:如果你的流处理逻辑涉及“解析、判断、可能回退一部分数据”的模式,它本质上是一个转换过程。Transform流允许你在_transform回调中自由决定输出什么、输出多少,不需要把数据塞回输入侧的缓冲区。输入和输出的分离让状态管理更清晰,也避开了unshift()与读取状态机的耦合问题。

个人经验:在一个自定义二进制协议解析模块中,最初用'readable'事件配合read()和unshift()来实现“读定长头、读变长体”的逻辑。代码在大多数情况下工作正常,但偶尔出现数据块“消失”或重复交付。定位后发现是在某些边界条件下,unshift()的调用时机落在了_read触发的同步执行窗口中。改用Transform流后,解析逻辑在_transform中按字节累积、判断、输出完整帧,不再需要回退操作,异常路径也变得可追踪。unshift()并非不能用,但当你发现需要反复调用它时,通常意味着消费侧的架构选型值得重新考虑。

六、常见误区速查

误区一:unshift()可以在任意时刻把数据放回头部
'end'之后调用抛运行时错误。异步回调中调用需检查流状态。

误区二:unshift(null)等同于立即结束流
行为与push(null)类似,但EOF标记在缓冲区末端,已有数据仍会被消费。不应将其作为控制流结束的手段。

误区三:unshift()可以替代push()在自定义流中使用
unshift()不重置读取状态,在_read内使用会干扰状态机。push()才是自定义Readable实现中向流注入数据的正确方法。

误区四:unshift()是高频操作
它适用于“偶尔回退”的场景。频繁使用意味着消费逻辑的粒度或架构需要调整。

误区五:unshift()返回流本身
该方法没有定义有意义的返回值,不应参与链式调用。

七、单元测试与边界验证

以下用例可在Node.jsnode:test中运行:

const test = require('node:test');
const assert = require('node:assert');
const { Readable } = require('node:stream');

test('unshift 将数据放回头部供后续读取', (t, done) => {
  const source = Readable.from(['header\n\nbody-part']);
  source.once('data', (chunk) => {
    const text = chunk.toString();
    const idx = text.indexOf('\n\n');
    const body = text.slice(idx + 2);
    source.unshift(body);
    source.once('data', (next) => {
      assert.strictEqual(next.toString(), 'body-part');
      done();
    });
  });
});

test('unshift 在 end 事件后调用抛出错误', (t, done) => {
  const source = Readable.from(['x']);
  source.on('end', () => {
    assert.throws(() => source.unshift('late'), /unshift/);
    done();
  });
  source.resume();
});

test('unshift 后读取顺序为头部优先', (t, done) => {
  const source = Readable.from(['original']);
  source.once('data', (chunk) => {
    assert.strictEqual(chunk.toString(), 'original');
    source.unshift('prepended');
    source.once('data', (next) => {
      assert.strictEqual(next.toString(), 'prepended');
      done();
    });
  });
});

第二个用例验证了'end'后的异常边界。第三个用例说明unshift的数据确实进入了头部队列,下一次读取优先交付。

八、延伸阅读与参考文献

Node.js官方文档:readable.unshift(chunk[,encoding])
https://nodejs.org/api/stream.html#readableunshiftchunk-encoding
注解:规范原文,包含'end'后抛错、_read内调用风险、Transform替代建议的完整说明。

Node.js官方文档:APIforStreamImplementers
https://nodejs.org/api/stream.html#api-for-stream-implementers
注解:自定义流实现中_read与push()的协作规则,是理解unshift()为何不适合在_read内使用的上下文依据。

Node.js官方文档:readable.push(chunk[,encoding])
https://nodejs.org/api/stream.html#readablepushchunk-encoding
注解:与unshift()对照阅读,明确两者对读取状态机的不同影响。

MicrosoftLearn:AzureStorageBlobRetriableReadableStream
https://learn.microsoft.com/en-us/javascript/api/@azure/storage-blob/retriablereadablestream
注解:实际生产库中对unshift()的使用示例,展示了在分块读取场景中回退多余数据的模式。

练习与思考

  1. 编写一个基于'readable'事件和read()的解析器,从流中提取以\r\n分隔的文本行,当某次read()拿到的块包含不完整行时,用unshift()将剩余部分还回流中。然后将其改写为Transform流,对比两种写法的错误处理路径。

  2. 构造一个自定义Readable流,在_read方法中故意调用unshift(),观察'data'事件的发射顺序是否出现异常。解释观察到的现象与读取状态机的关系。

  3. 思考:如果在一个已经调用过destroy()的流上调用unshift(),会发生什么?用代码验证你的判断。

← readable.pause() writable.cork() →
分享笔记 (共有 篇笔记)
验证码:
微信公众号