nodejs源码分析之可写流

可写流是对数据流向的抽象,用户调用可写流的接口,可写流负责控制数据的写入。流程如图1所示。

下面是可写流的代码逻辑图如图2所示。

我们看一下可写流的实现。

1 WritableState

WritableState是管理可写流配置的类。里面包含了非常的字段,具体含义我们会在后续分析的时候讲解。

1.  function WritableState(options, stream) {  
2.    options = options || {};  
3.    
4.    // 是不是双向流  
5.    var isDuplex = stream instanceof Stream.Duplex;  
6.    
7.    // 数据模式  
8.    this.objectMode = !!options.objectMode;  
9.    /*
10.    双向流的流默认共享objectMode配置,
11.    用户可以自己配置成非共享,即读流和写流的数据模式独立
12.   */  
13.   if (isDuplex)  
14.     this.objectMode = this.objectMode || 
15.                         !!options.writableObjectMode;  
16.   
17.   /*  
18.     阈值,超过后说明需要暂停调用write,0代表每次调用write
19.     的时候都返回false,用户等待drain事件触发后再执行write 
20.   */  
21.   this.highWaterMark = getHighWaterMark(this, 
22.                options, 'writableHighWaterMark',isDuplex);  
23.   
24.   // 是否调用了_final函数  
25.   this.finalCalled = false;  
26.   
27.   // 是否需要触发drain事件,重新驱动生产者  
28.   this.needDrain = false;  
29.    
30.   // 正在执行end流程  
31.   this.ending = false;  
32.   
33.   // 是否执行过end函数  
34.   this.ended = false;  
35.    
36.   // 是否触发了finish事件  
37.   this.finished = false;  
38.   
39.   // 流是否被销毁了  
40.   this.destroyed = false;  
41.   
42.   var noDecode = options.decodeStrings === false;  
43.   // 是否需要decode流数据后在执行写(调用用户定义的_write)  
44.   this.decodeStrings = !noDecode;  
45.   
46.   // 编码类型  
47.   this.defaultEncoding = options.defaultEncoding || 'utf8';  
48.   
49.   // 待写入的数据长度或对象数  
50.   this.length = 0;  
51.   
52.   // 正在往底层写  
53.   this.writing = false;  
54.   
55.   // 加塞,缓存生产者的数据,停止往底层写入  
56.   this.corked = 0;  
57.   
58.   // 用户定义的_write或者_writev是同步还是异步调用可写流的回调函数onwrite  
59.   this.sync = true;  
60.   
61.   // 是否正在处理缓存的数据  
62.   this.bufferProcessing = false;  
63.   
64.   // 用户实现的钩子_write函数里需要执行的回调,告诉写流写完成了  
65.   this.onwrite = onwrite.bind(undefined, stream);  
66.   
67.   // 当前写操作对应的回调  
68.   this.writecb = null;  
69.   
70.   // 当前写操作的数据长度或对象数  
71.   this.writelen = 0;  
72.   
73.   // 缓存的数据链表头指针  
74.   this.bufferedRequest = null;  
75.   
76.   // 指向缓存的数据链表最后一个节点  
77.   this.lastBufferedRequest = null;  
78.   
79.   // 待执行的回调函数个数  
80.   this.pendingcb = 0;  
81.   
82.   // 是否已经触发过prefinished事件  
83.   this.prefinished = false;  
84.   
85.   // 是否已经触发过error事件  
86.   this.errorEmitted = false;  
87.   
88.   // count buffered requests  
89.   // 缓存的buffer数  
90.   this.bufferedRequestCount = 0;  
91.   
92.   /* 
93.     空闲的节点链表,当把缓存数据写入底层时,corkReq保数据的上下文(如 
94.     用户回调),因为这时候,缓存链表已经被清空,
95.     this.corkedRequestsFree始终维护一个空闲节点,最多两个 
96.   */  
97.   var corkReq = { next: null, entry: null, finish: undefined };  
98.   corkReq.finish = onCorkedFinish.bind(undefined, corkReq, this);  
99.   this.corkedRequestsFree = corkReq;  
100.  }  

2 Writable

