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 相关文章推荐
单击浏览器右上角的X关闭窗口弹出提示的小例子
Jun 12 Javascript
让jQuery与其他JavaScript库并存避免冲突的方法
Dec 23 Javascript
详解JavaScript中undefined与null的区别
Mar 29 Javascript
jQuery插件Skippr实现焦点图幻灯片特效
Apr 12 Javascript
BootStrap轮播HTML代码(推荐)
Dec 10 Javascript
angular+bootstrap的双向数据绑定实例
Mar 03 Javascript
微信小程序非跳转式组件授权登录的方法示例
May 22 Javascript
JS插件amCharts实现绘制柱形图默认显示数值功能示例
Nov 26 Javascript
JS实现图片切换特效
Dec 23 Javascript
JavaScript canvas绘制渐变颜色的矩形
Feb 18 Javascript
JS XMLHttpRequest原理与使用方法深入详解
Apr 30 Javascript
JS hasOwnProperty()方法检测一个属性是否是对象的自有属性的方法
Jan 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常用函数 推荐收藏保存
2010/02/21 PHP
PHP错误提示的关闭方法详解
2013/06/23 PHP
PHP实现动态执行代码的方法
2016/03/25 PHP
简单实现php上传文件功能
2017/09/21 PHP
yii框架redis结合php实现秒杀效果(实例代码)
2017/10/26 PHP
jQuery 瀑布流 浮动布局(一)(延迟AJAX加载图片)
2012/05/23 Javascript
六款帮助你实现惊艳视差滚动效果的jQuery插件
2012/09/14 Javascript
js判断样式className同时增加class或删除class
2013/01/30 Javascript
删除select中所有option选项jquery代码
2013/08/12 Javascript
jQuery插件Skippr实现焦点图幻灯片特效
2015/04/12 Javascript
JS函数多个参数默认值指定方法分析
2016/11/28 Javascript
angular十大常见问题
2017/03/07 Javascript
vue按需加载组件webpack require.ensure的方法
2017/12/13 Javascript
创建echart多个联动的示例代码
2018/11/23 Javascript
微信小程序实现的绘制table表格功能示例
2019/04/26 Javascript
JS实现处理时间,年月日,星期的公共方法示例
2019/05/31 Javascript
Nuxt.js实战和配置详解
2019/08/05 Javascript
使用python搭建Django应用程序步骤及版本冲突问题解决
2013/11/19 Python
python遍历文件夹下所有excel文件
2018/01/03 Python
python与sqlite3实现解密chrome cookie实例代码
2018/01/20 Python
Python查找第n个子串的技巧分享
2018/06/27 Python
python+numpy实现的基本矩阵操作示例
2019/07/19 Python
Python使用Slider组件实现调整曲线参数功能示例
2019/09/06 Python
python super的使用方法及实例详解
2019/09/25 Python
python使用 __init__初始化操作简单示例
2019/09/26 Python
PyTorch安装与基本使用详解
2020/08/31 Python
html5中为audio标签增加停止按钮动作实现方法
2013/01/04 HTML / CSS
特罗佩亚包官方网站:Tropea
2017/01/03 全球购物
外贸业务员工作职责
2014/01/06 职场文书
《山谷中的谜底》教学反思
2014/04/26 职场文书
办公室个人总结
2015/02/28 职场文书
2015年全国助残日活动方案
2015/05/04 职场文书
推销搭讪开场白
2015/05/28 职场文书
员工聘用合同范本
2015/09/21 职场文书
60条职场经典语录,总有一条能触动你的心
2019/08/21 职场文书
js中Map和Set的用法及区别实例详解
2022/02/15 Javascript