Node.js Stream ondata触发时机与顺序的探索


Posted in Javascript onMarch 08, 2019

上次写Stream pipe细节时,在源码中发现一段无用逻辑,由此引发了对Stream data事件触发时机与顺序的探索。

无用逻辑

当时研究pipe细节是基于Node.js v8.11.1的源码,其中针对上游的ondata事件处理有如下一段代码:

// If the user pushes more data while we're writing to dest then we'll end up
// in ondata again. However, we only want to increase awaitDrain once because
// dest will only emit one 'drain' event for the multiple writes.
// => Introduce a guard on increasing awaitDrain.
var increasedAwaitDrain = false;
src.on('data', ondata);
function ondata(chunk) {
  debug('ondata');
  increasedAwaitDrain = false;
  var ret = dest.write(chunk);
  if (false === ret && !increasedAwaitDrain) {
    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;
    }
    src.pause();
  }
}

重点关注increasedAwaitDrain变量,理解这个变量期望达到什么目的,然后仔细阅读代码,会发现if (false === ret && !increasedAwaitDrain)语句中increasedAwaitDrain变量肯定是false,因为前一行才将该变量赋值为false,这样一来这个变量就变得毫无意义。

increasedAwaitDrain = false; 
var ret = dest.write(chunk); 
if (false === ret && !increasedAwaitDrain) {}

以上就是关键的三行代码,因为Node.js是单线程且dest.write(chunk)内部没有修改变量increasedAwaitDrain的值,那么if语句中increasedAwaitDrain的值肯定还是false,即increasedAwaitDrain相关逻辑没有达到所期望的目标。

无用代码出现的原因

前段虽已经分析出increasedAwaitDrain没起到作用,但作者为什么写了这样一段逻辑呢?其实在定义increasedAwaitDrain语句的上方,作者说可能存在这样一种情况:“当我们接收到一次上游的ondata事件并尝试将数据写到下游时,上游可能同时又有一个data事件触发,而这两个ondata的数据在写入下游时可能都返回false,从而导致src._readableState.awaitDrain++执行两次”。

awaitDrain++执行两次是作者不希望看到的情况,因为下游触发drain事件时awaitDrain相应减1,直到其值为0时才让上游重新流动,如果awaitDrain++执行两次,下游却只触发一次drain事件,awaitDrain就不会为0,上游不重新流动也就无法继续读取数据。

真相的探索过程

虽然从理性上认为increasedAwaitDrain没起到作用,但也无法肯定加绝对,自己尝试去求助,没有出现高手指点出问题所在,但一个同事听我描述后,说可能这就是个BUG,虽心中觉得可能性不大,但还是抱着试试看的心态切换到master分支上去瞅瞅,随即发现最新的代码里并没有与increasedAwaitDrain类似的逻辑,间接说明v8.11.1分支上increasedAwaitDrain相关逻辑的确无用。

虽然比较肯定这里存在一段无用代码,但应该如何理解作者在increasedAwaitDrain上方的注释呢?为了进一步揭露真相,自己继续花时间去看了看stream.Readable相关代码,想知道data事件的触发时机与顺序是如何决定的。

readable流的简单原理

在进一步解释data事件的触发顺序前,简单讲一下readable流的实现原理,如果需要自己实现一个readable流,可以使用new stream.Readable(options)方法,其中options可包含四个属性:highWaterMark、encoding、objectMode、read。最主要的是read属性,当流的使用者需要数据时,read方法被用来从数据源获取数据,然后通过this.push(chunk)将数据传递给使用者,如果没有更多数据可供读取时使用this.push(null)表示读取结束。

const Readable = require('stream').Readable;
let letter = 'ABCDEFG'.split('');
let index = 0;
const rs = new Readable({
  read(size) {
    this.push(letter[index++] || null);
  }
});
rs.on('data', chunk => {
  console.log(chunk.toString());
});
// 输出
// A
// B
// C
// ...

这里ondata虽然没有明显调用read方法,但内部依旧是通过调用read方法结合this.push输出数据,并且在源代码内部可以发现通过参数传递的read方法实际上被赋值给this._read,然后在Readable.prototype.read中调用this._read获取数据。

灵魂代码

为了进一步说明stream.Readable的data事件触发顺序与场景,将有关官方源码经过修改和删减成如下:

function Readable(options) {
  this._read = options.read; // 将参数传递的read函数赋值到this._read
}
// 使用者通过调用read方法获取数据
Readable.prototype.read = function (size) {
  var state = this._readableState;
  // 模拟锁,一次_read如果没有返回(this.push),后续read不会继续调用_read读取数据
  if (!state.reading) {
    state.reading = true;
    state.sync = true; // sync用于在push方法中指示_read内部是否同步调用了push
    this._read(size);
    state.sync = false;    
  }
  // _read内部如果是同步调用push,数据会放入缓冲区
  // _read内部如果是异步调用push且缓冲区没有内容,数据可能emit data返回
  // 尝试从缓冲区(state.buffer)中获取大小为size的数据,如果获取成功则触发data事件
  if (ret) 
    this.emit('data', ret);
  return ret;
};
// 在this._read执行过程中通过this.push输出数据
Readable.prototype.push = function (chunk, encoding) {
  var state = this._readableState;
  // 本次_read获取到数据,打开锁
  state.reading = false;
  // 流动模式 & 缓冲区没有数据 & 非同步返回,则直接触发data事件
  if (state.flowing && state.length === 0 && !state.sync) {
    stream.emit('data', chunk);
    stream.read(0); // 触发下一次读取,_read异步push的话还是会到这里,类似flow中的保持流出于流动
  }
  else {
    // 将数据放入缓冲区
    state.length += chunk.length;
    state.buffer.push(chunk);
  }
};
// 暂停流动
Readable.prototype.pause = function() {
  if (this._readableState.flowing !== false) {
    this._readableState.flowing = false;
    this.emit('pause');
  }
  return this;
};
function flow(stream) {
  const state = stream._readableState;
  while (state.flowing && stream.read() !== null);
}

data事件的触发时机与顺序

时机

data的触发只有两处:

  • 流如果处于流动模式 & 缓冲区没有数据 & 异步调用push,此时数据不经过缓冲区,直接触发data事件
  • 不满足上述情况时,push的数据会被放入缓冲区,然后再尝试从缓冲区读取指定size的数据并触发data事件

顺序

关于data的触发顺序,实际是由emit顺序决定,为讨论原始问题:“increasedAwaitDrain相关逻辑为什么可以被删除?”,将代码简化:

let count = 0;
src.on('data', chunk => {
  let ret = dest.write(chunk);
  if (!ret) {
    count++;
    src.pause();
  }
});

当监听流的data事件时,流最终会通过resume并调用flow函数进入流动模式模式,即不断的调用read方法读取数据。接下来分析以下几种场景,当dest.write(chunk)返回false时++count会执行几次,注意结合前文的灵魂代码。

  • 场景一:每次_read同步push一次数据

当发生第一次读取,数据同步push到缓冲区,紧接着从缓冲区中读取数据并通过emit data的方式传递到ondata中,如果此时dest.write(chunk)返回false,count++将执行一次,接着由于调用了stream.pause(),while条件state.flowing为false导致stream.read不再被调用,在流重新流动前,count的值不会继续增加。

  • 场景二:每次_read异步push一次数据

当发生第一次读取,异步push的数据将直接通过emit data传递到ondata中,而read函数中的emit由于无法从缓冲区读取数据从而不会触发,同时read返回null导致while循环也相应停止,此种情况下异步push触发data事件后,紧接着的stream.read(0)会继续保持流的流动,当dest.write(chunk)返回false,count++执行一次并将流暂停,紧接着会继续调用一次read,但这次数据将被放入缓冲区且不触发data事件,count++依旧只执行一次。

场景二流暂停一次后再次流动时,数据消耗模式与之前会有所差异,会优先消耗缓冲区数据直至为空时回到之前的模式,但这同样不会导致count++执行多次。

  • 场景三:每次_read多次同步push数据

与场景一类似,只是每次_read会多次往缓冲区写入数据,最终data事件还是依靠从缓冲区读数据后触发。

  • 场景四:每次_read多次异步push数据

同场景二类似,假设在一次_read中有两次异步push,当第一个异步push执行时,data事件触发且其中的dest.write(chunk)返回false,导致count++同时流被暂停,等第二个异步push执行时,由于流已经暂停,数据将写入缓冲区而不是触发data事件,所以count++只执行一次。

  • 场景五:_read操作可能同步或异步push

不管是同步或者异步push,当一次ondata内部将流设置为暂停模式后,flow函数中while条件state.flowing为false将导致stream.read不再调用,异步的push的emit data判断条件同样不再满足,即目前阶段内部不会再有data事件触发直到外部再次间接或直接调用read方法。

以上五个场景是为了分析该问题而模拟的,实际只要能理解第五个场景就能明白所有。

小结

文章最终写出来的内容与我最开始的初衷所偏离,而且自己不知道如何评价这篇文章的好坏,但为了写这文章花了两天业余时间去深入理解stream.Readable却是非常有收获的一件事情,更坚定自己在写文章的路途上可以走的更远。