Writable是可写流的具体实现,我们可以直接使用Writable作为可写流来使用,也可以继承Writable实现自己的可写流。

1.  function Writable(options) {  
2.    this._writableState = new WritableState(options, this);  
3.    // 可写  
4.    this.writable = true;  
5.    // 支持用户自定义的钩子  
6.    if (options) {  
7.      if (typeof options.write === 'function')  
8.        this._write = options.write;  
9.    
10.     if (typeof options.writev === 'function')  
11.       this._writev = options.writev;  
12.   
13.     if (typeof options.destroy === 'function')  
14.       this._destroy = options.destroy;  
15.   
16.     if (typeof options.final === 'function')  
17.       this._final = options.final;  
18.   }  
19.   
20.   Stream.call(this);  
21. }  

可写流继承于流基类,提供几个钩子函数,用户可以自定义钩子函数实现自己的逻辑。如果用户是直接使用Writable类作为可写流,则options.write函数是必须传的,options.wirte函数控制数据往哪里写,并且通知可写流是否写完成了。如果用户是以继承Writable类的形式使用可写流,则_write函数是必须实现的,_write函数和options.write函数的作用是一样的。

3 数据写入

可写流提供write函数给用户实现数据的写入,写入有两种方式。一个是逐个写,一个是批量写,批量写是可选的,取决于用户的实现,如果用户直接使用Writable则需要传入writev,如果是继承方式使用Writable则实现_writev函数。我们先看一下write函数的实现

1.  Writable.prototype.write = function(chunk, encoding, cb) {  
2.    var state = this._writableState;  
3.    // 告诉用户是否还可以继续调用write  
4.    var ret = false;  
5.    // 数据格式  
6.    var isBuf = !state.objectMode && Stream._isUint8Array(chunk);  
7.    // 是否需要转成buffer格式  
8.    if (isBuf && Object.getPrototypeOf(chunk) !== Buffer.prototype) {  
9.      chunk = Stream._uint8ArrayToBuffer(chunk);  
10.   }  
11.   // 参数处理,传了数据和回调,没有传编码类型  
12.   if (typeof encoding === 'function') {  
13.     cb = encoding;  
14.     encoding = null;  
15.   }  
16.   // 是buffer类型则设置成buffer,否则如果没传则取默认编码  
17.   if (isBuf)  
18.     encoding = 'buffer';  
19.   else if (!encoding)  
20.     encoding = state.defaultEncoding;  
21.   
22.   if (typeof cb !== 'function')  
23.     cb = nop;  
24.   // 正在执行end,再执行write,报错  
25.   if (state.ending)  
26.     writeAfterEnd(this, cb);  
27.   else if (isBuf || validChunk(this, state, chunk, cb)) {  
28.     // 待执行的回调数加一,即cb  
29.     state.pendingcb++;  
30.     // 写入或缓存,见该函数  
31.     ret = writeOrBuffer(this, state, isBuf, chunk, encoding, cb);  
32.   }  
33.   /// 还能不能继续写  
34.   return ret;  
35. };  

write函数首先做了一些参数处理和数据转换,然后判断流是否已经结束了,如果流结束再执行写入,则会报错。如果流没有结束则执行写入或者缓存处理。最后通知用户是否还可以继续调用write写入数据(我们看到如果写入的数据比阈值大,可写流还是会执行写入操作,但是会返回false告诉用户些不要写入了,如果调用方继续写入的话,也是没会继续写入的,但是可能会导致写入端压力过大)。我们首先看一下writeAfterEnd的逻辑。然后再看writeOrBuffer。

1.  function writeAfterEnd(stream, cb) {  
2.    var er = new errors.Error('ERR_STREAM_WRITE_AFTER_END');  
3.    stream.emit('error', er);  
4.    process.nextTick(cb, er);  
5.  }  

writeAfterEnd函数的逻辑比较简单,首先触发可写流的error事件,然后下一个tick的时候执行用户在调用write时传入的回调。接着我们看一下writeOrBuffer。writeOrBuffer函数会对数据进行缓存或者直接写入目的地(目的地可以是文件、socket、内存,取决于用户的实现),取决于当前可写流的状态。

