學習ES6生成器(Generator)

背景

在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

生成器的盧山真面目

所謂「生成器」,實際上是一個函數,可是這個函數的行爲會比較特殊:程序員

  1. 它並不直接執行邏輯,而是用來生成另外一個對象(這也正是「生成器」的含義)
  2. 它所生成的對象中的函數能夠把邏輯拆開來,一片一片調用執行,而不是像普通的函數,只能從頭至尾一次執行完畢

生成器的語法和普通函數相似,特殊之處在於:ajax

  1. 字面量(函數聲明/函數表達式)的關鍵字function後面多了一個*,並且這個*先後容許有空白字符
  2. 函數體中多了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

  1. 在調用GenA()時,函數體中的邏輯並不會執行(控制檯沒有輸出),直接調用a.next()時纔會執行
  2. a是一個對象,它由生成器GenA()調用而來,注意GenA()並無返回a對象,這很是像構造函數的執行形式,可是不容許添加new
  3. 調用a.next()時,函數體中的邏輯纔開始真正執行,每次調用時會到yield語句結束,並將yield的運算數做爲結果返回
  4. a.next()返回的結果是一個對象,對yield的運算數作了包裝,並帶上了done屬性
  5. done屬性爲false時,表示該函數邏輯還未執行完,能夠調用a.next()繼續執行
  6. 最後一次返回的結果爲return語句返回的結果,且done值爲true。若是不寫return,則值爲undefined
  7. value3 = yield 2這句是指,這一段邏輯返回2,在下一次調用a.next()時,將參數賦給value3。換句話說,這句只執行了後面半段就暫停了,等到再次調用a.next()時纔會將參數賦給value3並繼續執行下面的邏輯
  8. 返回值中donetrue時,仍然能夠繼續調用,返回的值爲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()函數中,並無使用回調,就完成了異步操做。

這裏有幾個值得注意的點:

  1. myAjax()函數返回的是一個Promise對象
  2. myLogic中的第一個語句,返回給外界的是myAjax()返回的Promise對象,等外界再次調用next()方法時將數據傳進來,賦值給serverDate
  3. 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(…)

可見MyLogicyield後的語句的確被執行了,可是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生成器這一特性能迅速引發如此多轟動的真正緣由。

相關文章
相關標籤/搜索