??【異步】7. 生成器 & 迭代器 & for..of

ES6生成器(generator)讓一種順序、看似同步的異步流程控制表達風格成為可能。

生成器

生成器是一類特殊的函數(shù),可以一次或多次啟動和停止,并不一定非得要完成。

  1. 生成器本身也是一個函數(shù),因此它可以接受參數(shù),也能夠返回值。
function *foo(x, y){
  return x * y;
}

// 構造一個迭代器it來控制這個生成器
var it = foo(6,7);
var res = it.next();
console.log(res); //{ value: 42, done: true }

生成器和普通函數(shù)在調用上的一個區(qū)別: foo(6,7),生成器 *foo(...) 并沒有像普通函數(shù)一樣實際運行。
事實上,我們只是創(chuàng)建了一個迭代器對象,把它賦給變量it,用于控制生成器*foo(...)。調用it.next(),指示生成器從當前位置開始繼續(xù)運行,停在下一個yield處或直到生成器結束。
next(...) 調用的結果是一個對象,它有一個value屬性,持有從*foo(...)返回的值。

  1. 生成器提供內建消息輸入輸出能力,通過yieldnext()實現(xiàn)。
function *foo(x){
  var y = x * (yield);
  return y;
}
var it = foo(6); // 傳入6作為參數(shù)x
it.next(); // 啟動 *foo(...)

var res = it.next(7);
console.log(res); //{ value: 42, done: true }

第一次調用it.next();時,在*foo(...)內部開始執(zhí)行語句var y = x ..,隨后遇見yield表達式。它會在這一點上暫停*foo(...),并在本質上要求調用代碼為yield表達式提供一個結果值
調用it.next(7) 將值7作為被暫停的yield表達式的結果。所以,這時賦值語句實際上就是var y = 6 * 7。

  1. 一般來講,需要的next(...)調用要比yield語句多一個。
    ??? ?因為第一個next(...)總是啟動一個生成器,并運行到第一個yield處。是第二個next(...)調用完成第一個被暫停的yield表達式。

  2. 每次構建一個迭代器,實際上就隱式構建了生成器的一個實例,通過這個迭代器來控制的是這個生成器的實例。
    同一個生成器的多個實例可以同時運行,它們甚至可以彼此交互。

迭代器

?? 場景:假定要生成一系列值,其中每個值都與前面一個有特定都關系。要實現(xiàn)這一點,就需要一個有狀態(tài)的生產者能夠記住其生成的最后一個值!
?? 1. 直接使用函數(shù)閉包實現(xiàn):

var gimmeSomething = (function(){
  var nextVal;

  return function(){
    if(nextVal === undefined){
      nextVal = 1;
    }else{
      nextVal = (3 * nextVal) + 6;
    }
    return nextVal;
  }
})();
console.log(gimmeSomething()); //1
console.log(gimmeSomething()); //9
console.log(gimmeSomething()); //33
console.log(gimmeSomething()); //105

?? 2. 通過迭代器來解決
迭代器是一個定義良好的接口,用于從一個生產者一步步得到一系列值,每次想要從生成者得到下一個值的時候就調用next()。每次調用 next() 都會返回一個結果對象,該結果對象有兩個屬性,value 表示當前的值,done 表示遍歷是否結束。

var something = (function(){
  var nextVal;

  return {
    [Symbol.iterator]: function(){ return this; }, //for..of循環(huán)需要
    next: function(){ //標準迭代器接口方法
      if(nextVal === undefined){
        nextVal = 1;
      }else{
        nextVal = (3 * nextVal) + 6;
      }
      return { done: false, value: nextVal };
      //done標識迭代器的完成狀態(tài),value放置迭代值
    }
  }
})();
console.log(something.next().value); //1
console.log(something.next().value); //9
console.log(something.next().value); //33
console.log(something.next().value); //105

for(var v of something){
  console.log(v);
  if(v > 500){ //避免死循環(huán)
    break;
  }
} // 321 969

for..of