1.  function writeOrBuffer(stream, state, isBuf, chunk, encoding, cb) {  
2.    // 数据处理  
3.    if (!isBuf) {  
4.      var newChunk = decodeChunk(state, chunk, encoding);  
5.      if (chunk !== newChunk) {  
6.        isBuf = true;  
7.        encoding = 'buffer';  
8.        chunk = newChunk;  
9.      }  
10.   }  
11.   // 对象模式的算一个  
12.   var len = state.objectMode ? 1 : chunk.length;  
13.   // 更新待写入数据长度或对象个数  
14.   state.length += len;  
15.   // 待写入的长度是否超过了阈值  
16.   var ret = state.length < state.highWaterMark;  
17.     
18.   /*
19.     超过了阈值,则设置需要等待drain事件标记,
20.     这时候用户不应该再执行write,而是等待drain事件触发
21.   */  
22.   if (!ret)  
23.     state.needDrain = true;  
24.   // 如果正在写或者设置了阻塞则先缓存数据,否则直接写入  
25.   if (state.writing || state.corked) {  
26.     // 指向当前的尾节点  
27.     var last = state.lastBufferedRequest;  
28.     // 插入新的尾结点  
29.     state.lastBufferedRequest = {  
30.       chunk,  
31.       encoding,  
32.       isBuf,  
33.       callback: cb,  
34.       next: null  
35.     };  
36.     /*
37.       之前还有节点的话,旧的尾节点的next指针指向新的尾节点,
38.       形成链表
39.      */  
40.     if (last) {  
41.       last.next = state.lastBufferedRequest;  
42.     } else {  
43.       /*
44.         指向buffer链表,bufferedRequest相等于头指针,
45.         插入第一个buffer节点的时候执行到这  
46.        */
47.       state.bufferedRequest = state.lastBufferedRequest;  
48.     }  
49.     // 缓存的buffer个数加一  
50.     state.bufferedRequestCount += 1;  
51.   } else {  
52.     // 直接写入  
53.     doWrite(stream, state, false, len, chunk, encoding, cb);  
54.   }  
55.   // 返回是否还可以继续执行wirte,如果没有达到阈值则可以继续写  
56.   return ret;  
57. }  

writeOrBuffer函数主要的逻辑如下
1 更新待写入数据的长度,判断是否达到阈值,然后通知用户是否还可以执行write继续写入。
2 判断当前是否正在写入或者处于cork模式。是的话把数据缓存起来,否则执行写操作。
我们看一下缓存的逻辑和形成的数据结构。
缓存第一个节点时,如图3所示。

缓存第二个节点时,如图4所示。

缓存第三个节点时,如图5

我们看到,函数的数据是以链表的形式管理的,其中bufferedRequest是链表头结点,lastBufferedRequest指向尾节点。假设当前可写流不处于写入或者cork状态。我们看一下写入的逻辑。

1.  function doWrite(stream, state, writev, len, chunk, encoding, cb) {  
2.    // 本次写入的数据长度  
3.    state.writelen = len;  
4.    // 本次写完成后执行的回调  
5.    state.writecb = cb;  
6.    // 正在写入  
7.    state.writing = true;  
8.    // 假设用户定义的_writev或者_write函数是同步回调onwrite  
9.    state.sync = true;  
10.   if (writev)  
11.     // chunk为缓存待写入的buffer节点数组  
12.     stream._writev(chunk, state.onwrite);  
13.   else  
14.     // 执行用户定义的写函数,onwrite是Node.js定义的,在初始化的时候设置了该函数  
15.     stream._write(chunk, encoding, state.onwrite);  
16.   /*
17.     如果用户是同步回调onwrite,则这句代码没有意义,
18.     如果是异步回调onwrite,这句代码会在onwrite之前执行,
19.     它标记用户是异步回调模式,在onwrite中需要判断回调模式,即sync的值
20.   */
21.   state.sync = false;  
22. }  

doWrite函数记录了本次写入的上下文,比如长度,回调,然后设置正在写标记。最后执行写入。如果当前待写入的数据是缓存的数据并且用户实现了_writev函数,则调用_writev。否则调用_write。下面我们实现一个可写流的例子,把这里的逻辑串起来。

