ES6生成器(generator)讓一種順序、看似同步的異步流程控制表達風格成為可能。
生成器
生成器是一類特殊的函數(shù),可以一次或多次啟動和停止,并不一定非得要完成。
- 生成器本身也是一個函數(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(...)返回的值。
- 生成器提供內建消息輸入輸出能力,通過
yield和next()實現(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。
一般來講,需要的
next(...)調用要比yield語句多一個。
??? ?因為第一個next(...)總是啟動一個生成器,并運行到第一個yield處。是第二個next(...)調用完成第一個被暫停的yield表達式。每次構建一個迭代器,實際上就隱式構建了生成器的一個實例,通過這個迭代器來控制的是這個生成器的實例。
同一個生成器的多個實例可以同時運行,它們甚至可以彼此交互。
迭代器
?? 場景:假定要生成一系列值,其中每個值都與前面一個有特定都關系。要實現(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)