ES6异步方案——生成器Generators/yield
一、Generator函数简介
generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。
ES6定义generator标准时借鉴了Python的generator的概念和语法。
1、理解Generator函数
Generator函数有多种理解角度。
function* gen() { yield 1; yield 2; yield 3; return 'ending'; } let g = gen(); // "Generator { }"
上面代码定义了一个Generator函数 gen,它内部有三个yield 表达式,即该函数有四个状态:1,2,3和return语句(结束执行)。
(1)语法
首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象。
也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
(2)形式
Generator 函数是一个普通函数,但是有两个特征。
一是,function
关键字与函数名之间有一个星号;
二是,函数体内部使用yield
表达式,定义不同的内部状态(yield
在英语里的意思就是“产出”)。
2、next方法
Generator.prototype.next()方法用于恢复执行,返回值是包含value和done属性的对象。
调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。
因此必须调用遍历器对象的next方法,使得指针移动向下一个状态。
每次调用next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield
表达式(或return
语句)为止。
换言之,Generator 函数是分段执行的,yield
表达式是暂停执行的标记,而next
方法可以恢复执行。
function* gen() { yield 1; yield 2; yield 3; return 'ending'; } let g = gen(); // "Generator { }" console.log(g.next()); // { value: 1, done: false } console.log(g.next()); // { value: 2, done: false } console.log(g.next()); // { value: 3, done: false } console.log(g.next()); // { value: 'ending', done: true }
上面的代码中调用了四次next方法。第一次调用,Gererator函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,value属性是当前yield表达式的值,done属性值为false,表示遍历未结束。
第二次、第三次调用则是从上次yield表达式停止的地方开始,一直执行到下一个yield表达式。
第四次调用,此时 Generator 函数已经运行完毕,next
方法返回对象的value
属性为undefined
,done
属性为true
。以后再调用next
方法,返回的都是这个值。
3、return方法
Generator.prototype.return()方法用于立即结束遍历,并返回给定的值。
function* gen() { yield 1; yield 2; yield 3; } var g = gen(); console.log(g.next()); // { value: 1, done: false } console.log(g.return('foo')); // { value: "foo", done: true } console.log(g.next()); // { value: undefined, done: true }
Generator函数的返回值是遍历器对象,但可以用return方法指定返回的值。参数就是返回值的value属性。使用return方法后,done属性将设为true,立即终结遍历Generator函数。
4、Generator调用
调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next
方法,就会返回一个有着value
和done
两个属性的对象。value
属性表示当前的内部状态的值,是yield
表达式后面那个表达式的值;done
属性是一个布尔值,表示是否遍历结束。
ES6 没有规定,function
关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
function * foo(x, y) { ··· } function *foo(x, y) { ··· } function* foo(x, y) { ··· } function*foo(x, y) { ··· }
由于 Generator 函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在function
关键字后面。
5、yield表达式
Generator 函数返回的遍历器对象,只有调用next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停标志。
(1)next 方法运行逻辑
1)遇到 yield 表达式,则暂停执行后面的操作,并紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值。
2)下次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式。
3)若没有再遇到新的 yield 表达式,则一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值。
4)若该函数没有 return 语句,则返回的对象的 value 属性值为 undefined。
(2)yield 与 return 对比
yield
表达式与return
语句既有相似之处,也有区别。
相似处:都能返回紧跟在语句后面的那个表达式的值。
不同处:
- 每次遇到
yield
,函数暂停执行,下一次再从该位置继续向后执行;return
语句不具备位置记忆的功能 - 一个函数里面,可以执行多次(或者说多个)
yield
表达式;只能执行一次(或者说一个)return
语句 - Generator 函数可以返回一系列的值,因为可以有任意多个
yield;
正常函数只能返回一个值,因为只能执行一次return
二、Generator函数异步应用
由于Generator函数可以交出函数的执行权,整个Generator函数可以看作一个封装的异步任务,或者说是异步任务的容器。
异步操作需要暂停的地方,都用 yield 语句注明。Generator 函数的执行方法如下:
function* gen(x) { var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next() // { value: undefined, done: true }
调用函数,返回一个内部指针(遍历器)g。
1、Generator数据交换和错误处理
Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
(1)next实现函数体内外数据交换
next
返回值的 value 属性,是 Generator 函数向外输出数据;next
方法还可以接受参数,向 Generator 函数体内输入数据。
function* gen(x){ var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next(2) // { value: 2, done: true }
第一个next
方法的value
属性,返回表达式x + 2
的值3
。
第二个next
方法带有参数2
,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量y
接收。因此,这一步的value
属性,返回的就是2
(变量y
的值)。
(2)函数内部错误处理
Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。
function* gen(x){ try { var y = yield x + 2; } catch (e){ console.log(e); } return y; } var g = gen(1); g.next(); g.throw('出错了');
Generator 函数体外,使用指针对象的throw
方法抛出的错误,可以被函数体内的try...catch
代码块捕获。
这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
2、异步任务封装
var fetch = require('node-fetch'); function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio); }
上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。
这段代码非常像同步操作,除了加上了yield
命令。
执行这段代码:
var g = gen(); var result = g.next(); result.value.then(function(data){ return data.json(); }).then(function(data){ g.next(data); });
首先执行 Generator 函数,获取遍历器对象,然后使用next
方法(第二行),执行异步任务的第一阶段。由于Fetch
模块返回的是一个 Promise 对象,因此要用then
方法调用下一个next
方法。
由此可见,虽然Generator函数将异步操作表达很简洁,但流程管理不便。