Web Streams标准流隐藏的陷阱(WritableStream 篇)

13 阅读2分钟

一、缓存writer.ready导致背压控制失效。

写数据时流检查是否背压,如果是,为writer.ready创建新的未决Promise,注册到ready的回调不调用而不写数据。处理写入的数据时流检查是否解除背压,如果是,兑现这个Promise,调用ready注册的回调向流写数据。这意味着ready是随着流的背压和解除背压动态改变的。如果缓存ready,它是缓存时ready的“快照”,不反映流背压和解除背压的变化

使用ready写数据的初衷是接受背压控制。比如:

let  ready=writer.ready

ready.then( ()=>]()writer.write(data1) );

第一条语句缓存写入器的ready,第二条语句用缓存的ready向流写数据data1。假定流背压,则ready未决,向其注册的回调不会调用,故不调用write()向流写数据data1。流解除背压,ready兑现, ready注册的回调被调用写数据data1到流。特别地,之后每次用缓存的ready写数据,无论流背压还是解除背压,因为它是总是兑现的,数据都会写到流,因此违背了背压控制写数据的初衷。

 链式调用也隐式缓存ready。比如:

writer.ready.then( ()=>writer.write (data1)()).then(()=>writer.write (data2) )…then( ()=>writer.write (dataN)) ;  

  除第一个then外,后续的所有then依附于前一个write()返回的 Promise,而不是 writer.ready。

正确的做法是每次写数据时都直接访问 writer.ready。比如:

    writer.ready.then( ()=>writer.write(data1);writer.ready.then(…))

用async/await简化为

async writeData(){

for(…){

        await    writer.ready.then(()=>writer.write (datai) )

    }

 }

二、关闭流时数据丢失的风险

比如:

     writer.ready.then( ()=> writer.write(data1) );

     writer.close();

本意是把数据data1写到流后再关闭流。但实际情况是数据data1没有写到流而被丢失。对于一个Promise,无论它的状态是否改变,向它注册的回调总是异步调用。本例向ready注册()=>writer.write(data1)回调,接着调用close()关闭流,这意味这ready的回调永远得不到调用而导致数据data1被丢失。

规范已考虑到这个问题,close()先把流的[[CloseRequest]]设置为它返回的Promise,接着在流背压的情况下兑现写入器的ready,原意是调用因ready未决而没有调用把数据写到流的回调,确保数据写到流,然后再关闭流。但实际情况却是在关闭流之前数据仍然不会写到流。这是因为,兑现ready,它的回确实调用了,不过是异步调用的,导致它调用的write()在间接检查流的[[CloseRequest]]时因被close()提前设置而不会写数据。

 注意:即使在close()方法中方法中调整兑现ready和设置 [[CloseRequest]]执行顺序,结果也一样。

正确的关闭方法逻辑是:

writer.ready.then( ()=>writer.write(data1);writer.close())