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 相关文章推荐
浅析js中2个等号与3个等号的区别
Aug 06 Javascript
深入理解JQuery keyUp和keyDown的区别
Dec 12 Javascript
JavaScript弹出新窗口并控制窗口移动到指定位置的方法
Apr 06 Javascript
JavaScript实现简单的数字倒计时
May 15 Javascript
JS使用post提交的两种方式
Dec 03 Javascript
使用重写url机制实现验证码换一张功能
Aug 01 Javascript
Node.js利用断言模块assert进行单元测试的方法
Sep 28 Javascript
Vue CLI 3搭建vue+vuex最全分析(推荐)
Sep 27 Javascript
vue.js实现简单的计算器功能
Feb 22 Javascript
基于VSCode调试网页JavaScript代码过程详解
Jul 20 Javascript
解决antd datepicker 获取时间默认少8个小时的问题
Oct 29 Javascript
jQuery实现tab栏切换效果
Dec 22 jQuery
详解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 阴历-农历-转换类代码
2012/01/16 PHP
php中拷贝构造函数、赋值运算符重载
2012/07/25 PHP
PHP批量检测并去除文件BOM头代码实例
2014/05/08 PHP
PHP处理Oracle的CLOB实例
2014/11/03 PHP
学习php设计模式 php实现备忘录模式(Memento)
2015/12/09 PHP
Yii2下点击验证码的切换实例代码
2017/03/14 PHP
thinkphp整合系列之极验滑动验证码geetest功能
2019/06/18 PHP
js数组的基本用法及数组根据下标(数值或字符)移除元素
2013/10/20 Javascript
Nodejs实现的一个静态服务器实例
2014/12/06 NodeJs
JavaScript解析json格式数据简单示例
2014/12/09 Javascript
Node.js抓取中文网页乱码问题和解决方法
2015/02/10 Javascript
javaScript事件机制兼容【详细整理】
2016/07/23 Javascript
JS控制div跳转到指定的位置的几种解决方案总结
2016/11/05 Javascript
详解js树形控件—zTree使用总结
2016/12/28 Javascript
微信小程序button组件使用详解
2018/01/31 Javascript
vue强制刷新组件的方法示例
2019/02/28 Javascript
node之本地服务器图片上传的方法示例
2019/03/26 Javascript
vue.js中ref及$refs的使用方法解析
2019/10/08 Javascript
vue单文件组件无法获取$refs的问题
2020/06/24 Javascript
vant 自定义 van-dropdown-item的用法
2020/08/05 Javascript
基于javascript的无缝滚动动画1
2020/08/07 Javascript
influx+grafana自定义python采集数据和一些坑的总结
2018/09/17 Python
Python中使用logging和traceback模块记录日志和跟踪异常
2019/04/09 Python
python读取并定位excel数据坐标系详解
2019/06/26 Python
使用pickle存储数据dump 和 load实例讲解
2019/12/30 Python
Django和Ueditor自定义存储上传文件的文件名
2021/02/25 Python
美国排名第一的泳池用品直接来源:In The Swim
2019/09/23 全球购物
庆祝教师节活动方案
2014/01/31 职场文书
领导调研接待方案
2014/02/27 职场文书
医疗纠纷协议书
2014/04/16 职场文书
2014年五四青年节演讲稿范文
2014/04/22 职场文书
美国留学经济担保书
2014/05/20 职场文书
革命英雄事迹演讲稿
2014/09/13 职场文书
群众路线表态发言材料
2014/10/17 职场文书
MySql 8.0及对应驱动包匹配的注意点说明
2021/06/23 MySQL
详解Java实现设计模式之责任链模式
2021/06/23 Java/Android