從生成器到async/await

曉宸發表於2019-03-25

回顧

所謂的非同步,就是程式的一部分現在進行,而另一部分則在將來執行。非同步處理的重點就是如何處理將來執行的那一部分。

回撥是 JavaScript 中最基本的非同步模式,就是事先約定好將來要做的事然後回頭呼叫。簡單直接,但也存在不信任、呼叫巢狀過深等問題。對於編寫程式碼、維護程式碼的我們而言,人類的大腦還是習慣於線性的處理方式。

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

生成器

Promise 雖然有序、可靠地管理回撥,但是我們還是希望如同步般表達非同步。

我們已經知道生成器是作為生產迭代器的工廠函式,同時我們還要知道生成器也是一個訊息傳遞系統。

為什麼是生成器

在生成器出現之前,程式程式碼一旦執行,就沒有停下來的時候,直到程式結束?。然而在生成器裡程式碼是可以暫停的,而且還可以和生成器之外通訊☎️,通訊結束後又可以恢復執行。回想一下之前的非同步流程控制,我們一直在想方設法使得非同步任務能夠同步表達。現在,我們可以藉助生成器來實現這一想法?。

瞭解了生成器的特性之後,我們就應該知道,當生成器在執行一個非同步任務時,完全可以把非同步任務放在生成器外部執行,待非同步任務執行結束後再返回?生成器恢復執行。要知道,生成器暫停的只是內部的狀態,程式的其餘部分還是正常執行的。這樣的話,生成器內部的所有程式碼看起來都是同步表達了。

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

生成器與Promise的結合

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

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 都是同一生成器的例項。

如前文所述,理解這塊程式碼還是要從兩方面入手 ———— 迭代和訊息傳遞。迭代屬性在此不再贅述,現在重點是訊息傳遞的屬性。在生成器中,生成器函式被呼叫後並未立即執行,而是構造了一個迭代器。而生成器正是靠著 yield/next 來完成生成器內外部的雙向通訊。

在生成器內部,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 函式的非同步應用

相關文章