1.  const { Writable } = require('stream');  
2.  class DemoWritable extends Writable {  
3.      constructor() {  
4.           super();
5.          this.data = null;  
6.      }  
7.      _write(chunk, encoding, cb) {  
8.          // 保存数据  
9.          this.data = this.data ? Buffer.concat([this.data, chunk]) : chunk;  
10.         // 执行回调告诉可写流写完成了,false代表写成功,true代表写失败  
11.         cb(null);  
12.     }  
13. } 

DemoWritable定义了数据流向的目的地,在用户调用write的时候,可写流会执行用户定义的_write,_write保存了数据,然后执行回调并传入参数,通知可写流数据写完成了,并通过参数标记写成功还是失败。这时候回到可写流侧。我们看到可写流设置的回调是onwrite,onwrite是在初始化可写流的时候设置的。

1.  this.onwrite = onwrite.bind(undefined, stream);  
我们接着看onwrite的实现。
1.  function onwrite(stream, er) {  
2.    var state = stream._writableState;  
3.    var sync = state.sync;  
4.    // 本次写完时执行的回调  
5.    var cb = state.writecb;  
6.    // 重置内部字段的值  
7.    // 写完了,重置回调,还有多少单位的数据没有写入,数据写完,重置本次待写入的数据数为0  
8.    state.writing = false;  
9.    state.writecb = null;  
10.   state.length -= state.writelen;  
11.   state.writelen = 0;  
12.   // 写出错  
13.   if (er)  
14.     onwriteError(stream, state, sync, er, cb);  
15.   else {  
16.     // 是否已经执行了end,并且数据也写完了(提交写操作和最后真正执行中间可能执行了end)  
17.     var finished = needFinish(state);  
18.     // 还没结束,并且没有设置阻塞标记,也不在处理buffer,并且有待处理的缓存数据,则进行写入  
19.     if (!finished &&  
20.         !state.corked &&  
21.         !state.bufferProcessing &&  
22.         state.bufferedRequest) {  
23.       clearBuffer(stream, state);  
24.     }  
25.     // 用户同步回调onwrite则Node.js异步执行用户回调  
26.     if (sync) {  
27.       process.nextTick(afterWrite, stream, state, finished, cb);  
28.     } else {  
29.       afterWrite(stream, state, finished, cb);  
30.     }  
31.   }  
32. }  

onwrite的逻辑如下
1 更新可写流的状态和数据
2 写出错则触发error事件和执行用户回调,写成功则判断是否满足继续执行写操作,是的话则继续写,否则执行用户回调。
我们看一下clearBuffer函数的逻辑,该逻辑主要是把缓存的数据写到目的地。

