上节课程我们讲到了Promise,Promise最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。

那么,有没有更好的写法呢?

这就是我们今天接下来要讲解的Generator迭代器。

异步

先回顾下上节课所讲到的异步概念:
举个例子:
假设有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。
相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以当一个同步任务在执行的时候会阻塞其它任务的执行。

在其他的编成语言对于异步还有另外一种称呼更为贴切, 它们叫”多任务”, 我们经常谈异步编程解决方案,他们就谈多任务解决方案。 其中有一种叫法特别有意思叫做”协程”(coroutine),意思是多个线程互相协作,完成异步任务。

协程有点像函数,又有点像线程。它的运行流程大致如下。

  • 第一步,协程A开始执行。
  • 第二步,协程A执行到一半,进入暂停,执行权转移到协程B。
  • 第三步,(一段时间后)协程B交还执行权。
  • 第四步,协程A恢复执行。

上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。 这个过程像不像我们之前讲到过的时间片。
回顾下:

时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称作它的时间片,即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。而不会造成CPU资源浪费。

举例来说,读取文件的协程写法如下。

  1. function* asyncJob() {
  2. // ...其他代码
  3. var f = yield readFile(fileA);
  4. // ...其他代码
  5. }

先来看语法:
Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态。

然后,Generator 函数的调用方法与普通函数不一样,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的遍历器对象。 也就说必须调用遍历器对象的next方法,使得指针移向下一个状态。每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

实操

  1. function* helloWorldGenerator() {
  2. yield 'hello';
  3. yield 'world';
  4. return 'ending';
  5. }
  6. var hw = helloWorldGenerator();
  1. hw.next()
  2. // { value: 'hello', done: false }
  3. hw.next()
  4. // { value: 'world', done: false }
  5. hw.next()
  6. // { value: 'ending', done: true }
  7. hw.next()
  8. // { value: undefined, done: true }

第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。

第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。

第三次调用: 同上。

第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。

总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

异步任务封装

下面看看如何使用 Generator 函数,执行一个真实的异步任务。

  1. function* gen(){
  2. var url = 'https://api.github.com/users/github';
  3. var result = yield fetch(url);
  4. console.log(result.bio);
  5. }

上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上的 yield 命令。

fetch()是 XMLHttpRequest 的升级版,用于在 JavaScript 脚本里面发出 HTTP 请求。 浏览器原生提供这个对象。

fetch()的功能与 XMLHttpRequest 基本相同,但用法上有很大差异。fetch()接受一个 URL 字符串作为参数,默认向该网址发出 GET 请求,返回一个 Promise 对象。它的基本用法如下。

  1. fetch(url)
  2. .then(...)
  3. .catch(...)

当接收到一个代表错误的 HTTP 状态码时,从fetch()返回的 Promise不会被标记为 reject,即使响应的 HTTP 状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve ,仅当网络故障时或请求被阻止时,才会标记为 reject。

fetch()请求成功以后,得到的是一个Response 对象。它对应服务器的 HTTP 回应。上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

执行这段代码的方法如下。

  1. var g = gen();
  2. var result = g.next();
  3. result.value.then(function(response){
  4. return response.json();
  5. }).then(function(data){
  6. g.next(data);
  7. });

可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是如果有多个异步操作且如果必须保证前一步执行完,才能执行后异步流程管理就会很困难。

虽然如此我们还是有办法来解决。

  1. var fs = require('fs');
  2. function thunkify(fn) {
  3. return function() {
  4. var args = new Array(arguments.length);
  5. var ctx = this;
  6. for (var i = 0; i < args.length; ++i) {
  7. args[i] = arguments[i];
  8. }
  9. return function(done) {
  10. var called;
  11. args.push(function() {
  12. if (called) return;
  13. called = true;
  14. done.apply(null, arguments);
  15. });
  16. try {
  17. fn.apply(ctx, args);
  18. } catch (err) {
  19. done(err);
  20. }
  21. }
  22. }
  23. };
  24. var readFileThunk = thunkify(fs.readFile);
  25. var gen = function*() {
  26. var f1 = yield readFileThunk('./etc/fstab.txt');
  27. var f2 = yield readFileThunk('./etc/shells.txt');
  28. console.log(f1.toString());
  29. console.log(f2.toString());
  30. };
  31. var g = gen();
  32. var r1 = g.next();
  33. r1.value(function (err, data) {
  34. if (err) throw err;
  35. var r2 = g.next(data);
  36. r2.value(function (err, data) {
  37. if (err) throw err;
  38. g.next(data);
  39. });
  40. });

这里我们利用了node 搭建了一个异步读取文件的小 demo。 通过封装thunkify 来进行流程的自动管理。

Thunk 函数并不是 Generator 函数自动执行的唯一方案。在node 社区里面有一个 co 模块 通过该模块也能更加简便的控制 Generator 函数的自动执行。

var fs = require('fs');
var co = require("co");

var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) return reject(error);
      resolve(data);
    });
  });
};

var gen = function* () {
  var f1 = yield readFile('./etc/fstab.txt');
  var f2 = yield readFile('./etc/shells.txt');
  console.log(f1.toString());
  console.log(f2.toString());
};

co(gen);