PS:猜测为什么有烂电影的存在,可能是因为导演长时间投入的创作会让他迷失在内部而无法发现问题,写文章也是,难以通过阅读去优化费心思写的文章。

PS:下图是美团博客的,也许我写了这么多却抵不上这张图,说明方式很重要。

Node.js Stream ondata触发时机与顺序的探索

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。如果你想了解更多相关内容请查看下面相关链接

Javascript 相关文章推荐
jquery tablesorter.js 支持中文表格排序改进
Dec 09 Javascript
用于deeplink的js方法(判断手机是否安装app)
Apr 02 Javascript
JS计算网页停留时间代码
Apr 28 Javascript
浅谈javascript语法和定时函数
May 03 Javascript
用原生JS对AJAX做简单封装的实例代码
Jul 13 Javascript
KnockoutJS 3.X API 第四章之表单textInput、hasFocus、checked绑定
Oct 11 Javascript
深入浅析Vue组件开发
Nov 25 Javascript
理解AngularJs篇:30分钟快速掌握AngularJs
Dec 23 Javascript
jquery实现图片平滑滚动详解
Mar 22 jQuery
AngularJS表单验证功能分析
May 26 Javascript
Node.js爬取豆瓣数据实例分析
Mar 05 Javascript
React中使用Vditor自定义图片详解
Dec 25 Javascript
详解JSON和JSONP劫持以及解决方法
Mar 08 #Javascript
Node.js Event Loop各阶段讲解
Mar 08 #Javascript
vue基础之data存储数据及v-for循环用法示例
Mar 08 #Javascript
vue.js使用v-model实现表单元素(input) 双向数据绑定功能示例
Mar 08 #Javascript
JavaScript解析机制与闭包原理实例详解
Mar 08 #Javascript
零基础之Node.js搭建API服务器的详解
Mar 08 #Javascript
详解vue项目中使用token的身份验证的简单实践
Mar 08 #Javascript
You might like
用PHP制作静态网站的模板框架(三)
2006/10/09 PHP
PHP4中session登录页面的应用
2008/07/25 PHP
8个出色的WordPress SEO插件收集
2011/02/26 PHP
PHP Hash算法:Times33算法代码实例
2015/05/13 PHP
分享10段PHP常用代码
2015/11/11 PHP
Extjs ajax同步请求时post方式参数发送方式
2009/08/05 Javascript
jquery获取自定义属性(attr和prop)实例介绍
2013/04/21 Javascript
Jquery解析json数据详解
2013/12/26 Javascript
JavaScript匿名函数与委托使用示例
2014/07/22 Javascript
node.js中的console.time方法使用说明
2014/12/09 Javascript
JS简单实现动画弹出层效果
2015/05/05 Javascript
js实现3D图片环展示效果
2017/03/09 Javascript
详解Vue 中 extend 、component 、mixins 、extends 的区别
2017/12/20 Javascript
vue的mixins属性详解
2018/03/14 Javascript
Vux+Axios拦截器增加loading的问题及实现方法
2018/11/08 Javascript
[02:25]专访DOTA2负责人Erik 国际邀请赛暂不会离开西雅
2014/07/21 DOTA
win7 下搭建sublime的python开发环境的配置方法
2014/06/18 Python
基于Linux系统中python matplotlib画图的中文显示问题的解决方法
2017/06/15 Python
基于pandas将类别属性转化为数值属性的方法
2018/07/25 Python
python中正则表达式 re.findall 用法
2018/10/23 Python
浅谈python 导入模块和解决文件句柄找不到问题
2018/12/15 Python
Python根据当前日期取去年同星期日期
2019/04/14 Python
python打开文件的方式有哪些
2020/06/29 Python
欧洲最大的笔和书写专家:The Pen Shop
2017/03/19 全球购物
Allsole美国/加拿大:英国一家专门出售品牌鞋子的网站
2018/10/21 全球购物
彪马荷兰官网:PUMA荷兰
2019/05/08 全球购物
Unineed旗下时尚轻奢网站:FABHunt
2019/05/13 全球购物
音乐之声音乐广播稿
2014/09/10 职场文书
四风问题查摆剖析材料
2014/10/11 职场文书
防暑降温通知书
2015/04/27 职场文书
举起手来观后感
2015/06/09 职场文书
2015年“我们的节日·重阳节”活动总结
2015/07/29 职场文书
校园安全主题班会
2015/08/12 职场文书
筑梦中国心得体会
2016/01/18 职场文书
教你怎么用Python selenium操作浏览器对象的基础API
2021/06/23 Python
Oracle安装TNS_ADMIN环境变量设置参考
2021/11/01 Oracle