当我们达成这个共识以后,就要开始进行问题的分析了。
首先你要了解你所使用的这几个包的作用是什么,如果能知道他们是怎么实现的那就更好了。
对于co,就是一个利用yield语法特性将Promise转换为更直观的写法罢了,没有什么额外的逻辑。
而urllib也会在每次调用request时创建一个新的client(刚开始有想过会不会是因为多次调用urllib导致的,不过用简单的Promise.resolve代替之后,这个念头也打消了)
那么矛头就指向了formstream,现在要进一步的了解它,不过通过官方文档进行查阅,并不能得到太多的有效信息。
源码阅读源码地址
所以为了解决问题,我们需要去阅读它的源码,从你在代码中调用的那些 API 入手:
构造函数营养并不多,就是一些简单的属性定义,并且看到了它继承自Stream,这也是为什么能够在urllib的options中直接填写它的原因,因为是一个Stream的子类。
util.inherits(FormStream, Stream);然后就要看field函数的实现了。
FormStream.prototype.field = function (name, value) { if (!Buffer.isBuffer(value)) { // field(String, Number) // https://github.com/qiniu/nodejs-sdk/issues/123 if (typeof value === 'number') { value = String(value); } value = new Buffer(value); } return this.buffer(name, value); };从代码的实现看,field也只是一个Buffer的封装处理,最终还是调用了.buffer函数。
那么我们就顺藤摸瓜,继续查看函数的实现。
代码不算少,不过大多都不是这次需要关心的,大致的逻辑就是将Buffer拼接到数组中去暂存,在最后结尾的地方,发现了这样的一句代码:process.nextTick(this.resume.bind(this))。
顿时眼前一亮,重点的是那个process.nextTick,大家应该都知道,这个是在Node中实现微任务的其中一个方式,而另一种实现微任务的方式,就是用Promise。
拿到这样的结果以后,我觉得仿佛找到了突破口,于是尝试性的将前边的代码改为这样:
const form = new Formstream() form.field('timestamp', moment().unix()) yield Promise.resolve(1) const options = { method: 'POST', headers: form.headers(), stream: form } process.nextTick(() => { urllib.request(url, options) })发现,果然超时了。
从这里就能大致推断出问题的原因了。
因为看代码可以很清晰的看出,field函数在调用后,会注册一个微任务,而我们使用的yield或者process.nextTick也会注册一个微任务,但是field的先注册,所以它的一定会先执行。
那么很显而易见,问题就出现在这个resume函数中,因为resume的执行早于urllib.request,所以导致其超时。
这时候也可以同步的想一下造成request超时的情况会是什么。
只有一种可能性是比较高的,因为我们使用的是stream,而这个流的读取是需要事件来触发的,stream.on('data')、stream.on('end'),那么超时很有可能是因为程序没有正确接收到stream的事件导致的。
当然了,「程序员修炼之道」还讲过:
Don't Assume it - Prove It
不要假定,要证明
所以为了证实猜测,需要继续阅读formstream的源码,查看resume函数究竟做了什么。
resume函数是一个很简单的一次性函数,在第一次被触发时调用drain函数。
那么继续查看drain函数做的是什么事情。
因为上述使用的是field,而非stream,所以在获取item的时候,肯定为空,那么这就意味着会继续调用_emitEnd函数。
而_emitEnd函数只有简单的两行代码emit('data')和emit('end')。