在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生成器這一特性能迅速引發如此多轟動的真正緣由。