概述

这是ES6提供的一种异步编程解决方案,异步的实现就是靠这方案实现的!

这函数可以理解成一个状态机,封装了许多内部状态,并且返回一个遍历器对象,可以依次遍历Generator函数内部的每一个状态。

这个函数与平常写的函数有些不同:

  • function 关键字与函数名之间有一个星号。
  • 函数体内部使用 yield 表达式,用来定义不同的内部状态。

以下定义一个名为 helloWorldGenerator ,内部有两个 yield 表达式,表示三状态:hello、world和return语句 (结束执行)

  1. function* helloWorldGenerator(){
  2. yield 'hello';
  3. yield 'world';
  4. return 'ending';
  5. }

同时,这个函数调用后并不会执行,返回的也不是该函数的执行结果,而是一个内部状态的指针对象,也就是遍历器对象(Iteratro Object)。

如果一个对象的属性是Generator函数,可以简写如下:

  1. let obj = {
  2. //简写前:
  3. myGeneratorMethod: function* (){ //some code }
  4. //简写后:
  5. *myGeneratorMethod(){ //some code }
  6. }

yield 表达式

定义: 有于Generator函数需要调用next方法才会遍历下一个内部状态,所以会提供一个可以暂停执行的函数,yield就是这样的一个标志。

换句话说,执行一次Generator函数后,指针就会指向当前下一个状态(即yield后面紧跟着的表达式的值),并停留在那个状态。
到下一次调用next方法后,也会继续往下执行,直到遇到下一个yield表达式。如果到最后没有yield表达式,就一直到函数结束直到return为止,并将后面的表达式的值,作为返回的对象的value属性值。要是连return都没有,返回的value属性就会是undefined

注意:yield只能用在Generator函数里,用在其他地方会报错!即使在Generator函数里包含一个普通函数,而这个普通函数里面用yield也会报错。

Iterator接口关系

如何使一个对象拥有Iterator接口?如下:

  1. let myIterable = {}
  2. myIterable[Symbol.iterator] = function* (){
  3. yield 1;
  4. yield 2;
  5. yield 3;
  6. }
  7. [...myIterable] //[1, 2, 3]

上面代码中,Generator函数赋给对象的Symbol.iterator属性,使得myIterable对象具有了Iterator接口,可以对这个对象使用扩展运算符了。
Generator函数执行后,返回一个遍历器对象,该对象本身也具有Symbol.iterator属性,执行后返回自身。如下代码:

  1. function* gen(){
  2. //some code
  3. }
  4. let g = gen();
  5. g[Symbol.iterator]() === g //true

next 方法的参数

其实yiedl本身是不会有返回值的,但是,如果next带一个参数的话,该参数就会被当作上一个yield表达式的返回值。

一般函数一旦执行了就会直到结束,而因为Generator函数有暂停机制,即每次都要执行next方法才会继续执行到下一个yield表达式。

这时,可以利用next方法的参数,来调整下一次的运行。
例如:

  1. function* foo(x) {
  2. let y = 2 * (yield (x + 1))
  3. let z = yield (y/3)
  4. return (x + y + z)
  5. }
  6. let a = foo(5)
  7. a.next() //Object{ value: 6, done: false}
  8. a.next() //Object{ value: NaN, done: false}
  9. a.next() //Object{ value: NaN, done: true}
  10. let b = foo(5)
  11. b.next() //Object{ value: 6, done: false}
  12. b.next(12) //Object{ value: 8, done: false}
  13. b.next(13) //Object{ value: 42, done: true}

注意:第一次调用next传参无效,按语议来说第一次调用next是用来启动遍历器对象,所以不用带参。

以上代码可一看出,第一次传参是有效的,但第二次没有传参,导致第2行的运行结果是let y = 2 * (yield (undefined + 1)) ,y的值就成了NaN
所以从12行开始的代码传参后没有报错。

for…of 循环

如果说next指向下一个状态并停留在那个状态的话,那么for...of则是一次性执行完所有状态。如:

  1. function* foo(){
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. yield 4;
  6. yield 5;
  7. return 6;
  8. }
  9. for(let v of foo()){
  10. console.log(v)
  11. }
  12. // 1 2 3 4 5

有一点与next相同,一旦返回的对象的done属性为truefor...of循环就会终止,且不包含该对象,所以上面的return语句不包括在for...of循环之中。

Generator.prototype.throw()

Generator函数返回的遍历器对象,都有个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获。如下:

  1. let g = function* (){
  2. try {
  3. yield;
  4. } catch (e) {
  5. console.log('内部捕获', e)
  6. }
  7. }
  8. let i = g();
  9. i.next();
  10. try {
  11. i.throw('a');
  12. i.throw('b')
  13. } catch (e){
  14. console.log('外部捕获', e)
  15. }

Generator.prototype.return()

最大的作用是:终结遍历Generator函数。如下:

  1. function* gen(){
  2. yield 1;
  3. yield 2;
  4. yield 3;
  5. }
  6. let g = gen()
  7. g.next() // {value: 1, done: false}
  8. g.return("foo") // {value: "foo", done: true}
  9. g.next() // {value: undefined, done: true}

使用了return后,即使done属性就变成true了,遍历器就终止,即使后面继续next也无济于事 ,但值得注意的是,如果reutn里面带参,那么返回的value属性的值就是return的参数。如果不带参,那么value的值为undefined

next()、throw()、return() 共同点

都是可以让Generator函数恢复执行,并使用不同的语句替换yield表达式。

如下:

  1. const g = function* (x, y){
  2. let result = yield x + y;
  3. return result;
  4. }
  5. const gen = g(1, 2);
  6. gen.next(); //Object {value: 3, done: false}
  7. gen.next(1); //Object {value: 1, done: true}
  8. gen.throw(new Error('出错了'));
  9. gen.return(2);

上面代码中的第9行,next(1)相当于将yield表达式替换成一个数字值1。如果next()没有参数,就相当于替换成undefined

同理,第11行的替换yield表达式,相当于第2行变成了let result = new Error('出错了')。当然,第13行会替换第2行,变成let result = return 2

yield* 表达式

定义:可以在一个Generator函数里面执行另一个Generator函数。如下:

  1. function foo(){
  2. yield 'a';
  3. yield 'b';
  4. }
  5. function* bar(){
  6. yield 'x';
  7. yield* foo();
  8. yield 'y';
  9. }
  10. // "x"
  11. // "a"
  12. // "b"
  13. // "y"

还有例子:

  1. function* gen(){
  2. yield* ["a", "b", "c"];
  3. }
  4. gen().next() // {value: "a", done: false}

因为原生数组本身就支持遍历器,因此会遍历数组成员,但如果上面的不加*号,就会返回整个数组。

字符串也一样,因为是原生支持遍历器,所以也能使用这个加了*号的表达式:

  1. function* read(){
  2. yield 'hello';
  3. yield* 'hello';
  4. }
  5. read().next() //"hello"
  6. read().next() //"h"

实际上,数据结构只要有Iterator接口,就可以被yield*遍历。

Generator含义

协程

定义: 协程(coroutine)是一种程序运行方式,可以理解成“协作的线程 / 协作的函数”。协程即可以用单线程实现,也可用多线程实现,前者是一种特殊的子例程,后者是一种特殊的线程。

  • 子例程:

传统的子例程采用堆叠式“后进先出”的执行方式,只有当子函数完全执行完毕,才会结束执行父函数。

  • 普通的线程:

有自己的上下文,可以在同一个时间内有多个线程处于运行状态。

  • 协程与子例程的差异:

多个线程的情况情况下(单线程的情况下,即多个函数)可以并行执行,但只有一个线程(或函数)处于正运行的状态,其他的线程(或函数)都处于暂停状态,而且线程之间可以交换执行权,意思是一个线程可以执行到一半,然后暂停,并把执行权交给另一个线程执行,等稍后执行回收执行权的时候,再恢复执行。这种并行执行、交换执行权的线程,就称为协程。

  • 协程与普通线程的差异:

虽然可以在同一个时间有许多线程同时处于运行状态,但是运行的协程只能有一个,其他的协程都处于暂停状态。并且普通的线程都是抢先式的,哪个线程得到资源,必须由运行环境决定。而协程是合作式的,执行权由协程自己分配。

在ES6里,Generator函数就是ES6对协程的实现,不过并不是完全的实现。Generator函数被称为“半协程”,意思是说只有Generator函数的调用者,才可以把执行权还给Generator函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。

堆栈

JS代码运行的时候,会产生一个全局的上下文,也就是堆栈,然后在这个上下里面如果又一个块级代码或执行函数,又创建一个上下文。由此就形成了个“后进先出”的堆栈数据结果。但Generator函数不是这样的,虽然执行的时候也会创建上下文,但一旦遇到了yield命令,就会暂时退出堆栈,里面所有变量和对象会冻结当前状态,等调用了next命令时,这个上下文环境就会重新加入调用栈,恢复执行。

应用操作

因此,Generator可以暂停函数执行,返回任意表达式的值。可应用很多场景。

异步操作

可以把异步操作写在yield表达式里面,等调用next方法时再往后执行。如下例子:

  1. function* loadUI(){
  2. showLoadingScreen();
  3. yield loadUIDataAsynchronously();
  4. hideLoadingScreen();
  5. }
  6. let loader = loadUI();
  7. loader.next() //加载UI
  8. loader.next() //卸载UI

上面的代码是所有Loading界面的逻辑,被封装在一个函数,按部就班的执行!