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中的其他对象
Jan 16 Javascript
基于JQuery模仿苹果桌面的Dock效果(初级版)
Oct 15 Javascript
JS获取各种浏览器窗口大小的方法
Jan 14 Javascript
javascript实现动态加载CSS
Jan 26 Javascript
js控件Kindeditor实现图片自动上传功能
Jul 20 Javascript
BootStrap实现带关闭按钮功能
Feb 15 Javascript
基于JS实现仿京东搜索栏随滑动透明度渐变效果
Jul 10 Javascript
webpack实现热更新(实施同步刷新)
Jul 28 Javascript
js微信应用场景之微信音乐相册案例分享
Aug 11 Javascript
详解javascript常用工具类的封装
Jan 30 Javascript
Vue.js 使用v-cloak后仍显示变量的解决方法
Nov 19 Javascript
JS中的继承操作实例总结
Jun 06 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
967 个函式
2006/10/09 PHP
Zend的AutoLoad机制介绍
2012/09/27 PHP
与文件上传有关的php配置参数总结
2013/06/14 PHP
php判断输入是否是纯数字,英文,汉字的方法
2015/03/05 PHP
百度工程师讲PHP函数的实现原理及性能分析(三)
2015/05/13 PHP
php基于curl重写file_get_contents函数实例
2016/11/08 PHP
PHP实现的分页类定义与用法示例
2017/07/05 PHP
Yii框架安装简明教程
2020/05/15 PHP
关于图片按比例自适应缩放的js代码
2011/10/30 Javascript
jquery的each方法使用示例分享
2014/03/25 Javascript
jQuery实现级联菜单效果(仿淘宝首页菜单动画)
2014/04/10 Javascript
基于jQuery滑动杆实现购买日期选择效果
2015/09/15 Javascript
超漂亮的Bootstrap 富文本编辑器summernote
2016/04/05 Javascript
使用Object.defineProperty实现简单的js双向绑定
2016/04/15 Javascript
原生js实现日期计算器功能
2017/02/17 Javascript
Angular获取手机验证码实现移动端登录注册功能
2017/05/17 Javascript
JS图片预加载插件详解
2017/06/21 Javascript
Vue.js实现列表清单的操作方法
2017/11/15 Javascript
vue项目打包部署到服务器的方法示例
2018/08/27 Javascript
详解Vue.directive 自定义指令
2019/03/27 Javascript
vue中注册自定义的全局js方法
2019/11/15 Javascript
Python中的super()方法使用简介
2015/08/14 Python
Django框架静态文件使用/中间件/禁用ip功能实例详解
2019/07/22 Python
python匿名函数用法实例分析
2019/08/03 Python
python+pygame实现坦克大战
2019/09/10 Python
win10系统Anaconda和Pycharm的Tensorflow2.0之CPU和GPU版本安装教程
2019/12/03 Python
Python用input输入列表的实例代码
2020/02/07 Python
pandas参数设置的实用小技巧
2020/08/23 Python
NBA欧洲商店(法国):NBA Europe Store FR
2016/10/19 全球购物
中专生自我鉴定
2013/12/17 职场文书
个人党性剖析材料
2014/02/03 职场文书
《谁的本领大》教后反思
2014/04/25 职场文书
学习优秀共产党员先进事迹思想报告
2014/09/17 职场文书
涉及车辆房产分割的离婚协议书范文
2014/10/12 职场文书
某某幼儿园的教育教学管理调研分析报告
2019/11/29 职场文书
Python re.sub 反向引用的实现
2021/07/07 Python