让人头痛的Generator 函数的异步应用真的有用吗?

异步编程对 JavaScript 语言太重要 。 JavaScript 语言的执行环境是“单线程”的 , 如果没有异步编程 , 根本没法用 , 非卡死不可 。 本章主要介绍 Generator 函数如何完成异步操作 。
1、传统方法ES6 诞生以前 , 异步编程的方法 , 大概有下面四种 。

  • 回调函数
  • 事件监听
  • 发布/订阅
  • Promise 对象
Generator 函数将 JavaScript 异步编程带入了一个全新的阶段 。
2、基本概念异步所谓"异步" , 简单说就是一个任务不是连续完成的 , 可以理解成该任务被人为分成两段 , 先执行第一段 , 然后转而执行其他任务 , 等做好了准备 , 再回过头执行第二段 。
比如 , 有一个任务是读取文件进行处理 , 任务的第一段是向操作系统发出请求 , 要求读取文件 。 然后 , 程序执行其他任务 , 等到操作系统返回文件 , 再接着执行任务的第二段(处理文件) 。 这种不连续的执行 , 就叫做异步 。
相应地 , 连续的执行就叫做同步 。 由于是连续执行 , 不能插入其他任务 , 所以操作系统从硬盘读取文件的这段时间 , 程序只能干等着 。
回调函数JavaScript 语言对异步编程的实现 , 就是回调函数 。 所谓回调函数 , 就是把任务的第二段单独写在一个函数里面 , 等到重新执行这个任务的时候 , 就直接调用这个函数 。 回调函数的英语名字callback , 直译过来就是"重新调用" 。
读取文件进行处理 , 是这样写的 。
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {if (err) throw err;console.log(data);});上面代码中 , readFile函数的第三个参数 , 就是回调函数 , 也就是任务的第二段 。 等到操作系统返回了/etc/passwd这个文件以后 , 回调函数才会执行 。
一个有趣的问题是 , 为什么 Node 约定 , 回调函数的第一个参数 , 必须是错误对象err(如果没有错误 , 该参数就是null)?
原因是执行分成两段 , 第一段执行完以后 , 任务所在的上下文环境就已经结束了 。 在这以后抛出的错误 , 原来的上下文环境已经无法捕捉 , 只能当作参数 , 传入第二段 。
Promise回调函数本身并没有问题 , 它的问题出现在多个回调函数嵌套 。 假定读取A文件之后 , 再读取B文件 , 代码如下 。
fs.readFile(fileA, 'utf-8', function (err, data) {fs.readFile(fileB, 'utf-8', function (err, data) {// ...});});不难想象 , 如果依次读取两个以上的文件 , 就会出现多重嵌套 。 代码不是纵向发展 , 而是横向发展 , 很快就会乱成一团 , 无法管理 。 因为多个异步操作形成了强耦合 , 只要有一个操作需要修改 , 它的上层回调函数和下层回调函数 , 可能都要跟着修改 。 这种情况就称为"回调函数地狱"(callback hell) 。
Promise 对象就是为了解决这个问题而提出的 。 它不是新的语法功能 , 而是一种新的写法 , 允许将回调函数的嵌套 , 改成链式调用 。 采用 Promise , 连续读取多个文件 , 写法如下 。
var readFile = require('fs-readfile-promise');readFile(fileA).then(function (data) {console.log(data.toString());}).then(function () {return readFile(fileB);}).then(function (data) {console.log(data.toString());}).catch(function (err) {console.log(err);});上面代码中 , 我使用了fs-readfile-promise模块 , 它的作用就是返回一个 Promise 版本的readFile函数 。 Promise 提供then方法加载回调函数 , catch方法捕捉执行过程中抛出的错误 。
可以看到 , Promise 的写法只是回调函数的改进 , 使用then方法以后 , 异步任务的两段执行看得更清楚了 , 除此以外 , 并无新意 。
Promise 的最大问题是代码冗余 , 原来的任务被 Promise 包装了一下 , 不管什么操作 , 一眼看去都是一堆then , 原来的语义变得很不清楚 。
那么 , 有没有更好的写法呢?
3、Generator 函数协程传统的编程语言 , 早有异步编程的解决方案(其实是多任务的解决方案) 。 其中有一种叫做"协程"(coroutine) , 意思是多个线程互相协作 , 完成异步任务 。
协程有点像函数 , 又有点像线程 。 它的运行流程大致如下 。
  • 第一步 , 协程A开始执行 。
  • 第二步 , 协程A执行到一半 , 进入暂停 , 执行权转移到协程B 。
  • 第三步 , (一段时间后)协程B交还执行权 。
  • 第四步 , 协程A恢复执行 。
上面流程的协程A , 就是异步任务 , 因为它分成两段(或多段)执行 。
举例来说 , 读取文件的协程写法如下 。
function* asyncJob() {// ...其他代码var f = yield readFile(fileA);// ...其他代码}