Node.js 异步流程同步化

这些天,我在陆续地用 Node.js 重写以前的 Python 和 PHP 代码,是为了让技能树长得更高,而修剪掉次要的技能。

大三时发现学校某系统上有学生证件照,而且它们的地址是明文(学号)。于是写了个 Python 脚本,去抓取所有学生的照片。看看当时的代码,不得不说 Python 确实优雅,怪不得那么多人说“人生苦短,我用 Python”。

相对而言,JavaScript(下文简称 JS)就不优雅了,部分由基因决定:Python 作为纯后端语言,可以使用缩进来定义代码块,既保持了良好的代码格式,又省去了冗长的大括号。而 JS 作为一门前端语言,为了节省带宽往往要做代码压缩,所以代码块必须用大括号来标明。另一部分原因是,Python 内置了许多实用函数,虽然 npm 社区有许多开源的包作为替代 ,但为了练手我先不用。事实上,本文基本围绕着 urllib.request.urlretrieve 这个函数展开。

这段程序大致分为两块:其一为迭代器,遍历所有应抓取的学号。其二为抓取文件并写入磁盘。现在开始翻译吧!首先,引入模块,补全实用函数。

和 Python 不同的是,这个 range 函数仅仅返回一个可迭代对象而不是一个数组,以减少内存占用。什么叫可迭代?看下边的例子你就懂了:

接下来,为了专注于主题,先把学号迭代器分离出来,同样使用生成器函数:

把大括号叠起来,看起来更像 Python 了。这个 iter 会一次吐一个学号出来。它的行为会根据外部环境(文件是否已存在? 已连续失败多少次?)进行微调。

基于协程的同步流程

Node.js 自带的文件读写函数提供了同步和异步两个版本,为了写起来舒服,看起来也舒服,我这里尽量用了同步的。但是,http 库的 get 方法并未提供同步版本。有没有什么办法能造出一个同步版本的 get,即下文的 getSync 呢?

getSync 函数应当是 http.get 函数的封装。其接受 url 作为参数,(简单起见)仅返回响应主体,或者抛出一个错误。先试着写一个出来:

点击,运行,啪!程序卡死在了 while 循环处。因为 JS 运行时是单线程模型,http.get 的回调函数被放到了消息队列中,必须等待主线程执行完毕,才会执行。换言之,while 循环阻塞了回调函数,故 donee 的值永远不会发生变化。

为了修复这个问题,我们需要操控一种叫协程(coroutine),又名纤程(fiber)的东西。协程是一种微线程,表现为函数调用,可随时中断并记忆执行环境,也可随时恢复运行。其实生成器的 yield 语句就是协程的一种实现,只不过没有暴露直接操控协程的接口。所幸,有个用 C 语言底层实现的库 node-fibers 满足了我的需要。

使用起来很简单,用 Fiber.yield() 中断当前协程,f.run() 启动指定协程。这个问题就这样解决啦!不过还要注意一点,for (let identity of iter) 这个语句要整个包在一个函数里,并传给 Fiber() 作为参数,像这样:

当整个流程被包装成一个协程时,我们才能中断或启动它。否则我们是不能暂停主线程的。

附:基于事件回调的异步流程

iter 还是那个 iter,倒也是挺简洁的。

附:基于 Promise 的异步流程

虽然是 ES6 的新内容,但 Promise/A+规范其实很老了,互联网上也有很多关于 Promise 的优质文章,所以这里就不详细介绍了。

简而言之,Promise 要求程序定义,一个异步操作何时叫成功(resolve),何时叫失败(reject)。而且若这个操作已成功,就绝不会再失败,反之亦然。还有个好处是,状态的处理函数可以在状态改变后添加,随后立即执行。而回调函数只能在事件触发前添加。

 

2 条评论

  1. Art9说道:

    无可奉告好评!不过看到协程那里就不懂了 =_= 为啥要那么多的 run()

    • HandsomeOne说道:

      在外层获取主进程 f,然后在异步回调的结束状态交回执行权嘛

发表评论

电子邮件地址不会被公开。 必填项已用*标注