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 Validation插件remote验证方式的Bug解决
Jul 01 Javascript
jQuery控制TR显示隐藏的三种常用方法
Aug 21 Javascript
JS 获取鼠标左右键的键值方法
Oct 11 Javascript
Javascript中的高阶函数介绍
Mar 15 Javascript
纯js模拟div层弹性运动的方法
Jul 27 Javascript
微信小程序 flex实现导航实例详解
Apr 26 Javascript
Windows下Node.js安装及环境配置方法
Sep 18 Javascript
jquery之基本选择器practice(实例讲解)
Sep 30 jQuery
JavaScript实现AOP详解(面向切面编程,装饰者模式)
Dec 19 Javascript
Vue数据双向绑定原理及简单实现方法
May 18 Javascript
JavaScript 扩展运算符用法实例小结【基于ES6】
Jun 17 Javascript
layui表格分页 记录勾选的实例
Sep 02 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 Image Resize图片大小调整的函数代码
2011/01/17 PHP
PHP curl 获取响应的状态码的方法
2014/01/13 PHP
PHP调用.NET的WebService 简单实例
2015/03/27 PHP
php面向对象基础详解【星际争霸游戏案例】
2020/01/23 PHP
使用TextRange获取输入框中光标的位置的代码
2007/03/08 Javascript
JS打开图片另存为对话框实现代码
2012/12/26 Javascript
form.submit()不能提交表单的错误原因及解决方法
2014/10/13 Javascript
火狐和ie下获取javascript 获取event的方法(推荐)
2016/11/26 Javascript
微信小程序 二维码canvas绘制实例详解
2017/01/06 Javascript
Bootstrap导航中表单简单实现代码
2017/03/06 Javascript
JavaScript学习总结之正则的元字符和一些简单的应用
2017/06/30 Javascript
Vue源码解析之Template转化为AST的实现方法
2018/12/14 Javascript
微信接入之获取用户头像的方法步骤
2019/09/23 Javascript
小程序使用wxs解决wxml保留2位小数问题
2019/12/13 Javascript
JavaScript变量Dom对象的所有属性
2020/04/30 Javascript
vue3弹出层V3Popup实例详解
2021/01/04 Vue.js
Python的Flask框架中集成CKeditor富文本编辑器的教程
2016/06/13 Python
python 循环遍历字典元素的简单方法
2016/09/11 Python
Python基于回溯法子集树模板解决找零问题示例
2017/09/11 Python
利用Python读取txt文档的方法讲解
2018/06/23 Python
python实现顺序表的简单代码
2018/09/28 Python
Pyinstaller 打包exe教程及问题解决
2019/08/16 Python
pytorch如何冻结某层参数的实现
2020/01/10 Python
设置jupyter中DataFrame的显示限制方式
2020/04/12 Python
Python爬虫分析微博热搜关键词的实现代码
2021/02/22 Python
浏览器实现移动端高性能css3动画(开启gpu加速)
2013/12/23 HTML / CSS
amazeui页面校验功能的实现代码
2020/08/24 HTML / CSS
德国机场停车位比较和预订网站:Ich-parke-billiger
2018/01/08 全球购物
全球地下的服装和态度:Slam Jam
2018/02/04 全球购物
定制别致的瑜伽垫:Sugarmat
2019/06/21 全球购物
SQL语言面试题
2013/08/27 面试题
《大作家的小老师》教学反思
2014/04/16 职场文书
项目建议书模板
2014/05/12 职场文书
承诺书模板大全
2015/05/04 职场文书
JavaScript高级程序设计之基本引用类型
2021/11/17 Javascript
Python中的协程(Coroutine)操作模块(greenlet、gevent)
2022/05/30 Python