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 相关文章推荐
URI、URL和URN之间的区别与联系
Dec 20 Javascript
一起来写段JS drag拖动代码
Dec 09 Javascript
js实现同一页面可多次调用的图片幻灯切换效果
Feb 28 Javascript
JavaScript必看小技巧(必看)
Jun 07 Javascript
JavaScript SHA512加密算法详细代码
Oct 06 Javascript
基于JS实现弹出一个隐藏的div窗口body页面变成灰色并且不可被编辑
Dec 14 Javascript
js实现无缝滚动图(可控制当前滚动的方向)
Feb 22 Javascript
EasyUI的TreeGrid的过滤功能的解决思路
Aug 08 Javascript
详解vue2.0 不同屏幕适配及px与rem转换问题
Feb 23 Javascript
Vue安装浏览器开发工具的步骤详解
May 12 Javascript
微信小程序制作扭蛋机代码实例
Sep 24 Javascript
Vue js with语句原理及用法解析
Sep 03 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实现的返回数据格式化类实例
2014/09/22 PHP
使用PHP开发留言板功能
2019/11/19 PHP
javascript 类型判断代码分析
2010/03/28 Javascript
用nodejs写的一个简单项目打包工具
2013/05/11 NodeJs
nodejs 提示‘xxx’ 不是内部或外部命令解决方法
2014/11/20 NodeJs
JavaScript直播评论发弹幕切图功能点集合效果代码
2016/06/26 Javascript
js智能获取浏览器版本UA信息的方法
2016/08/08 Javascript
jQuery实现鼠标跟随效果
2017/02/20 Javascript
使用yeoman构建angular应用的方法
2017/08/14 Javascript
vue中实现在外部调用methods的方法(推荐)
2018/02/08 Javascript
jquery拖拽自动排序插件使用方法详解
2020/07/20 jQuery
Element实现表格分页数据选择+全选所有完善批量操作
2019/06/07 Javascript
详解ECMAScript2019/ES10新属性
2019/12/06 Javascript
python解决字典中的值是列表问题的方法
2013/03/04 Python
python调用短信猫控件实现发短信功能实例
2014/07/04 Python
Python 数据结构之堆栈实例代码
2017/01/22 Python
python将秒数转化为时间格式的实例
2018/09/16 Python
解决Python下json.loads()中文字符出错的问题
2018/12/19 Python
Python3.5模块的定义、导入、优化操作图文详解
2019/04/27 Python
代码实例讲解python3的编码问题
2019/07/08 Python
Python 进程之间共享数据(全局变量)的方法
2019/07/16 Python
Python Django简单实现session登录注销过程详解
2019/08/06 Python
基于pytorch的lstm参数使用详解
2020/01/14 Python
Python3 中sorted() 函数的用法
2020/03/24 Python
Python的Tqdm模块实现进度条配置
2021/02/24 Python
HTML5 新事件 小结
2009/07/16 HTML / CSS
意大利制造的男鞋和女鞋:SCAROSSO
2018/03/07 全球购物
英国太阳镜品牌:Taylor Morris Eyewear
2018/04/18 全球购物
巴西手表购物网站:eclock
2019/03/19 全球购物
RUIFIER官网:英国奢侈高级珠宝品牌
2020/06/12 全球购物
实习生体会的自我评价范文
2013/11/28 职场文书
工作表现评语
2014/01/19 职场文书
学生保证书范文
2014/04/28 职场文书
大学生个人学习总结
2015/02/15 职场文书
解决Python中的modf()函数取小数部分不准确问题
2021/05/28 Python
python代码实现扫码关注公众号登录的实战
2021/11/01 Python