從生成器到async/await

回顧

所謂的異步,就是程序的一部分如今進行,而另外一部分則在未來運行。異步處理的重點就是如何處理未來運行的那一部分。es6

回調是 JavaScript 中最基本的異步模式,就是事先約定好未來要作的事而後回頭調用。簡單直接,但也存在不信任、調用嵌套過深等問題。對於編寫代碼、維護代碼的咱們而言,人類的大腦仍是習慣於線性的處理方式。app

基於回調的異步模式所存在的問題促使着咱們尋求一種機制來保證回調的可信任,同時能更好的表達異步。這時候 Promise 出現了,Promise 的出現,並不是要取代回調。而是把回調轉交給了一個位於咱們和其它工具之間的可信任的中介機制。Promise 鏈也提供(儘管並不完美)以順序的方式表達異步流的一個更好的方法,這有助於咱們的大腦更好地計劃和維護異步 JavaScript 代碼。異步

生成器

Promise 雖然有序、可靠地管理回調,可是咱們仍是但願如同步般表達異步。

咱們已經知道生成器是做爲生產迭代器的工廠函數,同時咱們還要知道生成器也是一個消息傳遞系統。async

爲何是生成器

在生成器出現以前,程序代碼一旦執行,就沒有停下來的時候,直到程序結束🔚。然而在生成器裏代碼是能夠暫停的,並且還能夠和生成器以外通訊☎️,通訊結束後又能夠恢復執行。回想一下以前的異步流程控制,咱們一直在千方百計使得異步任務可以同步表達。如今,咱們能夠藉助生成器來實現這一想法💡。ide

瞭解了生成器的特性以後,咱們就應該知道,當生成器在執行一個異步任務時,徹底能夠把異步任務放在生成器外部執行,待異步任務執行結束後再返回🔙生成器恢復執行。要知道,生成器暫停的只是內部的狀態,程序的其他部分仍是正常運行的。這樣的話,生成器內部的全部代碼看起來都是同步表達了。函數

同時咱們也要注意到,生成器不過是一種新🆕的表達方式,和異步仍是同步沒有半毛錢💰關係。既然沒有關係,那在異步模式選擇上就更無所謂了。考慮到異步系列文章是漸進式的,因此咱們就用 Promise + 生成器 模式來表達異步。工具

生成器與Promise的結合

在異步流程控制方面,生成器是由兩部分組成的。一部分是生成器內部代碼以同步的方式表達任務,另外一部分是由生成器生成的迭代器處理異步。ui

const async = n => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(`第${n}個異步任務`);
        }, 0);
    })
};

const generator = function *generator(){
    const response_1 = yield async(1);
    const response_2 = yield async(2);
    const response_3 = yield async(3);
    console.log('response_1: %s;response_2: %s;response_3: %s;',response_1,response_2,response_3);
};

const gen = generator();
const gen_1 = generator();
console.log('gen_next_1: %s; gen_next_2: %s; gen_next_3: %s;', gen_1.next().value, gen_1.next().value, gen_1.next().value);
gen.next().value.then(yield_1 => {
    console.log('yield_1: %s;', yield_1);
    return gen.next(yield_1).value.then(yield_2 => {
        console.log('yield_2: %s;', yield_2);
        return gen.next(yield_2).value.then(yield_3 => {
            console.log('yield_3: %s', yield_3);
            return gen.next(yield_3);
        })
    })
});

// gen_next_1: [object Promise]; gen_next_2: [object Promise]; gen_next_3: [object Promise];
// yield_1: 第1個異步任務;
// yield_2: 第2個異步任務;
// yield_3: 第3個異步任務
// response_1: 第1個異步任務;response_2: 第2個異步任務;response_3: 第3個異步任務;

若是隻看 generator 函數這塊,函數內部的寫法和同步無異。gengen_1 都是同一輩子成器的實例。this

如前文所述,理解這塊代碼仍是要從兩方面入手 ———— 迭代和消息傳遞。迭代屬性在此再也不贅述,如今重點是消息傳遞的屬性。在生成器中,生成器函數被調用後並未當即執行,而是構造了一個迭代器。而生成器正是靠着 yield/next 來完成生成器內外部的雙向通訊。code

