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 相关文章推荐
用javascript父窗口控制只弹出一个子窗口
Apr 10 Javascript
js获取提交的字符串的字节数
Feb 09 Javascript
Jquery 学习笔记(一)
Oct 13 Javascript
用Javascript实现Windows任务管理器的代码
Mar 27 Javascript
jQuery中parents()和parent()的区别分析
Oct 28 Javascript
如何调试异步加载页面里包含的js文件
Oct 30 Javascript
嵌入式iframe子页面与父页面js通信的方法
Jan 20 Javascript
详解JavaScript中基于原型prototype的继承特性
May 05 Javascript
详解Ant Design of React的安装和使用方法
Dec 27 Javascript
vue + typescript + 极验登录验证的实现方法
Jun 27 Javascript
vue使用混入定义全局变量、函数、筛选器的实例代码
Jul 29 Javascript
微信小程序 函数防抖 解决重复点击消耗性能问题实现代码
Sep 12 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 Undefined index和Undefined variable的解决方法
2008/03/27 PHP
PHP程序员最常犯的11个MySQL错误小结
2010/11/20 PHP
php利用iframe实现无刷新文件上传功能的代码
2011/09/29 PHP
通过缓存数据库结果提高PHP性能的原理介绍
2012/09/05 PHP
PHP实现提取一个图像文件并在浏览器上显示的代码
2012/10/06 PHP
5种PHP创建数组的实例代码分享
2014/01/17 PHP
Yii2 加载css、js 载静态资源的方法
2017/03/10 PHP
Linux下快速搭建php开发环境
2017/03/13 PHP
PHP编程实现csv文件导入mysql数据库的方法
2017/04/29 PHP
PHP时间戳和日期相互转换操作实例小结
2018/12/18 PHP
CSS中一些@规则的用法小结
2021/03/09 HTML / CSS
jQuery实现的类flash菜单效果代码
2010/05/17 Javascript
js通过八个点 拖动改变div大小的实现方法
2014/03/05 Javascript
Area 区域实现post提交数据的js写法
2014/04/22 Javascript
javascript写的异步加载js文件函数(支持数组传参)
2014/06/07 Javascript
js添加select下默认的option的value和text的方法
2014/10/19 Javascript
浅谈JavaScript数据类型
2015/03/03 Javascript
js创建对象的方法汇总
2016/01/07 Javascript
js+html5实现canvas绘制网页时钟的方法
2016/05/21 Javascript
jQuery获取剪贴板内容的方法
2016/06/16 Javascript
JS实现iframe自适应高度的方法示例
2017/01/07 Javascript
ES6中的箭头函数实例详解
2017/04/06 Javascript
jquery动态添加带有样式的HTML标签元素方法
2018/02/24 jQuery
微信小程序上传图片功能(附后端代码)
2020/06/19 Javascript
解决JavaScript layui 下拉框不显示的问题
2018/08/14 Javascript
vue微信分享到朋友圈 vue微信发送给好友
2018/11/28 Javascript
[13:38]2015国际邀请赛中国战队出征仪式
2015/05/29 DOTA
Python加pyGame实现的简单拼图游戏实例
2015/05/15 Python
python脚本内运行linux命令的方法
2015/07/02 Python
用Python实现大文本文件切割的方法
2019/01/12 Python
Python设计模式之备忘录模式原理与用法详解
2019/01/15 Python
python集合常见运算案例解析
2019/10/17 Python
PyCharm下载和安装详细步骤
2019/12/17 Python
Seavenger官网:潜水服、浮潜、靴子和袜子
2020/03/05 全球购物
求职信的最佳写作思路
2014/02/01 职场文书
学习党章的体会
2014/11/07 职场文书