在JS的使用场景中,异步操做的处理是一个不可回避的问题,若是不作任何抽象、组织,只是“跟着感受走”,那么面对“按顺序发起3个ajax请求”的需求,很容易就能写出以下代码(假设已引入jQuery):javascript
// 第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){
}
}
});
}
}
});
}
}
});
|
当顺序执行的异步操做愈来愈多的时候,回调层级也就越多,这也就是传说中的“回调恶魔金字塔”。java
所谓“生成器”,实际上是一个函数,可是这个函数的行为会比较特殊:程序员
生成器的语法和普通函数相似,特殊之处在于:ajax
function
后面多了一个*
,并且这个*
先后容许有空白字符yield
运算符举个粟子:json
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();
|
接下来依次执行:promise
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}
|
这个例子反映了生成器的基本用法,有如下几点值得注意:app
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()
方法,它才会执行一次,不调用则不执行,因此不用担忧死循环的问题。koa
如何用生成器解决异步场景下的“回调恶魔金字塔”呢?满心期待对吧,很遗憾,它并不能那么简单地解决……异步
从前面的例子中,其实已经能够体会出来了,生成器的用法中并不包含对异步的处理,因此其实没有办法帮助咱们对异步回调进行封闭。那么为何你们将它视为解决回调嵌套的神器呢?在翻阅了很多资料后找到这篇文章,文章做者一开始也认为生成器并不能解决回调嵌套的问题,但下面本身作了解释,若是生成器的返回的是一系列的Promise对象的话,状况就会不同了,举个粟子:
function myAjax(){
return fetch('http://echo.113.im?data=1');
}
|
咱们使用window.fetch
方法来处理ajax请求,这个方法会返回一个Promise对象。而后,咱们使用一个生成器来包装这个操做:
function * MyLogic(){
var serverData = 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 = 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 = 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 = 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生成器这一特性能迅速引发如此多轰动的真正缘由。