1.  function clearBuffer(stream, state) {  
2.    // 正在处理buffer  
3.    state.bufferProcessing = true;  
4.    // 指向头结点  
5.    var entry = state.bufferedRequest;  
6.    // 实现了_writev并且有两个以上的数据块,则批量写入,即一次把所有缓存的buffer都写入  
7.    if (stream._writev && entry && entry.next) {  
8.      // Fast case, write everything using _writev()  
9.      var l = state.bufferedRequestCount;  
10.     var buffer = new Array(l);  
11.     var holder = state.corkedRequestsFree;  
12.     // 指向待写入数据的链表  
13.     holder.entry = entry;  
14.   
15.     var count = 0;  
16.     // 数据是否全部都是buffer格式  
17.     var allBuffers = true;  
18.     // 把缓存的节点放到buffer数组中  
19.     while (entry) {  
20.       buffer[count] = entry;  
21.       if (!entry.isBuf)  
22.         allBuffers = false;  
23.       entry = entry.next;  
24.       count += 1;  
25.     }  
26.     buffer.allBuffers = allBuffers;  
27.   
28.     doWrite(stream, state, true, state.length, buffer, '', holder.finish);  
29.   
30.     // 待执行的cb加一,即holder.finish  
31.     state.pendingcb++;  
32.     // 清空缓存队列  
33.     state.lastBufferedRequest = null;  
34.     // 还有下一个节点则更新指针,下次使用  
35.     if (holder.next) {  
36.       state.corkedRequestsFree = holder.next;  
37.       holder.next = null;  
38.     } else {  
39.       // 没有下一个节点则恢复值,见初始化时的设置  
40.       var corkReq = { next: null, entry: null, finish: undefined };  
41.       corkReq.finish = onCorkedFinish.bind(undefined, corkReq, state);  
42.       state.corkedRequestsFree = corkReq;  
43.     }  
44.     state.bufferedRequestCount = 0;  
45.   } else {  
46.     // 慢慢写,即一个个buffer写,写完后等需要执行用户的cb,驱动下一个写  
47.     // Slow case, write chunks one-by-one  
48.     while (entry) {  
49.       var chunk = entry.chunk;  
50.       var encoding = entry.encoding;  
51.       var cb = entry.callback;  
52.       var len = state.objectMode ? 1 : chunk.length;  
53.       // 执行写入  
54.       doWrite(stream, state, false, len, chunk, encoding, cb);  
55.       entry = entry.next;  
56.       // 处理完一个,减一  
57.       state.bufferedRequestCount--;  
58.        
59.       /* 
60.         在onwrite里清除这个标记,onwrite依赖于用户执行,如果用户没调, 
61.         或者不是同步调,则退出,等待执行onwrite的时候再继续写 
62.       */  
63.       if (state.writing) {  
64.         break;  
65.       }  
66.     }  
67.     // 写完了缓存的数据,则更新指针  
68.     if (entry === null)  
69.       state.lastBufferedRequest = null;  
70.   }  
71.   /* 
72.     更新缓存数据链表的头结点指向, 
73.     1 如果是批量写则entry为null 
74.     2 如果单个写,则可能还有值(如果用户是异步调用onwrite的话) 
75.   */  
76.   state.bufferedRequest = entry;  
77.   // 本轮处理完毕(处理完一个或全部)  
78.   state.bufferProcessing = false;  
79. }  

clearBuffer的逻辑看起来非常多,但是逻辑并不算很复杂。主要分为两个分支。
1 用户实现了批量写函数,则一次把缓存的时候写入目的地。首先把缓存的数据(链表)全部收集起来,然后执行执行写入,并设置回调是finish函数。corkedRequestsFree字段指向一个节点数最少为一,最多为二的链表,用于保存批量写的数据的上下文。批量写时的数据结构图如图6和7所示(两种场景)。

corkedRequestsFree保证最少有一个节点,用于一次批量写,当使用完的时候,会最多保存两个空闲节点。我们看一下批量写成功后,回调函数onCorkedFinish的逻辑。

1.  function onCorkedFinish(corkReq, state, err) {  
2.    // corkReq.entry指向当前处理的buffer链表头结点  
3.    var entry = corkReq.entry;  
4.    corkReq.entry = null;  
5.    // 遍历执行用户传入的回调回调  
6.    while (entry) {  
7.      var cb = entry.callback;  
8.      state.pendingcb--;  
9.      cb(err);  
10.     entry = entry.next;  
11.   }  
12.   
13.   // 回收corkReq,state.corkedRequestsFree这时候已经等于新的corkReq,指向刚用完的这个corkReq,共保存两个  
14.   state.corkedRequestsFree.next = corkReq;  
15. }  

onCorkedFinish首先从本次批量写的数据上下文取出回调,然后逐个执行。最后回收节点。corkedRequestsFree总是指向一个空闲节点,所以如果节点超过两个时,每次会把尾节点丢弃,如图8所示。

