首页 > 代码库 > 学习ES6生成器(Generator)
学习ES6生成器(Generator)
背景
在JS的使用场景中,异步操作的处理是一个不可回避的问题,如果不做任何抽象、组织,只是“跟着感觉走”,那么面对“按顺序发起3个ajax请求”的需求,很容易就能写出如下代码(假设已引入jQuery):
// 第1个ajax请求 $.ajax({ url:‘http://echo.113.im‘, dateType:‘json‘, type:‘get‘, data:{ data:JSON.stringify({status:1,data:‘hello world‘}), type:‘json‘, timeout:1000 }, success:function(data){ if(data.status === 1){ // 第2个ajax请求 $.ajax({ ......此处省略500字 success:function(data){ if(data.status === 1){ // 第3个ajax请求 $.ajax({ ......此处省略500字 success:function(data){ if(data.status === 1){ } } }); } } }); } } }); |
当顺序执行的异步操作越来越多的时候,回调层级也就越多,这也就是传说中的“回调恶魔金字塔”。
生成器的卢山真面目
所谓“生成器”,其实是一个函数,但是这个函数的行为会比较特殊:
- 它并不直接执行逻辑,而是用来生成另一个对象(这也正是“生成器”的含义)
- 它所生成的对象中的函数可以把逻辑拆开来,一片一片调用执行,而不是像普通的函数,只能从头到尾一次执行完毕
生成器的语法和普通函数类似,特殊之处在于:
- 字面量(函数声明/函数表达式)的关键字
function
后面多了一个*
,而且这个*
前后允许有空白字符 - 函数体中多了
yield
运算符
举个粟子:
function * GenA(){ console.log(‘from GenA, first.‘); yield 1; console.log(‘from GenA, second.‘); var value3 = yield 2; console.log(‘from GenA, third.‘,value3); return 3; } var a = GenA(); |
接下来依次执行:
a.next(); // from GenA, first. // Object {value:1,done:false} a.next(); // from GenA, second. // Object {value:2,done:false} a.next(333); // from GenA, third. // 333 // Object {value:3,done:true} a.next(); // Object {value:undefined,done:true} |
这个例子反映了生成器的基本用法,有以下几点值得注意:
- 在调用
GenA()
时,函数体中的逻辑并不会执行(控制台没有输出),直接调用a.next()
时才会执行 a
是一个对象,它由生成器GenA()
调用而来,注意GenA()
并没有返回a
对象,这非常像构造函数的执行形式,但是不允许添加new
- 调用
a.next()
时,函数体中的逻辑才开始真正执行,每次调用时会到yield
语句结束,并将yield
的运算数作为结果返回 a.next()
返回的结果是一个对象,对yield
的运算数做了包装,并带上了done
属性- 当
done
属性为false
时,表示该函数逻辑还未执行完,可以调用a.next()
继续执行 - 最后一次返回的结果为
return
语句返回的结果,且done
值为true
。如果不写return
,则值为undefined
value3 = yield 2
这句是指,这一段逻辑返回2,在下一次调用a.next()
时,将参数赋给value3。换句话说,这句只执行了后面半段就暂停了,等到再次调用a.next()
时才会将参数赋给value3并继续执行下面的逻辑- 返回值中
done
为true
时,仍然可以继续调用,返回的值为undefined
同步场景下生成器的使用
来看看同步场景下,如何使用生成器:
function * Square(){ for(var i=1;;i++){ yield i*i; } } var square = Square(); square.next(); // 1 square.next(); // 4 square.next(); // 9 ...... |
同步场景下大概就是这么用的,很无趣是吧?我也这么觉得,其实和直接函数调用差别不大。不过值得注意的是,我们在循环中并没有设中止条件,因为调用一个square.next()
方法,它才会执行一次,不调用则不执行,所以不用担心死循环的问题。
异步场景下的生成器使用
如何用生成器解决异步场景下的“回调恶魔金字塔”呢?满心期待对吧,很遗憾,它并不能那么简单地解决……
从前面的例子中,其实已经可以体会出来了,生成器的用法中并不包含对异步的处理,所以其实没有办法帮助我们对异步回调进行封闭。那么为什么大家将它视为解决回调嵌套的神器呢?在翻阅了不少资料后找到这篇文章,文章作者一开始也认为生成器并不能解决回调嵌套的问题,但下面自己做了解释,如果生成器的返回的是一系列的Promise对象的话,情况就会不一样了,举个粟子:
function myAjax(){ return fetch(‘http://echo.113.im?data=http://www.mamicode.com/1‘); } |
我们使用window.fetch
方法来处理ajax请求,这个方法会返回一个Promise对象。然后,我们使用一个生成器来包装这个操作:
function * MyLogic(){ var serverData = http://www.mamicode.com/yield myAjax(); console.log(‘MyLogic after myAjax‘); console.log(‘serverStatus:%s‘,serverData.status); } |
使用的时候这样用:
var myLogic = MyLogic(); var promise = myLogic.next().value; promise.then(function(serverData){ myLogic.next(serverData); }); |
可以看到,我们这里的myAjax1()
以及MyLogic()
函数中,并没有使用回调,就完成了异步操作。
这里有几个值得注意的点:
myAjax()
函数返回的是一个Promise对象myLogic
中的第一个语句,返回给外界的是myAjax()
返回的Promise对象,等外界再次调用next()
方法时将数据传进来,赋值给serverDate
promise
的状态是由第三段代码,在外部进行处理,完成的时候调用myLogic.next()
方法并将serverData
再传回MyLogic()
中
你一定会问,下面这个promise.done
不就是回调操作么?Bingo!这正是精华所在!我们来看一下这段代码做了什么:
首先,myLogic.next()
返回了一个Promise对象(promise
),然后,promise.then
中的回调函数所做的事情就是调用myLogic.next()
方法就行了,除了调用next()
方法,其它的什么事情都没有。此时,我们就会想到一个程序员特别喜欢的词,叫“封装”!既然这个回调函数只是调用myLogic.next()
方法,那为什么不把它封装起来?
异步封装
首先,我们保持myAjax()
和MyLogic
定义不变,而将myLogic.next()
放到一个函数来调用,这个函数专门负责调用myLogic.next()
,得到返回的Promise对象,然后在Promise被resolve的时候再次调用myLogic.next()
:
var myLogic = MyLogic(); function genRunner(){ // 调用next()获取promise var yieldValue = http://www.mamicode.com/myLogic.next(); var promise = yieldValue.value; if(promise){ promise.then(function(data){ // promise被resolve的时候再次调用genRunner // 以继续执行MyLogic中后面的逻辑 genRunner(); }); } } |
这样我们就把不停地调用myLogic.next()
和不停地promise.then()
的过程进行了封装。运行genRunner()
跑一下:
MyLogic after myAjax1 Uncaught (in promise) TypeError: Cannot read property ‘status‘ of undefined(…) |
可见MyLogic
在yield
后的语句的确被执行了,但是serverData
却没有值,这是因为我们在调用myLogic.next()
的时候没有把值传回去。稍微修改下代码:
// diff1: genRunner接受参数val function genRunner(val){ // diff2: .next调用时把参数传过去,yield左边可以被赋值 var yieldValue = http://www.mamicode.com/myLogic.next(val); var promise = yieldValue.value; if(promise){ promise.then(function(data){ // diff3: 调用genRunner时传递参数 genRunner(data); }); } } |
这次一切都对了:
MyLogic after myAjax1 serverStatus:200 |
至此我们已经把封装最核心的部分抽离出来了,我们的业务代码MyLogic()
已经是“异步操作,同步写法”,而我们亲眼见证了这一切是怎么办到的。那么接下来?为什么不再封装得更通用一些呢?
var genRunner = function(GenFunc){ return new Promise(function(resolve, reject){ var gen = GenFunc(); var innerRun = function(val){ var val = gen.next(val); // 如果已经跑完了,则resolve if(val.done){ resolve(val.value); return; } // 如果有返回值,则调用`.then` // 否则直接调用下一次innerRun() // 为简单起见,假设有值的时候永远是promise if(val.value){ val.value.then(function(data){ innerRun(data); }); }else{ innerRun(val.value); } } innerRun(); }); }; |
这里我们将刚刚看过的封装改成了innerRun()
,并加上了自动调用。外面再封装了一层genRunner()
,返回一个Promise。在genFunc
全程调用完之后,Promise被resolve。
用起来大约是这样:
genRunner(function*(){ var serverData = http://www.mamicode.com/yield myAjax(); console.log(‘MyLogic after myAjax‘); console.log(‘serverStatus:%s‘,serverData.status); }).then(function(message){ console.log(message); }); |
生活真美好!
最后,以别人文章中的一段koa框架使用代码收尾吧:
var koa = require(‘koa‘), app = koa(); app.use(function *() { // 这是这个例子中最重要的部分,我们进行了一系列异步操作,却没有回调 var city = yield geolocation.getCityAsync(this.req.ip); var forecast = yield weather.getForecastAsync(city); this.body = ‘Today, ‘ + city + ‘ will be ‘ + forecast.temperature + ‘ degrees.‘; }); app.listen(8080); |
眼熟吗?koa就是像我们刚刚做的这样,封装了对生成器返回值的处理和调用next()
方法的细节(这里的app.use()
就像前面的genRunner()
函数),使得我们的逻辑代码看起来是如此简单,这正是koa的伟大之处,也是ES6生成器这一特性能迅速引起如此多轰动的真正原因。
学习ES6生成器(Generator)