很多人初接觸es6的Generator的時候可能會覺得云里霧里繞得慌挖息,本文從多種角度詳細解說了其基礎語法和使用方式金拒,希望看完的你能從霧中走出來~
主要有以下內容:
1.相關概念
2.消息傳遞
3.Generator在流程控制中的應用
4.Generator+Promise實現(xiàn)完美異步
5.async和await
6.yield委托
1.相關概念
1)為什么要引入Generator?
眾所周知套腹,傳統(tǒng)的JavaScript異步的實現(xiàn)是通過回調函數(shù)來實現(xiàn)的绪抛,但是這種方式有兩個明顯的缺陷:
- 缺乏可信任性。例如我們發(fā)起ajax請求的時候是把回調函數(shù)交給第三方進行處理电禀,期待它能執(zhí)行我們的回調函數(shù)睦疫,實現(xiàn)正確的功能
- 缺乏順序性。眾多回調函數(shù)嵌套使用鞭呕,執(zhí)行的順序不符合我們大腦常規(guī)的思維邏輯,回調邏輯嵌套比較深的話調試代碼時可能會難以定位。
Promise恢復了異步回調的可信任性葫松,具體參見(欸欸這個往后放)瓦糕,而Generator正是以一種看似順序、同步的方式實現(xiàn)了異步控制流程腋么,增強了代碼可讀性咕娄。
2)概念:
- Generator(生成器)是一類特殊的函數(shù),跟普通函數(shù)聲明時的區(qū)別是加了一個*號珊擂,以下兩種方式都可以得到一個生成器函數(shù):
// 聲明方式一(個人比較偏向這種風格啦)
function *main() {
// do something……
}
// 聲明方式二
function* main() {
// do something
}
- Iterator(迭代器):當我們實例化一個生成器函數(shù)之后圣勒,這個實例就是一個迭代器〈萆龋可以通過next()方法去啟動生成器以及控制生成器的是否往下執(zhí)行圣贸。
-
yield/next:這是控制代碼執(zhí)行順序的一對好基友。
通過yield語句可以在生成器函數(shù)內部暫停代碼的執(zhí)行使其掛起扛稽,此時生成器函數(shù)仍然是運行并且是活躍的吁峻,其內部資源都會保留下來,只不過是處在暫停狀態(tài)在张。
在迭代器上調用next()方法可以使代碼從暫停的位置開始繼續(xù)往下執(zhí)行用含。
// 首先聲明一個生成器函數(shù)
function *main() {
console.log('starting *main()');
yiled; // 打住,不許往下走了
console.log('continue yield 1');
yield; // 打住帮匾,又不許往下走了
console.log('continue yield 2');
}
// 構造處一個迭代器it
let it = main();
// 調用next()啟動*main生成器啄骇,表示從當前位置開始運行,停在下一個yield處
it.next(); // 輸出 starting *main()
// 繼續(xù)往下走
it.next(); // 輸出 continue yield 1
// 再繼續(xù)往下走
it.next(); // 輸出 continue yield 2
以上是一個非常簡單的yield/next相互配合控制代碼執(zhí)行的例子瘟斜,認真看的同學可能會產生一個疑問:
next()居然比yield多了一個缸夹??哼转?
沒錯明未,就是這樣的,因為let it = main(); 進行實例化之后壹蔓,main()里的代碼不會主動執(zhí)行趟妥。第一個next()永遠是用于啟動生成器,生成器啟動后要想運行到最后佣蓉,其內部的每個yield都會對應一個next()披摄,所以說next()永遠都會比yield多一個了~~
2.消息傳遞
生成器的作用之一是消息傳遞。通過yield ...和next(...)組合使用勇凭,可以在生成器的執(zhí)行過程中構成一個雙向消息傳遞系統(tǒng)疚膊。
當next(..)執(zhí)行到y(tǒng)ield語句處時會暫停生成器的執(zhí)行,同時next(...)會得到一個帶有value屬性的對象虾标,yield語句后面帶的值會賦給value(如果yield后面沒有值寓盗,value就為undefined)。可以將yield ...效果看成跟return ...類似傀蚌。
當生成器處于暫停狀態(tài)時基显,暫停的yield表達式處可以接收下一個啟動它的next(...)傳進來的值。當next(...)使生成器繼續(xù)往下執(zhí)行時善炫,其傳入的值會將原來的yield語句替換掉撩幽。
看個栗子:
function *main() {
let x = yield "starting";
let y = yield (x * 2);
console.log(x, y);
return x + y;
}
let it = main();
let res = it.next(); // 第一個next()用于啟動生成器
console.log(res.value); // 輸出"starting" (yield語句后跟的值傳給了next()的對象)
res = it.next(5); // 向等待的第一個yield傳入值5,*main()中的 x 被賦值為5
console.log(res.value); // 輸出10 (x * 2得到了10傳給next(5)運行后的對象)
res = it.next(20); // 向等待的第二個yield傳入值20箩艺, *main()中的x被賦值為20
// 輸出5 20 (執(zhí)行后面的console.log(x, y)語句分別輸出x,y的值)
console.log(res.value); // 輸出25 (return ...的值傳給了next(20)運行后的對象)
注意:
- 第一個next()僅僅是用于啟動生成器用的窜醉,并不會傳入任何東西,如果傳入了參數(shù)也會被自動忽略掉艺谆。
- yield ...在值傳遞方面的作用相當于return ...榨惰,你也可以把它當做一個return語句來看待,如果yield后面不加參數(shù)擂涛,則默認yield undefined;
- 最后一個next()執(zhí)行完畢之后读串,得到的值是*main()函數(shù)return出來的值,如果函數(shù)沒有自己加return語句撒妈,一樣也會默認return undefined;
- next()執(zhí)行完畢后會返回一個對象恢暖,屬性值有兩個,分別為value(從yield或return處拿到的值)和done(boolean值狰右,標識生成器是否執(zhí)行完畢)杰捂。
接下來嘗試一下異常傳值的情況:
function *main() {
let x = yield "starting";
let y = yield (x * 2);
console.log(x, y);
}
let it = main();
let res = it.next('1111'); // '1111'被丟棄啦~~
console.log(res.value); // 輸出"starting"
res = it.next(); // 不給yield傳值 x成了undefined
console.log(res.value); // 輸出NaN (undefined * 2得到了NaN傳給next()運行后的對象)
res = it.next(); // 不給yield傳值 y未拿到值
// 輸出undefined undefined
console.log(res.value); // 輸出undefined (默認return undefined;)
3.Generator在流程控制中的應用##
基礎概念說完了,那么棋蚌,Generator是如何解決傳統(tǒng)回調中存在的缺乏順序性問題的呢嫁佳?首先來看下面一個使用傳統(tǒng)回調函數(shù)實現(xiàn)異步的例子:
function getCallSettings() {
// utils.ajax方法用于發(fā)起ajax請求
utils.ajax({
url: '/dialer/dialerSetting',
method: "GET",
success: (res) => {
let settingInfo = res.dialerSetting;
dealData(settingInfo);
},
error: (err) => {
console.log(err);
}
});
}
function dealData(data) {
// do something……
}
getCallSettings();
可以看出,dealData只能在ajax請求拿到數(shù)據(jù)之后才能運行谷暮,所以需要嵌套在success回調中執(zhí)行蒿往。以上例子嵌套的不深,依賴settingInfo的地方也不多湿弦,只有一個dealData函數(shù)瓤漏,所以看起來還好,但是颊埃,試想一下如果接下來的很多其他請求都依賴于該請求返回的數(shù)據(jù)蔬充,或者很多代碼邏輯都需要拿到settingInfo之后才能進行,那么代碼可讀性就會差很多了班利。
所以饥漫,接下來嘗試以生成器的方式實現(xiàn)以上場景:
function getCallSettings() {
utils.ajax({
url: '/dialer/dialerSetting',
method: "GET",
success: (res) => {
it.next(res.dialerSetting); // 將res.dialerSetting傳給yield表達式
},
error: (err) => {
it.throw(err); // 拋出錯誤
}
});
}
function *dealData() {
try{
let settingInfo = yield getCallSettings();
// do something……
}
catch(err) {
console.log(err); // 接收錯誤
}
}
let it = dealData();
it.next(); // 啟動生成器
此處的yield是用于在異步流程中暫停阻塞代碼,當然罗标,它阻塞的只有生成器里面的代碼庸队,生成器外部的絲毫不受影響积蜻。let settingInfo = yield getCallSettings();中,通過yield把異步的流程完全抽離出去彻消,實現(xiàn)了看似順序同步的代碼浅侨,這無疑是巨大的改進。
4.Generator+Promise實現(xiàn)完美異步##
1.如果將Generator和Promise結合在一起使用证膨,既讓代碼看起來順序同步,又恢復了可信任性鼓黔,可以說是非常完美的了央勒。
接下來就把以上例子改成Generator + Promise實現(xiàn):
function getCallSettings() {
// utils.ajax方法支持返回promise對象,把得到的promise return出去
return utils.ajax({
url: '/dialer/dialerSetting',
method: "GET",
});
}
function *dealData() {
try {
let settingInfo = yield getCallSettings();
// do something……
}
catch(err) {
console.log(err); // 接收錯誤
}
}
let it = dealData();
let promise = it.next().value; // 注意澳化,這里拿到y(tǒng)ield出來的promise
promise.then(
(info) => {
it.next(info); // 拿到info傳給yield表達式
},
(err) => {
it.throw(err); // 拋出錯誤
}
);
2.這種方式的另一個好處在于崔步,當多個Promise并發(fā)請求時,正確的寫法可以更好地提高性能缎谷。
例如以下場景:
// 滿屏都是代碼井濒,這里代碼盡量精簡些啦
function *dealData() {
let r1 = yield utils.ajax(reqUrl1); // 請求1獲取到 r1
let r2 = yield utils.ajax(reqUrl2); // 請求2獲取到 r2
let reqUrl3 = getUrl(reqUrl1, reqUrl2); // 請求3需要的url依賴于前面兩個請求
let r3 = yield utils.ajax(reqUrl3);
// do something……
}
以上寫法中,生成器執(zhí)行時會先發(fā)出請求1列林,請求1返回后才會發(fā)出請求2瑞你,請求2返回之后,再發(fā)出請求3希痴。其實在這里請求1和2之間不存在依賴關系者甲,是可以同時進行的。所以還可以用一種效率更高的寫法:
// 滿屏都是代碼砌创,這里代碼也盡量精簡些啦
function *dealData() {
let p1 = utils.ajax(reqUrl1); // 請求1獲取到 r1
let p2 = utils.ajax(reqUrl2); // 請求2獲取到 r2
let r1 = yield p1;
let r2 = yield p2;
let reqUrl3 = getUrl(reqUrl1, reqUrl2); // 請求3需要的url依賴于前面兩個請求
let r3 = yield utils.ajax(reqUrl3);
// do something……
}
這樣p1和p2之間就可以同時進行虏缸,不會相互阻塞啦~~是不是很棒棒呢
5.async和await##
以上yield + Promise的寫法需要我們對拿到的promise的決議進行人工處理(區(qū)分成功或失敗),在ES7中提供了async/await幫我們省掉了這個步驟:async/await組合的出現(xiàn)使得異步的世界更加完美啦~~
下面改寫一下代碼實現(xiàn)形式:
function getCallSettings() {
return utils.ajax({
url: '/dialer/dialerSetting',
method: "GET",
});
}
async function dealData() {
try {
let settingInfo = await getCallSettings(); // await會暫停在這嫩实,直到promise決議(請求返回)
// do something……
}
catch(err) {
console.log(err);
}
}
dealData();
dealData函數(shù)不需要再被聲明為生成器函數(shù)刽辙,而是聲明為async函數(shù);
同時甲献,其內部也不用yield出一個promise宰缤,而是用await進行等待,直到promise決議竟纳。
那么async/await的寫法和yield相比孰優(yōu)孰劣呢撵溃?
其實個人感覺兩者都有自己獨到的長處,沒有優(yōu)劣之分(純屬個人見解锥累,不喜勿噴)
- async/await在處理promise的層面上省略了對決議的人工處理缘挑,讓代碼量得以減少,語義上也更容易理解桶略。
- yield包容性更廣泛语淘,async只能接口promise诲宇,yield除此之外還能接收字符串、數(shù)組惶翻、對象等各種類型的數(shù)據(jù)姑蓝。
6.yield 委托##
為什么要用委托呢?因為吕粗,一個代碼組織合理的程序中纺荧,出于功能模塊化等原因,我們很可能在一個生成器中調用另外一個生成器颅筋,比如在a()中調用b()宙暇,但是通常情況下a()的實例中是無法使用next方法對b()內部進行操控的,所以這個時候我們就可以使用yield將b()委托給a()议泵。
function *a() {
console.log('a start');
yield 2;
console.log('a end');
}
function *b() {
console.log('b start');
yield 1;
yield *a(); // 將a委托給b
yield 3;
console.log('b end');
}
let it = b();
it.next().value; // b start
// 1
it.next().value; // a start
// 2
it.next().value; // a end
// 3
it.next().value; // b end
合理地進行生成器分離和使用委托占贫,可以使代碼可讀性更強,更易維護~~~~~