2 接着我们看单个写的场景
单个写的时候,就是通过doWrite把数据逐个写到目的地,但是有一个地方需要注意的是,如果用户是异步执行可写流的回调onwrite(通过writing字段,因为onwrite会置writing为true,如果执行完doWrite,writing为false说明是异步回调),则写入一个数据后就不再执行doWrite进行写,而是需要等到onwrite回调被异步执行时,再执行下一次写,因为可写流是串行地执行写操作。
下面讲一下sync字段的作用。sync字段是用于标记执行用户自定义的write函数时,write函数是同步还是异步执行可写流的回调onwrite。主要用于控制是同步还是异步执行用户的回调。并且需要保证回调要按照定义的顺序执行。有两个地方涉及了这个逻辑,第一个是wirte的时候。我们看一下函数的调用关系,如图9所示。
在这里插入图片描述
如果用户是同步执行onwrite,则数据会被实时地消费,不存在缓存数据的情况,这时候Node.js异步并且有序地执行用户回调。如果用户连续两次调用了write写入数据,并且是以异步执行回调onwrite,则第一次执行onwrite的时候,会存在缓存的数据,这时候还没来得及执行用户回调,就会先发生第二次写入操作,同样,第二次写操作也是异步回调onwrite,所以接下来就会同步执行的用户回调。这样就保证了用户回调的顺序执行。第二种场景是uncork函数。我们看一下函数关系图,如图10所示。

在uncork的执行流程中,如果onwrite是被同步回调,则在onwrite中不会再次调用clearBuffer,因为这时候的bufferProcessing为true。这时候会先把用户的回调入队,然后再次执行doWrite发起下一次写操作。如果onwrite是被异步执行,在执行clearBuffer中,第一次执行doWrite完毕后,clearBuffer就会退出,并且这时候bufferProcessing为false。等到onwrite被回调的时候,再次执行clearBuffer,同样执行完doWrite的时候退出,等待异步回调,这时候用户回调被执行。
我们继续分析onwrite的代码,onwrite最后会调用afterWrite函数。

1.  function afterWrite(stream, state, finished, cb) {  
2.    // 还没结束,看是否需要触发drain事件  
3.    if (!finished)  
4.      onwriteDrain(stream, state);  
5.    // 准备执行用户回调,待执行的回调减一  
6.    state.pendingcb--;  
7.    cb();  
8.    finishMaybe(stream, state);  
9.  }  
10. 
11. function onwriteDrain(stream, state) {  
12.   // 没有数据需要写了,并且流在阻塞中等待drain事件  
13.   if (state.length === 0 && state.needDrain) {  
14.     // 触发drain事件然后清空标记  
15.     state.needDrain = false;  
16.     stream.emit('drain');  
17.   }  
18. }  
19.

afterWrite主要是判断是否需要触发drain事件,然后执行用户回调。最后判断流是否已经结束(在异步回调onwrite的情况下,用户调用回调之前,可能执行了end)。流结束的逻辑我们后面章节单独分析。

4 cork和uncork

cork和uncork类似tcp中的Negal算法,主要用于累积数据后一次性写入目的地。而不是有一块就实时写入。比如在TCP中,每次发送一个字节,而协议头远远大于一字节,有效数据占比非常低。使用cork的时候最好同时提供writev实现,否则最后cork就没有意义,因为最终还是需要一块块的数据进行写入。我们看看cork的代码。

1.  Writable.prototype.cork = function() {  
2.    var state = this._writableState;  
3.    state.corked++;  
4.  };  

cork的代码非常简单,这里使用一个整数而不是标记位,所以cork和uncork需要配对使用。我们看看uncork。

1.  Writable.prototype.uncork = function() {  
2.    var state = this._writableState;  
3.    
4.    if (state.corked) {  
5.      state.corked--;  
6.      /* 
7.        没有在进行写操作(如果进行写操作则在写操作完成的回调里会执行clearBuffer), 
8.        corked=0, 
9.        没有在处理缓存数据(writing为false已经说明), 
10.       有缓存的数据待处理 
11.     */  
12.     if (!state.writing &&  
13.         !state.corked &&  
14.         !state.bufferProcessing &&  
15.         state.bufferedRequest)  
16.       clearBuffer(this, state);  
17.   }  
18. };  

5 流结束

流结束首先会把当前缓存的数据写入目的地,并且允许再执行额外的一次写操作,然后把可写流置为不可写和结束状态,并且触发一系列事件。下面是结束一个可写流的函数关系图,如图11所示。
在这里插入图片描述
通过end函数可以结束可写流,我们看看该函数的逻辑。