在生成器內部,yield 是用來暫停(徹底保持其狀態)和向外部傳遞數據的關鍵字/表達式(初始時函數也是處於未執行狀態)。在生成器外部,next 具備恢復生成器和向生成器內部傳遞數據的能力。

混沌初始(gen 造出來了),盤古開天闢地(第一個 next() 執行),天地初成,繼女媧造人後,一切欣欣向榮。共工和祝融兩個調皮蛋撞壞了不周山,給女媧出了一個難題(yield),華夏史駐此不前。女媧向上天求助(yield async(1)),上天迴應了並送來了五彩石(yield_1),女媧順利補天,華夏史再次啓程(next(yield_1))。

然而好景不長,華夏部落常常受到蚩尤部落騷擾侵犯,蚩尤的存在再次阻礙了華夏史的前行(yield)。黃帝無奈向其師求助(yield async(2)),九天玄女授其兵法(yield_2),黃帝順利殺蚩尤,華夏史再次啓程(next(yield_2))。

然而好景不長,中原地帶洪水氾濫,華夏史再次受阻(yield)。夏禹無奈向太上老君求助(yield async(3)),太上老君贈其神鐵(yield_3),夏禹順利治水,華夏史再次啓程(next (yield_3))。

實在編不下去了,還好結束了。😓 代碼運行過程大抵如此。生成器內部生成一個數據,而後拋給迭代器消費,迭代器又把執行結果甩給了生成器。就是這麼簡單,別想的太複雜就行。

所謂的消息雙向傳遞,指的不只僅是正常狀況下生成器內外部的數據。對於異常錯誤,生成器內外部也能夠雙向捕捉。由於生成器內部的暫停,是保留了其上下文的,因此 try...catch 又能夠一展身手了。

生成器自執行 & async/await

Promise + 生成器 來表達異步算是實現了,然而咱們也應該注意到在用迭代器控制生成器的那部分太過繁瑣。
若是可以封裝下就行了, 以下:

const generator_wrap = function (generator) {
    const args = [...arguments].slice(1);
    const gen = generator.apply(this, args);
    return new Promise((resolve, reject) => {
        const handleNext = function handleNext(yield){
            let next;
            try {
                next = gen.next(yield);
            } catch (error) {
                reject(error);
            }
            if (next.done) {
                resolve(next.value);
            } else {
                return Promise.resolve(next.value).then(yield => {
                    handleNext(yield);
                }, error => {
                    gen.throw(error);
                })
            }
        };
        handleNext();
    })
};
// ———————————— 手動分割線 ————————————
const generator = function *generator(){
    const response_1 = yield async(1);
    const response_2 = yield async(2);
    const response_3 = yield async(3);
    console.log('response_1: %s;response_2: %s;response_3: %s;',response_1,response_2,response_3);
};

generator_wrap(generator);
// response_1: 第1個異步任務;response_2: 第2個異步任務;response_3: 第3個異步任務;

不看 generator_wrap 函數,只看分割線如下的部分。至此,異步流程的表達愈來愈接近理想中的模樣了。但 generator_wrap 函數仍是須要本身手動封裝,不過如今不用啦😄

ES2017 推出了 async/await ,咱們不用再本身去管理生成器,簡單、強大、方便的 async/await 爲咱們處理了一切。

const awati_async = async () => {
    const response_1 = await async(1);
    const response_2 = await async(2);
    const response_3 = await async(3);
    console.log('response_1: %s;response_2: %s;response_3: %s;', response_1, response_2, response_3);
};

awati_async();
// response_1: 第1個異步任務;response_2: 第2個異步任務;response_3: 第3個異步任務;

至此,關於 JavaScript 的異步表達暫時告一段落了👋。

異步的 JavaScript 系列:

異步的JavaScript(回調篇)

異步的JavaScript(Promise篇)

異步的JavaScript(終篇) 附(從迭代器模式到迭代協議

參考資料:

迭代器和生成器

你不知道的 JavaScript (中卷)

你不知道的 JavaScript (下卷)

Generator 函數的異步應用

相關文章
相關標籤/搜索