可写流是对数据流向的抽象,用户调用可写流的接口,可写流负责控制数据的写入。流程如图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事件。