1.  Writable.prototype.end = function(chunk, encoding, cb) {  
2.    var state = this._writableState;  
3.    
4.    if (typeof chunk === 'function') {  
5.      cb = chunk;  
6.      chunk = null;  
7.      encoding = null;  
8.    } else if (typeof encoding === 'function') {  
9.      cb = encoding;  
10.     encoding = null;  
11.   }  
12.   // 最后一次写入的机会,可能直接写入,也可以会被缓存(正在写护着处于corked状态)  
13.   if (chunk !== null && chunk !== undefined)  
14.     this.write(chunk, encoding);  
15.   
16.   // 如果处于corked状态,则上面的写操作会被缓存,uncork和write保存可以对剩余数据执行写操作  
17.   if (state.corked) {  
18.     // 置1,为了uncork能正确执行,可以有机会写入缓存的数据  
19.     state.corked = 1;  
20.     this.uncork();  
21.   }  
22.   
23.   if (!state.ending)  
24.     endWritable(this, state, cb);  
25. };  

我们接着看endWritable函数

1.  function endWritable(stream, state, cb) {  
2.    // 正在执行end函数  
3.    state.ending = true;  
4.    // 判断流是否可以结束了
5.    finishMaybe(stream, state);  
6.    if (cb) {  
7.      // 已经触发了finish事件则下一个tick直接执行cb,否则等待finish事件  
8.      if (state.finished)  
9.        process.nextTick(cb);  
10.     else  
11.       stream.once('finish', cb);  
12.   }  
13.   // 流结束,流不可写  
14.   state.ended = true;  
15.   stream.writable = false;  
16. }  

endWritable函数标记流不可写并且处于结束状态。但是只是代表不能再调用write写数据了,之前缓存的数据需要被写完后才能真正地结束流。我们看finishMaybe函数的逻辑。该函数用于判断流是否可以结束。

1.  function needFinish(state) {  
2.    /* 
3.      执行了end函数则设置ending=true, 
4.      当前没有数据需要写入了, 
5.      也没有缓存的数据, 
6.      还没有触发finish事件, 
7.      没有正在进行写入 
8.    */  
9.    return (state.ending &&  
10.           state.length === 0 &&  
11.           state.bufferedRequest === null &&  
12.           !state.finished &&  
13.           !state.writing);  
14. }  
15.   
16. // 每次写完成的时候也会调用该函数  
17. function finishMaybe(stream, state) {  
18.   // 流是否可以结束  
19.   var need = needFinish(state);  
20.   // 是则先处理prefinish事件,否则先不管,等待写完成再调用该函数  
21.   if (need) {  
22.     prefinish(stream, state);  
23.     // 如果没有待执行的回调,则触发finish事件  
24.     if (state.pendingcb === 0) {  
25.       state.finished = true;  
26.       stream.emit('finish');  
27.     }  
28.   }  
29.   return need;  
30. }  

当可写流中所有数据和回调都执行了才能结束流,在结束流之前会先处理prefinish事件。

1.  function callFinal(stream, state) {  
2.    // 执行用户的final函数  
3.    stream._final((err) => {  
4.      // 执行了callFinal函数,cb减一  
5.      state.pendingcb--;  
6.      if (err) {  
7.        stream.emit('error', err);  
8.      }  
9.      // 执行prefinish  
10.     state.prefinished = true;  
11.     stream.emit('prefinish');  
12.     // 是否可以触发finish事件  
13.     finishMaybe(stream, state);  
14.   });  
15. }  
16. function prefinish(stream, state) {  
17.   // 还没触发prefinish并且没有执行finalcall  
18.   if (!state.prefinished && !state.finalCalled) {  
19.     // 用户传了final函数则,待执行回调数加一,即callFinal,否则直接触发prefinish  
20.     if (typeof stream._final === 'function') {  
21.       state.pendingcb++;  
22.       state.finalCalled = true;  
23.       process.nextTick(callFinal, stream, state);  
24.     } else {  
25.       state.prefinished = true;  
26.       stream.emit('prefinish');  
27.     }  
28.   }  
29. }  

如果用户定义了_final函数,则先执行该函数(这时候会阻止finish事件的触发),执行完后触发prefinish,再触发finish。如果没有定义_final,则直接触发prefinish事件。最后触发finish事件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值