Node.js pipe实现源码解析


Posted in Javascript onAugust 12, 2017

从前面两篇文章,我们了解到。想要把 Readable 的数据写到 Writable,就必须先手动的将数据读入内存,然后写入 Writable。换句话说,每次传递数据时,都需要写如下的模板代码

readable.on('readable', (err) => {
 if(err) throw err

 writable.write(readable.read())
})

为了方便使用,Node.js 提供了 pipe() 方法,让我们可以优雅的传递数据

readable.pipe(writable)

现在,就让我们来看看它是如何实现的吧

pipe

首先需要先调用 Readable 的 pipe() 方法

// lib/_stream_readable.js

Readable.prototype.pipe = function(dest, pipeOpts) {
 var src = this;
 var state = this._readableState;

 // 记录 Writable
 switch (state.pipesCount) {
  case 0:
   state.pipes = dest;
   break;
  case 1:
   state.pipes = [state.pipes, dest];
   break;
  default:
   state.pipes.push(dest);
   break;
 }
 state.pipesCount += 1;

 // ...

  src.once('end', endFn);

 dest.on('unpipe', onunpipe);
 
 // ...

 dest.on('drain', ondrain);

 // ...

 src.on('data', ondata);

 // ...

 // 保证 error 事件触发时,onerror 首先被执行
 prependListener(dest, 'error', onerror);

 // ...

 dest.once('close', onclose);
 
 // ...

 dest.once('finish', onfinish);

 // ...

 // 触发 Writable 的 pipe 事件
 dest.emit('pipe', src);

 // 将 Readable 改为 flow 模式
 if (!state.flowing) {
  debug('pipe resume');
  src.resume();
 }

 return dest;
};

执行 pipe() 函数时,首先将 Writable 记录到 state.pipes 中,然后绑定相关事件,最后如果 Readable 不是 flow 模式,就调用 resume() 将 Readable 改为 flow 模式

传递数据

Readable 从数据源获取到数据后,触发 data 事件,执行 ondata()

ondata() 相关代码:

// lib/_stream_readable.js

 // 防止在 dest.write(chunk) 内调用 src.push(chunk) 造成 awaitDrain 重复增加,awaitDrain 不能清零,Readable 卡住的情况
 // 详情见 https://github.com/nodejs/node/issues/7278
 var increasedAwaitDrain = false;
 function ondata(chunk) {
  debug('ondata');
  increasedAwaitDrain = false;
  var ret = dest.write(chunk);
  if (false === ret && !increasedAwaitDrain) {
   // 防止在 dest.write() 内调用 src.unpipe(dest),导致 awaitDrain 不能清零,Readable 卡住的情况
   if (((state.pipesCount === 1 && state.pipes === dest) ||
      (state.pipesCount > 1 && state.pipes.indexOf(dest) !== -1)
     ) && 
     !cleanedUp) {
    debug('false write response, pause', src._readableState.awaitDrain);
    src._readableState.awaitDrain++;
    increasedAwaitDrain = true;
   }
   // 进入 pause 模式
   src.pause();
  }
 }

在 ondata(chunk) 函数内,通过 dest.write(chunk) 将数据写入 Writable

此时,在 _write() 内部可能会调用 src.push(chunk) 或使其 unpipe,这会导致 awaitDrain 多次增加,不能清零,Readable 卡住

当不能再向 Writable 写入数据时,Readable 会进入 pause 模式,直到所有的 drain 事件触发

触发 drain 事件,执行 ondrain()

// lib/_stream_readable.js

 var ondrain = pipeOnDrain(src);

 function pipeOnDrain(src) {
  return function() {
   var state = src._readableState;
   debug('pipeOnDrain', state.awaitDrain);
   if (state.awaitDrain)
    state.awaitDrain--;
   // awaitDrain === 0,且有 data 监听器
   if (state.awaitDrain === 0 && EE.listenerCount(src, 'data')) {
    state.flowing = true;
    flow(src);
   }
  };
 }

每个 drain 事件触发时,都会减少 awaitDrain,直到 awaitDrain 为 0。此时,调用 flow(src),使 Readable 进入 flow 模式

到这里,整个数据传递循环已经建立,数据会顺着循环源源不断的流入 Writable,直到所有数据写入完成

unpipe

不管写入过程中是否出现错误,最后都会执行 unpipe()

// lib/_stream_readable.js

// ...

 function unpipe() {
  debug('unpipe');
  src.unpipe(dest);
 }

// ...

Readable.prototype.unpipe = function(dest) {
 var state = this._readableState;
 var unpipeInfo = { hasUnpiped: false };

 // 啥也没有
 if (state.pipesCount === 0)
  return this;

 // 只有一个
 if (state.pipesCount === 1) {
  if (dest && dest !== state.pipes)
   return this;
  // 没有指定就 unpipe 所有
  if (!dest)
   dest = state.pipes;

  state.pipes = null;
  state.pipesCount = 0;
  state.flowing = false;
  if (dest)
   dest.emit('unpipe', this, unpipeInfo);
  return this;
 }

 // 没有指定就 unpipe 所有
 if (!dest) {
  var dests = state.pipes;
  var len = state.pipesCount;
  state.pipes = null;
  state.pipesCount = 0;
  state.flowing = false;

  for (var i = 0; i < len; i++)
   dests[i].emit('unpipe', this, unpipeInfo);
  return this;
 }

 // 找到指定 Writable,并 unpipe
 var index = state.pipes.indexOf(dest);
 if (index === -1)
  return this;

 state.pipes.splice(index, 1);
 state.pipesCount -= 1;
 if (state.pipesCount === 1)
  state.pipes = state.pipes[0];

 dest.emit('unpipe', this, unpipeInfo);

 return this;
};

