先记住这个答案
Transferable 对象在 postMessage 时通过转移列表传参,底层存储被移至接收方,不做结构化克隆的拷贝。例如传 ArrayBuffer,内存块在上下文中直接转移,耗时与大小无关。转移后原缓冲区被分离,byteLength 变 0,除 byteLength 等只读状态属性外,对数据的任何读写操作都会抛异常。这让内存物理上单一所有权,省去拷贝时间,也消除了因主线程和 Worker 同时引用而误改的问题,是传输大二进制数据的首选机制。
- Transferable 让 ArrayBuffer 零拷贝移交。
- 原对象转移后立即失去资源,不可再读写。
- 仅部分对象支持 transfer,需使用转移列表。
转移的底层机制
通常 postMessage 用结构化克隆深拷贝数据,但对于 100MB 的 ArrayBuffer,每个字节复制一遍代价高。Transferable 机制通过第二个参数(转移数组)指定某些可转移对象,其底层资源会被“搬移”而非常量复制。在浏览器内部,例如 ArrayBuffer 对应一块内存,转移时其底层内存资源会被整体移交到新上下文,原 ArrayBuffer 则被分离(detached),不再持有该内存。此时数据参数仍可以是包含该缓冲区的对象,如 TypedArray 或自定义对象,转移时用 [buffer] 指定实际要 move 的资源。
转移后的 ArrayBuffer 在源上下文中不再可用。若尝试读写会抛 TypeError,但读取 byteLength 会返回 0(可用来判断状态)。安全方面,因为内存只有唯一拥有者,避免两个线程同时写同一块内存造成竞态,也免得克隆后两边不同步导致逻辑错乱。注意,可转移对象范围受限,如 TypedArray 本身不可转移,必须转移其 .buffer;OffscreenCanvas、ImageBitmap、MessagePort 等也有各自的转移语义。
音频编辑中的大缓冲传递
比如一个音频编辑应用,主线程从磁盘读取 64MB 的 PCM 采样到 ArrayBuffer,需要交给一个 Worker 做滤波。若直接传该 buffer,浏览器会走结构化克隆,把 64MB 逐字节复制一次,内存瞬间翻倍,界面卡顿。改用 worker.postMessage({samples: buf}, [buf]),内存块直接从主线程“换手”给 Worker,传输为零拷贝操作,开销不随数据大小线性增长。Worker 拿到后处理,再通过转移把另一块大缓存传回主线程用于渲染。
这里的关键问题是决策:一旦 buf 被转移,主线程的 buf.byteLength 变 0,后续代码不能再引用它。因此主线程必须保证不再对原缓冲区做任何读写。这在管线设计中常见:生产者负责生成数据并转移,消费者拿到唯一所有权,处理完毕再转移回(或新建)。这样的协议让大块数据流经多个线程时既不复制、又不会出现两个线程同时操作一块内存。
适用边界与高风险情形
转移并非万金油。若数据需要在多个上下文间长期共享,或源端在传递后仍需读部分内容,就不宜用 Transferable,因为原对象立即失效。且不是所有对象都可转移,只有实现了 [Transferable] 接口的类型(如 ArrayBuffer、MessagePort、ImageBitmap、ReadableStream 等)才能用。若误将不可转移的对象放入转移数组,浏览器会抛 DataCloneError。
另一个边界是,转移只解决一次性搬运,不解决“并发访问”。若想多个 Worker 同时读写同一块内存,应使用 SharedArrayBuffer 并配合原子操作,但这又会带来安全要求(跨源隔离)。所以在实际项目中,需要判断数据流是单向流水线还是共享读写池。如果每次传输后源上下文立即放弃所有权,用 Transferable 能最大收益;若需要保留引用副本,保留结构化克隆的代价有时反而可控。
容易答错的地方
- 转移后原对象还可读取
- 转移后 ArrayBuffer 被分离,对数据的读取或写入会抛 TypeError,但
byteLength属性仍可读取且返回 0(这是规范允许的),不要误以为数据还在,只是被清空。 - 所有对象都能用 transferable 加快传输
- 只有明确支持转移的对象才能出现在转移列表中,如 ArrayBuffer、MessagePort、某些流类型。普通对象如 TypedArray 本身不行,需转其 buffer,否则触发 DataCloneError。
面试官还会怎么问?
Transferable 与结构化克隆在结果上有什么本质区别?
结构化克隆生成一个完全独立的副本,两边各自操作互不影响;Transferable 则把所有权搬走,源对象变成空壳,内存只有一端可用,因此两者表面结果相似,但生命周期和数据语义完全不同。
为什么 ArrayBuffer 可以转移而 TypedArray 不能?
TypedArray 是对 ArrayBuffer 的视图,其本身没有底层资源,只有引用关系;转移必须作用于拥有资源的对象。规范将 TypedArray 定义为可序列化但不可转移,要转移请传递它的 .buffer。
什么情况下转移会失败?
当对象不在可转移列表(如普通 TypedArray)或对象已被转移(处于 detached 状态)时,将它们放入转移列表会抛 DataCloneError;若转移列表中的对象未出现在消息数据中,根据 HTML 规范,序列化时会校验该对象必须包含在消息内容里,否则同样抛 DataCloneError。因此应确保转移数组中的资源与消息数据正确关联。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。