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函数将异步操作表达很简洁,但流程管理不便。

浙公网安备 33010602011771号