Generator詳解

很多人初接觸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)運行后的對象)

注意:

  1. 第一個next()僅僅是用于啟動生成器用的窜醉,并不會傳入任何東西,如果傳入了參數(shù)也會被自動忽略掉艺谆。
  2. yield ...在值傳遞方面的作用相當于return ...榨惰,你也可以把它當做一個return語句來看待,如果yield后面不加參數(shù)擂涛,則默認yield undefined;
  3. 最后一個next()執(zhí)行完畢之后读串,得到的值是*main()函數(shù)return出來的值,如果函數(shù)沒有自己加return語句撒妈,一樣也會默認return undefined;
  4. 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

合理地進行生成器分離和使用委托占贫,可以使代碼可讀性更強,更易維護~~~~~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末先口,一起剝皮案震驚了整個濱河市型奥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌碉京,老刑警劉巖厢汹,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異收夸,居然都是意外死亡坑匠,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門卧惜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厘灼,“玉大人,你說我怎么就攤上這事咽瓷∩璋迹” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵茅姜,是天一觀的道長闪朱。 經常有香客問我,道長钻洒,這世上最難降的妖魔是什么奋姿? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮素标,結果婚禮上称诗,老公的妹妹穿的比我還像新娘。我一直安慰自己头遭,他們只是感情好寓免,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布癣诱。 她就那樣靜靜地躺著,像睡著了一般袜香。 火紅的嫁衣襯著肌膚如雪撕予。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天蜈首,我揣著相機與錄音实抡,去河邊找鬼。 笑死欢策,一個胖子當著我的面吹牛澜术,可吹牛的內容都是我干的。 我是一名探鬼主播猬腰,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼猜敢!你這毒婦竟也來了姑荷?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤缩擂,失蹤者是張志新(化名)和其女友劉穎鼠冕,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胯盯,經...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡懈费,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了博脑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片憎乙。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖叉趣,靈堂內的尸體忽然破棺而出泞边,到底是詐尸還是另有隱情,我是刑警寧澤疗杉,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布阵谚,位于F島的核電站,受9級特大地震影響烟具,放射性物質發(fā)生泄漏梢什。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一朝聋、第九天 我趴在偏房一處隱蔽的房頂上張望嗡午。 院中可真熱鬧,春花似錦玖翅、人聲如沸翼馆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽应媚。三九已至严沥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間中姜,已是汗流浹背消玄。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留丢胚,地道東北人翩瓜。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像携龟,于是被迫代替她去往敵國和親兔跌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容