Readable.prototype.unpipe() 函数会根据 state.pipes 属性和 dest 参数,选择执行策略。最后会触发 dest 的 unpipe 事件

unpipe 事件触发后,调用 onunpipe(),清理相关数据

// lib/_stream_readable.js

 function onunpipe(readable, unpipeInfo) {
  debug('onunpipe');
  if (readable === src) {
   if (unpipeInfo && unpipeInfo.hasUnpiped === false) {
    unpipeInfo.hasUnpiped = true;
    // 清理相关数据
    cleanup();
   }
  }
 }

End

在整个 pipe 的过程中,Readable 是主动方 ( 负责整个 pipe 过程:包括数据传递、unpipe 与异常处理 ),Writable 是被动方 ( 只需要触发 drain 事件 )

总结一下 pipe 的过程:

  • 首先执行 readbable.pipe(writable),将 readable 与 writable 对接上
  • 当 readable 中有数据时,readable.emit('data'),将数据写入 writable
  • 如果 writable.write(chunk) 返回 false,则进入 pause 模式,等待 drain 事件触发
  • drain 事件全部触发后,再次进入 flow 模式,写入数据
  • 不管数据写入完成或发生中断,最后都会调用 unpipe()
  • unpipe() 调用 Readable.prototype.unpipe(),触发 dest 的 unpipe 事件,清理相关数据

参考:

https://github.com/nodejs/node/blob/master/lib/_stream_readable.js

https://github.com/nodejs/node/blob/master/lib/_stream_writable.js

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Jquery 组合form元素为json格式,asp.net反序列化
Jul 09 Javascript
从URL中提取参数与将对象转换为URL查询参数的实现代码
Jan 12 Javascript
js Date概念详细介绍
Nov 22 Javascript
JS字符串拼接在ie中都报错的解决方法
Mar 27 Javascript
JavaScript函数的4种调用方法详解
Apr 22 Javascript
JS兼容浏览器的导出Excel(CSV)文件的方法
May 03 Javascript
JQuery中节点遍历方法实例
May 18 Javascript
总结javascript中的六种迭代器
Aug 16 Javascript
利用Chrome DevTools直接调试Node.js和JavaScript的方法详解(并行)
Feb 16 Javascript
jQuery EasyUI ProgressBar进度条组件
Feb 28 Javascript
p5.js入门教程之键盘交互
Mar 19 Javascript
详解微信小程序实现跑马灯效果(附完整代码)
Apr 29 Javascript
vue实现移动端图片裁剪上传功能
Aug 18 #Javascript
JavaScript实现图片切换效果
Aug 12 #Javascript
vue表单绑定实现多选框和下拉列表的实例
Aug 12 #Javascript
浅谈angularJS的$watch失效问题的解决方案
Aug 11 #Javascript
EasyUI在Panel上动态添加LinkButton按钮
Aug 11 #Javascript
Javascript(es2016) import和require用法和区别详解
Aug 11 #Javascript
详解Node中导入模块require和import的区别
Aug 11 #Javascript
You might like
PHP把JPEG图片转换成Progressive JPEG的方法
2014/06/30 PHP
php+jQuery.uploadify实现文件上传教程
2014/12/26 PHP
laravel csrf排除路由,禁止,关闭指定路由的例子
2019/10/21 PHP
改变javascript函数内部this指针指向的三种方法
2010/04/23 Javascript
jquery json 实例代码
2010/12/02 Javascript
Javascript异步编程的4种方法让你写出更出色的程序
2013/01/17 Javascript
关于JavaScript对象的动态选择及遍历对象
2014/03/10 Javascript
全面解析Bootstrap中form、navbar的使用方法
2016/05/30 Javascript
Bootstrap3 内联单选和多选框
2016/12/29 Javascript
解决URL地址中的中文乱码问题的办法
2017/02/10 Javascript
微信小程序 template模板详解及实例
2017/02/21 Javascript
JS常见算法详解
2017/02/28 Javascript
利用js查找数组中指定元素并返回该元素的所有索引示例
2017/03/29 Javascript
node.js调用Chrome浏览器打开链接地址的方法
2017/05/17 Javascript
VUE 使用中踩过的坑
2018/02/08 Javascript
一次记住JavaScript的6个正则表达式方法
2018/02/22 Javascript
Angular 组件之间的交互的示例代码
2018/03/24 Javascript
微信小程序实现聊天对话(文本、图片)功能
2018/07/06 Javascript
Nodejs中的JWT和Session的使用
2018/08/21 NodeJs
详解jenkins自动化部署vue
2019/05/14 Javascript
运用js实现图层拖拽的功能
2019/05/24 Javascript
js设计模式之单例模式原理与用法详解
2019/08/15 Javascript
vue动态绘制四分之三圆环图效果
2019/09/03 Javascript
layer弹出层取消遮罩的方法
2019/09/25 Javascript
小程序双头slider选择器的实现示例
2020/03/31 Javascript
python自动安装pip
2014/04/24 Python
尝试使用Python多线程抓取代理服务器IP地址的示例
2015/11/09 Python
Python跨文件全局变量的实现方法示例
2017/12/10 Python
python实现一个简单的并查集的示例代码
2018/03/19 Python
一篇文章搞定Python操作文件与目录
2019/08/13 Python
python函数中将变量名转换成字符串实例
2020/05/11 Python
公认8个效率最高的爬虫框架
2020/07/28 Python
大学生开西餐厅创业计划书
2014/02/01 职场文书
节能标语大全
2014/06/21 职场文书
预备党员群众意见
2015/06/01 职场文书
Go 内联优化让程序员爱不释手
2022/06/21 Golang