ES6 新增了一個 for..of 循環(huán),可以通過原生循環(huán)語法自動迭代標準迭代器。因為我們的迭代器 something 總是返回 done: false,因此這個for..of循環(huán)將永遠運行下去,為避免死循環(huán)放了一個 break。
for..of 循環(huán)在每次迭代中自動調用 next() ,它不會向next() 傳入任何值,并且會在接收到 done:true 之后自動停止。
?? 除了構造自己的迭代器,許多JavaScript的內建數(shù)據結構(從ES6開始)都默認部署了Symbol.iterator屬性(即默認迭代器),比如

  • 數(shù)組
  • Set
  • Map
  • 類數(shù)組對象,如 arguments 對象、DOM NodeList 對象
  • Generator對象
  • 字符串
for(var v of [1,2,3,4,5]){
  console.log(v)
}
// 1,2,3,4,5

?? 一般的 object 沒有像 array 一樣有默認的迭代器。

iterable

iterable 可迭代,指一個包含可以在其值上迭代的迭代器的對象。

?? ES6 規(guī)定,默認的 Iterator 接口部署在數(shù)據結構的 Symbol.iterator 屬性,或者說,一個數(shù)據結構只要具有 Symbol.iterator 屬性,就可以認為是"可遍歷的"(iterable)。

從ES6開始,從一個iterable中提取迭代器的方法是:iterable 必須支持一個函數(shù),其名稱是專門的ES6符號值 Symbol.iterator 。調用這個函數(shù)時,它會返回一個迭代器。通常每次調用都會返回一個全新的迭代器。for..of 遍歷的其實是對象的 Symbol.iterator 屬性。

異步迭代生成器

?? 用生成器來表達異步任務流程控制:

function foo(x, y) {
  ajax(`http://some.url.1?x=${x}&y=${y}`, function(err, data){
    if(err){
      it.throw(err);
    }else{
      it.next(data);
    }
  });
}
function *main(){
  try {
    var text = yield foo(11, 12);
    console.log(text);
  } catch (error) {
    console.log(error);
  }
}

var it = main();
it.next();

回想使用回調的時候,下面代碼幾乎不能實現(xiàn)!

var data = ajax("...url 1 ...");
console.log(data);

二者區(qū)別在于生成器中使用了 yield,這一點使得我們看似阻塞同步的代碼,實際上并不會阻塞整個程序,它只是暫?;蜃枞松善鞅旧淼拇a。
錯誤處理:
??? Q:生成器*main內部的try..catch是如何工作的呢?調用foo(..)是異步完成的,try..catch 不是無法捕獲異步錯誤嗎?
?? A:yield 讓賦值語句暫停來等待 foo(..) 完成,使得響應完成后可以被賦給text。yield的暫停也使得生成器能夠捕獲錯誤。
生成器yield暫停的特性意味著我們不僅能夠從異步函數(shù)調用得到看似同步的返回值,還可以同步捕獲來自這些異步函數(shù)調用的錯誤!

生成器 + Promise 協(xié)作運作模式

ES6中最完美的世界就是生成器(看似同步的異步代碼)和 Promise(可信任可組合)的結合。
?? Ajax調用返回一個promise,再外面包一層通過生成器將它yield出來,然后迭代器控制代碼就可以接收到這個promise了。迭代器偵聽promise的決議(完成或拒絕),然后要么使用完成消息恢復生成器運行,要么向生成器拋出一個帶拒絕原因的錯誤。

function foo(x,y){
  return request(`http://some.url.1?x=${x}&y=${y}`)
}
function *main(){
  try {
    var text = yield foo(11, 31);
    console.log(text);
  } catch (error) {
    console.log(error);
  }
}

//運行
var it = main();
var p = it.next().value;
// 等待promise決議
p.then(function(text){
  it.next(text);
},function(err){
  it.throw(err);
})

async/await

function foo(x,y){
  return request(`http://some.url.1?x=${x}&y=${y}`)
}
async function main(){
  try {
    var text = await foo(11, 31);
    console.log(text);
  } catch (error) {
    console.error(error);
  }
}
main();

可以看到,main()不再被聲明為生成器函數(shù)了,它現(xiàn)在是一類新的函數(shù),async函數(shù);我們不再yield出Promise,而是用await等待它決議。
如果你await一個Promise,async函數(shù)就會自動獲知要做什么,它會暫停這個函數(shù)(就像生成器一樣),知道Promise決議。
調用一個像main()這樣的async函數(shù)會自動返回一個Promise。在函數(shù)完全結束之后,這個promise會決議。

其他

ES6 系列之 Generator 的自動執(zhí)行
ES6 系列之我們來聊聊 Async
ES6系列之異步處理實戰(zhàn)

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容