ES6 Generator 函数
-
定义和使用
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍 Generator 函数的语法和 API,它的异步编程应用请看《Generator 函数的异步应用》一章。Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式,定义不同的内部状态(yield 在英语里的意思就是“产出”)。function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator();
上面代码定义了一个 Generator 函数 helloWorldGenerator,它内部有两个 yield 表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。ES6 没有规定,function 关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。function * foo(x, y) { ··· } function *foo(x, y) { ··· } function* foo(x, y) { ··· } function*foo(x, y) { ··· }
由于 Generator 函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在function 关键字后面。本书也采用这种写法。
-
next 方法的参数
yield
表达式本身没有返回值,或者说总是返回 undefined。next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。function* f() { for(var i = 0; true; i++) { var reset = yield i; if(reset) { i = -1; } } } var g = f(); g.next() // { value: 0, done: false } g.next() // { value: 1, done: false } g.next(true) // { value: 0, done: false }
上面代码先定义了一个可以无限运行的 Generator 函数 f,如果next
方法没有参数,每次运行到yield
表达式,变量 reset 的值总是 undefined。当next
方法带一个参数 true 时,变量 reset 就被重置为这个参数(即true),因此 i 会等于-1,下一轮循环就会从 -1 开始递增。这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next
方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。再看一个例子。function* foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z); } var a = foo(5); a.next() // Object{value:6, done:false} a.next() // Object{value:NaN, done:false} a.next() // Object{value:NaN, done:true} var b = foo(5); b.next() // { value:6, done:false } b.next(12) // { value:8, done:false } b.next(13) // { value:42, done:true }
上面代码中,第二次运行next
方法的时候不带参数,导致 y 的值等于 2 * undefined(即NaN),除以 3 以后还是 NaN,因此返回对象的 value 属性也等于 NaN。第三次运行Next
方法的时候不带参数,所以 z 等于 undefined,返回对象的 value 属性等于5 + NaN + undefined,即 NaN。再看一个通过next
方法的参数,向 Generator 函数内部输入值的例子。function* dataConsumer() { console.log('Started'); console.log(`1. ${yield}`); console.log(`2. ${yield}`); return 'result'; } let genObj = dataConsumer(); genObj.next(); // Started genObj.next('a') // 1. a genObj.next('b') // 2. b
上面代码是一个很直观的例子,每次通过next
方法向 Generator 函数输入值,然后打印出来。如果想要第一次调用next
方法时,就能够输入值,可以在 Generator 函数外面再包一层。function wrapper(generatorFunction) { return function (...args) { let generatorObject = generatorFunction(...args); generatorObject.next(); return generatorObject; }; } const wrapped = wrapper(function* () { console.log(`First input: ${yield}`); return 'DONE'; }); wrapped().next('hello!') // First input: hello!
上面代码中,Generator 函数如果不用 wrapper 先包一层,是无法第一次调用next
方法,就输入参数的。 -
for...of 循环
for...of
循环可以自动遍历 Generator 函数运行时生成的 Iterator 对象,且此时不再需要调用next
方法。function* foo() { yield 1; yield 2; yield 3; yield 4; yield 5; return 6; } for (let v of foo()) { console.log(v); } // 1 2 3 4 5
上面代码使用for...of
循环,依次显示 5 个 yield 表达式的值。这里需要注意,一旦next
方法的返回对象的 done 属性为 true,for...of
循环就会中止,且不包含该返回对象,所以上面代码的 return 语句返回的 6,不包括在for...of
循环之中。下面是一个利用 Generator 函数和for...of
循环,实现斐波那契数列的例子。function* fibonacci() { let [prev, curr] = [0, 1]; for (;;) { yield curr; [prev, curr] = [curr, prev + curr]; } } for (let n of fibonacci()) { if (n > 1000) break; console.log(n); }
从上面代码可见,使用for...of
语句时不需要使用next
方法。利用for...of
循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用for...of
循环,通过 Generator 函数为它加上这个接口,就可以用了。function* objectEntries(obj) { let propKeys = Reflect.ownKeys(obj); for (let propKey of propKeys) { yield [propKey, obj[propKey]]; } } let jane = { first: 'Jane', last: 'Doe' }; for (let [key, value] of objectEntries(jane)) { console.log(`${key}: ${value}`); } // first: Jane // last: Doe
上面代码中,对象 jane 原生不具备 Iterator 接口,无法用for...of
遍历。这时,我们通过 Generator 函数 objectEntries 为它加上遍历器接口,就可以用for...of
遍历了。加上遍历器接口的另一种写法是,将 Generator 函数加到对象的Symbol.iterator
属性上面。function* objectEntries() { let propKeys = Object.keys(this); for (let propKey of propKeys) { yield [propKey, this[propKey]]; } } let jane = { first: 'Jane', last: 'Doe' }; jane[Symbol.iterator] = objectEntries; for (let [key, value] of jane) { console.log(`${key}: ${value}`); } // first: Jane // last: Doe
除了for...of
循环以外,扩展运算符(...
)、解构赋值和Array.from
方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。function* numbers () { yield 1 yield 2 return 3 yield 4 } // 扩展运算符 [...numbers()] // [1, 2] // Array.from 方法 Array.from(numbers()) // [1, 2] // 解构赋值 let [x, y] = numbers(); x // 1 y // 2 // for...of 循环 for (let n of numbers()) { console.log(n) } // 1 // 2
更多的
for...of
循环,请参考《for...of循环》这章 -
Generator.prototype.throw()
Generator 函数返回的遍历器对象,都有一个throw
方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。var g = function* () { try { yield; } catch (e) { console.log('内部捕获', e); } }; var i = g(); i.next(); try { i.throw('a'); i.throw('b'); } catch (e) { console.log('外部捕获', e); } // 内部捕获 a // 外部捕获 b
上面代码中,遍历器对象 i 连续抛出两个错误。第一个错误被 Generator 函数体内的catch
语句捕获。i 第二次抛出错误,由于 Generator 函数内部的catch
语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch
语句捕获。throw
方法可以接受一个参数,该参数会被catch
语句接收,建议抛出 Error 对象的实例。var g = function* () { try { yield; } catch (e) { console.log(e); } }; var i = g(); i.next(); i.throw(new Error('出错了!')); // Error: 出错了!(…)
注意,不要混淆遍历器对象的throw
方法和全局的throw
命令。上面代码的错误,是用遍历器对象的throw
方法抛出的,而不是用throw
命令抛出的。后者只能被函数体外的catch
语句捕获。var g = function* () { while (true) { try { yield; } catch (e) { if (e != 'a') throw e; console.log('内部捕获', e); } } }; var i = g(); i.next(); try { throw new Error('a'); throw new Error('b'); } catch (e) { console.log('外部捕获', e); } // 外部捕获 [Error: a]
上面代码之所以只捕获了 a,是因为函数体外的catch
语句块,捕获了抛出的 a 错误以后,就不会再继续try
代码块里面剩余的语句了。如果 Generator 函数内部没有部署try...catch
代码块,那么 throw 方法抛出的错误,将被外部try...catch
代码块捕获。var g = function* () { while (true) { yield; console.log('内部捕获', e); } }; var i = g(); i.next(); try { i.throw('a'); i.throw('b'); } catch (e) { console.log('外部捕获', e); } // 外部捕获 a
上面代码中,Generator 函数 g 内部没有部署try...catch
代码块,所以抛出的错误直接被外部catch
代码块捕获。如果 Generator 函数内部和外部,都没有部署try...catch
代码块,那么程序将报错,直接中断执行。var gen = function* gen(){ yield console.log('hello'); yield console.log('world'); } var g = gen(); g.next(); g.throw(); // hello // Uncaught undefined
上面代码中,g.throw 抛出错误以后,没有任何try...catch
代码块可以捕获这个错误,导致程序报错,中断执行。throw
方法抛出的错误要被内部捕获,前提是必须至少执行过一次next
方法。function* gen() { try { yield 1; } catch (e) { console.log('内部捕获'); } } var g = gen(); g.throw(1); // Uncaught 1
上面代码中,g.throw(1) 执行时,next
方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。这种行为其实很好理解,因为第一次执行next
方法,等同于启动执行 Generator 函数的内部代码,否则 Generator 函数还没有开始执行,这时throw
方法抛错只可能抛出在函数外部。throw
方法被捕获以后,会附带执行下一条 yield 表达式。也就是说,会附带执行一次next
方法。var gen = function* gen(){ try { yield console.log('a'); } catch (e) { // ... } yield console.log('b'); yield console.log('c'); } var g = gen(); g.next() // a g.throw() // b g.next() // c
上面代码中,g.throw 方法被捕获以后,自动执行了一次next
方法,所以会打印 b。另外,也可以看到,只要 Generator 函数内部部署了try...catch
代码块,那么遍历器的throw
方法抛出的错误,不影响下一次遍历。一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next
方法,将返回一个 value 属性等于 undefined、done 属性等于 true 的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。function* g() { yield 1; console.log('throwing an exception'); throw new Error('generator broke!'); yield 2; yield 3; } function log(generator) { var v; console.log('starting generator'); try { v = generator.next(); console.log('第一次运行next方法', v); } catch (err) { console.log('捕捉错误', v); } try { v = generator.next(); console.log('第二次运行next方法', v); } catch (err) { console.log('捕捉错误', v); } try { v = generator.next(); console.log('第三次运行next方法', v); } catch (err) { console.log('捕捉错误', v); } console.log('caller done'); } log(g()); // starting generator // 第一次运行next方法 { value: 1, done: false } // throwing an exception // 捕捉错误 { value: 1, done: false } // 第三次运行next方法 { value: undefined, done: true } // caller done
上面代码一共三次运行next
方法,第二次运行的时候会抛出错误,然后第三次运行的时候,Generator 函数就已经结束了,不再执行下去了。 -
Generator.prototype.return()
Generator 函数返回的遍历器对象,还有一个return
方法,可以返回给定的值,并且终结遍历 Generator 函数。function* gen() { yield 1; yield 2; yield 3; } var g = gen(); g.next() // { value: 1, done: false } g.return('foo') // { value: "foo", done: true } g.next() // { value: undefined, done: true }
上面代码中,遍历器对象 g 调用return
方法后,返回值的 value 属性就是return
方法的参数 foo。并且,Generator 函数的遍历就终止了,返回值的 done 属性为 true,以后再调用next
方法,done 属性总是返回 true。如果return
方法调用时,不提供参数,则返回值的 value 属性为 undefined。function* gen() { yield 1; yield 2; yield 3; } var g = gen(); g.next() // { value: 1, done: false } g.return() // { value: undefined, done: true }
如果 Generator 函数内部有try...finally
代码块,且正在执行try
代码块,那么return
方法会导致立刻进入finally
代码块,执行完以后,整个函数才会结束。function* numbers () { yield 1; try { yield 2; yield 3; } finally { yield 4; yield 5; } yield 6; } var g = numbers(); g.next() // { value: 1, done: false } g.next() // { value: 2, done: false } g.return(7) // { value: 4, done: false } g.next() // { value: 5, done: false } g.next() // { value: 7, done: true }
上面代码中,调用return()
方法后,就开始执行finally
代码块,不执行try
里面剩下的代码了,然后等到finally
代码块执行完,再返回return()
方法指定的返回值。 -
next()、throw()、return() 的共同点
next()
、throw()
、return()
这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式。next()
是将 yield 表达式替换成一个值。const g = function* (x, y) { let result = yield x + y; return result; }; const gen = g(1, 2); gen.next(); // Object {value: 3, done: false} gen.next(1); // Object {value: 1, done: true} // 相当于将 let result = yield x + y // 替换成 let result = 1;
上面代码中,第二个next(1)
方法就相当于将 yield 表达式替换成一个值 1。如果next()
方法没有参数,就相当于替换成 undefined。throw()
是将 yield 表达式替换成一个throw
语句。gen.throw(new Error('出错了')); // Uncaught Error: 出错了 // 相当于将 let result = yield x + y // 替换成 let result = throw(new Error('出错了'));
return()
是将 yield 表达式替换成一个return
语句。gen.return(2); // Object {value: 2, done: true} // 相当于将 let result = yield x + y // 替换成 let result = return 2;
-
yield* 表达式
如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。function* foo() { yield 'a'; yield 'b'; } function* bar() { yield 'x'; // 手动遍历 foo() for (let i of foo()) { console.log(i); } yield 'y'; } for (let v of bar()){ console.log(v); } // x // a // b // y
上面代码中,foo 和 bar 都是 Generator 函数,在 bar 里面调用 foo,就需要手动遍历 foo。如果有多个 Generator 函数嵌套,写起来就非常麻烦。ES6 提供了 yield* 表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。function* bar() { yield 'x'; yield* foo(); yield 'y'; } // 等同于 function* bar() { yield 'x'; yield 'a'; yield 'b'; yield 'y'; } // 等同于 function* bar() { yield 'x'; for (let v of foo()) { yield v; } yield 'y'; } for (let v of bar()){ console.log(v); } // "x" // "a" // "b" // "y"
再来看一个对比的例子。function* inner() { yield 'hello!'; } function* outer1() { yield 'open'; yield inner(); yield 'close'; } var gen = outer1() gen.next().value // "open" gen.next().value // 返回一个遍历器对象 gen.next().value // "close" function* outer2() { yield 'open' yield* inner() yield 'close' } var gen = outer2() gen.next().value // "open" gen.next().value // "hello!" gen.next().value // "close"
上面例子中,outer2 使用了 yield*,outer1 没使用。结果就是,outer1 返回一个遍历器对象,outer2 返回该遍历器对象的内部值。从语法角度看,如果 yield 表达式后面跟的是一个遍历器对象,需要在 yield 表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为 yield* 表达式。let delegatedIterator = (function* () { yield 'Hello!'; yield 'Bye!'; }()); let delegatingIterator = (function* () { yield 'Greetings!'; yield* delegatedIterator; yield 'Ok, bye.'; }()); for(let value of delegatingIterator) { console.log(value); } // "Greetings! // "Hello!" // "Bye!" // "Ok, bye."
上面代码中,delegatingIterator 是代理者,delegatedIterator 是被代理者。由于 yield* delegatedIterator 语句得到的值,是一个遍历器,所以要用星号表示。运行结果就是使用一个遍历器,遍历了多个 Generator 函数,有递归的效果。yield* 后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of
循环。function* concat(iter1, iter2) { yield* iter1; yield* iter2; } // 等同于 function* concat(iter1, iter2) { for (var value of iter1) { yield value; } for (var value of iter2) { yield value; } }
上面代码说明,yield* 后面的 Generator 函数(没有return语句时),不过是 for...of 的一种简写形式,完全可以用后者替代前者。反之,在有 return 语句时,则需要用 var value = yield* iterator 的形式获取 return 语句的值。如果 yield* 后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。function* gen(){ yield* ["a", "b", "c"]; } gen().next() // { value:"a", done:false }
上面代码中,yield 命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。实际上,任何数据结构只要有 Iterator 接口,就可以被 yield* 遍历。let read = (function* () { yield 'hello'; yield* 'hello'; })(); read.next().value // "hello" read.next().value // "h"
上面代码中,yield 表达式返回整个字符串,yield* 语句返回单个字符。因为字符串具有 Iterator 接口,所以被 yield* 遍历。如果被代理的 Generator 函数有 return 语句,那么就可以向代理它的 Generator 函数返回数据。function* foo() { yield 2; yield 3; return "foo"; } function* bar() { yield 1; var v = yield* foo(); console.log("v: " + v); yield 4; } var it = bar(); it.next() // {value: 1, done: false} it.next() // {value: 2, done: false} it.next() // {value: 3, done: false} it.next(); // "v: foo" // {value: 4, done: false} it.next() // {value: undefined, done: true}
上面代码在第四次调用next()
方法的时候,屏幕上会有输出,这是因为函数 foo 的 return 语句,向函数 bar 提供了返回值。 -
作为对象属性的 Generator 函数
如果一个对象的属性是 Generator 函数,可以简写成下面的形式。let obj = { * myGeneratorMethod() { ··· } };
上面代码中,myGeneratorMethod 属性前面有一个星号,表示这个属性是一个 Generator 函数。它的完整形式如下,与上面的写法是等价的。let obj = { myGeneratorMethod: function* () { // ··· } };
-
Generator 函数的this
Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的 prototype 对象上的方法。function* g() {} g.prototype.hello = function () { return 'hi!'; }; let obj = g(); obj instanceof g // true obj.hello() // 'hi!'
上面代码表明,Generator 函数 g 返回的遍历器 obj,是 g 的实例,而且继承了 g.prototype。但是,如果把 g 当作普通的构造函数,并不会生效,因为 g 返回的总是遍历器对象,而不是 this 对象。function* g() { this.a = 11; } let obj = g(); obj.next(); obj.a // undefined
上面代码中,Generator 函数 g 在 this 对象上面添加了一个属性 a,但是 obj 对象拿不到这个属性。Generator 函数也不能跟 new 命令一起用,会报错。function* F() { yield this.x = 2; yield this.y = 3; } new F() // TypeError: F is not a constructor
上面代码中,new 命令跟构造函数 F 一起使用,结果报错,因为 F 不是构造函数。那么,有没有办法让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this?
下面是一个变通方法。首先,生成一个空对象,使用 call 方法绑定 Generator 函数内部的 this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。function* F() { this.a = 1; yield this.b = 2; yield this.c = 3; } var obj = {}; var f = F.call(obj); f.next(); // Object {value: 2, done: false} f.next(); // Object {value: 3, done: false} f.next(); // Object {value: undefined, done: true} obj.a // 1 obj.b // 2 obj.c // 3
上面代码中,首先是 F 内部的 this 对象绑定 obj 对象,然后调用它,返回一个 Iterator 对象。这个对象执行三次next()
方法(因为F内部有两个yield表达式),完成 F 内部所有代码的运行。这时,所有内部属性都绑定在 obj 对象上了,因此 obj 对象也就成了 F 的实例。上面代码中,执行的是遍历器对象 f,但是生成的对象实例是 obj,有没有办法将这两个对象统一呢?一个办法就是将 obj 换成F.prototype
。function* F() { this.a = 1; yield this.b = 2; yield this.c = 3; } var f = F.call(F.prototype); f.next(); // Object {value: 2, done: false} f.next(); // Object {value: 3, done: false} f.next(); // Object {value: undefined, done: true} f.a // 1 f.b // 2 f.c // 3
再将 F 改成构造函数,就可以对它执行new
命令了。function* gen() { this.a = 1; yield this.b = 2; yield this.c = 3; } function F() { return gen.call(gen.prototype); } var f = new F(); f.next(); // Object {value: 2, done: false} f.next(); // Object {value: 3, done: false} f.next(); // Object {value: undefined, done: true} f.a // 1 f.b // 2 f.c // 3
-
Generator 与状态机
Generator 是实现状态机的最佳结构。比如,下面的 clock 函数就是一个状态机。var ticking = true; var clock = function() { if (ticking) console.log('Tick!'); else console.log('Tock!'); ticking = !ticking; }
上面代码的 clock 函数一共有两种状态(Tick和Tock),每运行一次,就改变一次状态。这个函数如果用 Generator 实现,就是下面这样。var clock = function* () { while (true) { console.log('Tick!'); yield; console.log('Tock!'); yield; } };
上面的 Generator 实现与 ES5 实现对比,可以看到少了用来保存状态的外部变量 ticking,这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。Generator 之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。
-
应用场景
(1)异步操作的同步化表达Generator 函数的暂停执行的效果,意味着可以把异步操作写在 yield 表达式里面,等到调用 next 方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在 yield 表达式下面,反正要等到调用 next 方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。function* loadUI() { showLoadingScreen(); yield loadUIDataAsynchronously(); hideLoadingScreen(); } var loader = loadUI(); // 加载UI loader.next() // 卸载UI loader.next()
上面代码中,第一次调用 loadUI 函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用 next 方法,则会显示 Loading 界面(showLoadingScreen),并且异步加载数据(loadUIDataAsynchronously)。等到数据加载完成,再一次使用 next方法,则会隐藏 Loading 界面。可以看到,这种写法的好处是所有 Loading 界面的逻辑,都被封装在一个函数,按部就班非常清晰。Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。function* main() { var result = yield request("http://some.url"); var resp = JSON.parse(result); console.log(resp.value); } function request(url) { makeAjaxCall(url, function(response){ it.next(response); }); } var it = main(); it.next();
上面代码的 main 函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个 yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall 函数中的 next 方法,必须加上 response 参数,因为 yield 表达式,本身是没有值的,总是等于 undefined。下面是另一个例子,通过 Generator 函数逐行读取文本文件。function* numbers() { let file = new FileReader("numbers.txt"); try { while(!file.eof) { yield parseInt(file.readLine(), 10); } } finally { file.close(); } }
上面代码打开文本文件,使用 yield 表达式可以手动逐行读取文件。(2)控制流管理如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // Do something with value4 }); }); }); });
采用 Promise 改写上面的代码。Promise.resolve(step1) .then(step2) .then(step3) .then(step4) .then(function (value4) { // Do something with value4 }, function (error) { // Handle any error from step1 through step4 }) .done();
上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。function* longRunningTask(value1) { try { var value2 = yield step1(value1); var value3 = yield step2(value2); var value4 = yield step3(value3); var value5 = yield step4(value4); // Do something with value4 } catch (e) { // Handle any error from step1 through step4 } }
然后,使用一个函数,按次序自动执行所有步骤。scheduler(longRunningTask(initialValue)); function scheduler(task) { var taskObj = task.next(task.value); // 如果Generator函数未结束,就继续调用 if (!taskObj.done) { task.value = taskObj.value scheduler(task); } }
注意,上面这种做法,只适合同步操作,即所有的 task 都必须是同步的,不能有异步操作。因为这里的代码一得到返回值,就继续往下执行,没有判断异步操作何时完成。如果要控制异步的操作流程,详见后面的《Generator 函数的异步应用》一章。
下面,利用 for...of 循环会自动依次执行 yield 命令的特性,提供一种更一般的控制流管理的方法。let steps = [step1Func, step2Func, step3Func]; function* iterateSteps(steps){ for (var i=0; i< steps.length; i++){ var step = steps[i]; yield step(); } }
上面代码中,数组 steps 封装了一个任务的多个步骤,Generator 函数 iterateSteps 则是依次为这些步骤加上 yield 命令。将任务分解成步骤之后,还可以将项目分解成多个依次执行的任务。let jobs = [job1, job2, job3]; function* iterateJobs(jobs){ for (var i=0; i< jobs.length; i++){ var job = jobs[i]; yield* iterateSteps(job.steps); } }
上面代码中,数组 jobs 封装了一个项目的多个任务,Generator 函数 iterateJobs 则是依次为这些任务加上 yield* 命令。最后,就可以用 for...of 循环一次性依次执行所有任务的所有步骤。for (var step of iterateJobs(jobs)){ console.log(step.id); }
再次提醒,上面的做法只能用于所有步骤都是同步操作的情况,不能有异步操作的步骤。如果想要依次执行异步的步骤,必须使用后面的《Generator 函数的异步应用》一章介绍的方法。
for...of 的本质是一个 while 循环,所以上面的代码实质上执行的是下面的逻辑。var it = iterateJobs(jobs); var res = it.next(); while (!res.done){ var result = res.value; // ... res = it.next(); }
(3)部署 Iterator 接口利用 Generator 函数,可以在任意对象上部署 Iterator 接口。function* iterEntries(obj) { let keys = Object.keys(obj); for (let i=0; i < keys.length; i++) { let key = keys[i]; yield [key, obj[key]]; } } let myObj = { foo: 3, bar: 7 }; for (let [key, value] of iterEntries(myObj)) { console.log(key, value); } // foo 3 // bar 7
上述代码中,myObj 是一个普通对象,通过 iterEntries 函数,就有了 Iterator 接口。也就是说,可以在任意对象上部署 next 方法。下面是一个对数组部署 Iterator 接口的例子,尽管数组原生具有这个接口。function* makeSimpleGenerator(array){ var nextIndex = 0; while(nextIndex < array.length){ yield array[nextIndex++]; } } var gen = makeSimpleGenerator(['yo', 'ya']); gen.next().value // 'yo' gen.next().value // 'ya' gen.next().done // true
(4)作为数据结构Generator 可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。function* doStuff() { yield fs.readFile.bind(null, 'hello.txt'); yield fs.readFile.bind(null, 'world.txt'); yield fs.readFile.bind(null, 'and-such.txt'); }
上面代码就是依次返回三个函数,但是由于使用了 Generator 函数,导致可以像处理数组那样,处理这三个返回的函数。for (task of doStuff()) { // task是一个函数,可以像回调函数那样使用它 }
实际上,如果用 ES5 表达,完全可以用数组模拟 Generator 的这种用法。function doStuff() { return [ fs.readFile.bind(null, 'hello.txt'), fs.readFile.bind(null, 'world.txt'), fs.readFile.bind(null, 'and-such.txt') ]; }
上面的函数,可以用一模一样的 for...of 循环处理!两相一比较,就不难看出 Generator 使得数据或者操作,具备了类似数组的接口。