JavaScript 工做原理之四-事件循環及異步編程的出現和 5 種更好的 async/await 編程方式(譯)

原文請查閱 這裏,略有改動。

本系列持續更新中,Github 地址請查閱這裏javascript

這是 JavaScript 工做原理的第四章。html

如今,咱們將會經過回顧單線程環境下編程的弊端及如何克服這些困難以建立使人驚歎的 JavaScript 交互界面來展開第一篇文章。老規矩,咱們將會在本章末尾分享 5 條利用 async/await 編寫更簡潔代碼的小技巧。java

單線程的侷限性

在第一篇文章開頭,咱們考慮了一個問題即當調用棧中含有須要長時間運行的函數調用的時候會發生什麼。git

譬如,試想下,在瀏覽器中運行着一個複雜的圖片轉化算法。github

剛好此時調用棧中有函數須要執行,此時瀏覽器將會被阻塞,它不可以作其它任何事情。這意味着,瀏覽器會沒有響應,不可以進行渲染和運行其它代碼。這將會帶來問題-程序界面將再也不高效和使人愉悅。web

程序沒有響應。ajax

在某些狀況下,這或許沒什麼大不了的。可是,這可能會形成更加嚴重的問題。一旦瀏覽器在調用棧中同時運行太多的任務的時候,瀏覽器會很長時間中止響應。到了那個時候,大多數瀏覽器會拋出一個錯誤,詢問你是否關閉網頁。算法

這很醜陋且它徹底摧毀了程序的用戶體驗。typescript

未響應

JavaScript 程序組件

你可能會在單一的 .js 文件中書寫 JavaScript 程序,可是程序是由多個代碼塊組成的,當前只有一個代碼塊在運行,其它代碼塊將在隨後運行。最多見的塊狀單元是函數。編程

大多數 JavaScript 菜鳥有可能須要理解的問題即以後運行表示的是並非必須嚴格且當即在如今以後執行。換句話說即,根據定義,如今不可以運行完畢的任務將會異步完成,這樣你就不會不經意間遇到以上說起的 UI 阻塞行爲。

看下以下代碼:

// ajax 爲一個庫提供的任意 ajax 函數
var response = ajax('https://example.com/api');

console.log(response);
// `response` 將不會有數據返回

可能你已經知道標準的 ajax 請求不會徹底同步執行完畢,意即在代碼運行階段,ajax(..) 函數不會返回任何值給 response 變量。

得到異步函數返回值的一個簡單方法是使用回調函數。

ajax('https://example.com/api', function(response) {
    console.log(response); // `response` 如今有值
});

只是要注意一點:即便能夠也永遠不要發起同步 ajax 請求。若是發起同步 ajax 請求,JavaScript 程序的 UI 將會被阻塞-用戶不可以點擊,輸入數據,跳轉或者滾動。這將會凍結任何用戶交互體驗。這是很是糟糕。

如下示例代碼,但請別這樣作,這會毀掉網頁:

// 假設你使用 jQuery
jQuery.ajax({
    url: 'https://api.example.com/endpoint',
    success: function(response) {
        // 成功回調.
    },
    async: false // 同步
});

咱們以 Ajax 請求爲例。你能夠異步執行任意代碼。

你可使用 setTimeout(callback, milliseconds) 函數來異步執行代碼。setTimeout 函數會在以後的某個時刻觸發事件(定時器)。以下代碼:

function first() {
    console.log('first');
}
function second() {
    console.log('second');
}
function third() {
    console.log('third');
}
first();
setTimeout(second, 1000); // 1 秒後調用 second 函數
third();

控制檯輸出以下:

first
third
second

事件循環詳解

咱們將會以一個有些讓人費解的問題開始-儘管容許異步執行 JavaScript 代碼(好比以前討論的 setTimetout),可是直到 ES6,實際上 JavaScript 自己並無集成任何直接的異步編程概念。JavaScript 引擎只容許在任意時刻執行單個的程序片斷。

能夠查看以前的文章來了解 JavaScript 引擎的工做原理。

那麼, JS 引擎是如何執行程序片斷的呢?實際上,JS 引擎並非隔離運行的-它運行在一個宿主環境中,對大多數開發者來講是典型的 web 瀏覽器或者 Node.js。實際上,如今 JavaScript 普遍應用於從機器到電燈泡的各類設備之中。每一個設備表明了 JS 引擎的不一樣類型的宿主環境。

全部宿主環境都含有一個被稱爲事件循環的內置機制,隨着時間的推移,事件循環會執行程序中多個代碼片斷,每次都會調用 JS 引擎。

這意味着 JS 引擎只是任意 JS 代碼的按需執行環境。這是一個封閉的環境,在其中進行事件的調度(運行JS 代碼)。

因此,打個比方,當 JavaScript 程序發起 Ajax 請求來從服務器得到數據,你在回調函數中書寫 "response" 代碼,JS 引擎會告訴宿主環境:

"嘿,我如今要掛起執行了,如今當你完成網絡請求的時候且返回了數據,請執行回調函數。"

以後瀏覽器會監遵從網絡中返回的數據,當有數據返回的時候,它會經過把回調函數插入事件循環以便調度執行。

讓咱們看下以下圖示:

內存圖示

你能夠在以前的文章中閱讀更多關於動態內存管理和調用棧的信息。

什麼是網頁 API ?本質上,你沒有權限訪問這些線程,你只可以調用它們。它們是瀏覽器自帶的,且能夠在瀏覽器中進行併發操做。若是你是個 Node.js 開發者,這些是 C++ APIs。

說了那麼多,事件循環究竟是啥?

事件循環圖示

事件循環只有一項簡單的工做-監測調用棧和回調隊列。若是調用棧是空的,它會從回調隊列中取得第一個事件而後入棧,並有效地執行該事件。

事件循環中的這樣一次遍歷被稱爲一個 tick。每一個事件就是一個回調函數。

console.log('Hi');
setTimeout(function cb1() { 
    console.log('cb1');
}, 5000);
console.log('Bye');

讓咱們執行這段代碼,而後看看會發生什麼:

1.空狀態。瀏覽器控制檯是空的,調用棧也是空的。

空狀態圖例

2.console.log('Hi') 入棧。

入棧圖例

3.執行 console.log('Hi')

4.console.log('Hi') 出棧

  1. setTimeout(function cb1() { ... }) 入棧。

6.執行 setTimeout(function cb1() { ... }),瀏覽器建立定時器做爲網頁 API 的一部分並將會爲你處理倒計時。

7.setTimeout(function cb1() { ... }) 執行完畢並出棧。

8.console.log('Bye') 入棧。

9.執行 console.log('Bye')

10.console.log('Bye') 出棧。

11.至少 5 秒以後,定時器結束運行並把 cb1 回調添加到回調隊列。

12.事件循環從回調隊列中得到 cb1 函數而且將其入棧。

13.運行 cb1 函數並將 console.log('cb1') 入棧。

14.執行 console.log('cb1')

15.console.log('cb1') 出棧。

16.cb1 出棧

錄像快速回放:

使人感興趣的是,ES6 規定事件循環如何工做的,這意味着從技術上講,它在 JS 引擎負責的範圍以內,而 JS 引擎將再也不只是扮演着宿主環境的角色。ES6 中 Promise 的出現是致使改變的主要緣由之一,由於 ES6 要求有權限直接細粒度地控制事件循環隊列中的調度操做(以後會深刻探討)。

setTimeout(…) 工做原理

須要注意的是 setTimeout(…) 並無自動把回調添加到事件循環隊列。它建立了一個定時器。當定時器過時,宿主環境會把回調函數添加至事件循環隊列中,而後,在將來的某個 tick 取出並執行該事件。查看以下代碼:

setTimeout(myCallback, 1000);

這並不意味着 1 秒以後會執行 myCallback 回調而是在 1 秒後將其添加到回調隊列。然而,該隊列有可能在以前就添加了其它的事件-因此回調就會被阻塞。

有至關一部分的文章和教程開始會建議你使用 setTimeout(callback, 0) 來書寫 JavaScript 異步代碼。那麼,如今你明白了事件循環和 setTimeout 的原理:調用 setTimeout 把其第二個參數設置爲 0 表示延遲執行回調直到調用棧被清空。

查看以下代碼:

console.log('Hi');
setTimeout(function() {
    console.log('callback');
}, 0);
console.log('Bye');

雖然定時時間設定爲 0, 可是控制檯中的結果將會以下顯示:

Hi
Bye
callback

ES6 做業概念

ES6 介紹了一個被稱爲『做業隊列』的概念。它位於事件循環隊列的頂部。你極有可能在處理 Promises(以後會介紹) 的異步行爲的時候無心間接觸到這一律念。

如今咱們將會接觸這個概念,以便當討論 Promises 的異步行爲以後,理解如何調度和處理這些行爲。

像這樣想象一下:做業隊列是附加於事件循環隊列中每一個 tick 末尾的隊列。事件循環的一個 tick 所產生的某些異步操做不會致使添加全新的事件到事件循環隊列中,可是反而會在當前 tick 的做業隊列末尾添加一個做業項。

這意味着,你能夠添加延時運行其它功能而且你能夠確保它會在其它任何功能以前馬上執行。

一個做業也能夠在同一隊列末尾添加更多的做業。理論上講,存在着做業循環的可能性(好比做業不停地添加其它做業)。

爲了無限循環,就會飢餓程序所須要的資源直到下一個事件循環 tick。從概念上講,這相似於在代碼裏面書寫耗時或者死循環(相似 while(true))。

做業是有些相似於 setTimeout(callback, 0) 小技巧,可是是以這樣的方式實現的,它們擁有明肯定義和有保證的執行順序:以後且儘快地執行。

回調

正如你已知的那樣,回調函數是 JavaScript 程序中用來表示和進行異步操做的最多見方法。的確,回調是 JavaScript 語言中最爲重要的異步模式。無數的 JS 程序,甚至很是複雜的那些,都是創建在回調函數之上的。

回調並非沒有缺點。許多開發者試圖找到更好的異步模式。然而,若是你不理解底層的原理而想要高效地使用任何抽象化的語法這是不可能的。

在接下來的章節中,咱們將會深刻探究這些抽象語法並理解更復雜的異步模式的必要性。

嵌套回調

查看如下示例:

listen('click', function (e){
    setTimeout(function(){
        ajax('https://api.example.com/endpoint', function (text){
            if (text == "hello") {
            doSomething();
        }
        else if (text == "world") {
            doSomethingElse();
            }
        });
    }, 500);
});

咱們有三個鏈式嵌套函數,每一個函數表明一個異步操做。

這類代碼一般被稱爲『回調地獄』。可是,實際上『回調地獄』和代碼嵌套及縮進沒有任何關係。這是一個更加深入的問題。

首先,咱們監聽點擊事件,而後,等待定時器執行,最後等待 Ajax 返回數據,在 Ajax 返回數據的時候,能夠重複執行這一過程。

乍一眼看上去,能夠上把以上具備異步特性的代碼拆分爲按步驟執行的代碼,以下所示:

listen('click', function (e) {
    // ..
});

以後:

setTimeout(function(){
    // ..
}, 500);

再後來:

ajax('https://api.example.com/endpoint', function (text){
    // ..
});

最後:

if (text == "hello") {
    doSomething();
}
else if (text == "world") {
    doSomethingElse();
}

所以,以這樣順序執行的方式來表示異步代碼看起來一鼓作氣,應該有這樣的方法吧?

Promises

查看以下代碼:

var x = 1;
var y = 2;
console.log(x + y);

這個很直觀:計算出 x 和 y 的值而後在控制檯打印出來。可是,若是 x 或者 y 的初始值是不存在的且不肯定的呢?假設,在表達式中使用 x 和 y 以前,咱們須要從服務器獲得 x 和 y 的值。想象下,咱們擁有函數 loadXloadY 分別從服務器獲取 x 和 y 的值。而後,一旦得到 xy 的值,就可使用 sum 函數計算出和值。

相似以下這樣:

function sum(getX, getY, callback) {
    var x, y;
    getX(function(result) {
        x = result;
        if (y !== undefined) {
            callback(x + y);
        }
    });
    getY(function(result) {
        y = result;
        if (x !== undefined) {
            callback(x + y);
        }
    });
}
// 同步或異步獲取 `x` 值的函數
function fetchX() {
    // ..
}


// 同步或異步獲取 `y` 值的函數
function fetchY() {
    // ..
}

sum(fetchX, fetchY, function(result) {
    console.log(result);
});

這裏須要記住的一點是-在代碼片斷中,xy 是將來值,咱們用 sum(..)(從外部)來計算和值,可是並無關注 xy 是否立刻同時有值。

固然嘍,這個粗糙的基於回調的技術還有不少須要改進的地方。這只是理解推出將來值而不用擔憂什麼時候有返回值的好處的一小步。

Promise 值

讓咱們簡略地看一下如何用 Promises 來表示 x+y

function sum(xPromise, yPromise) {
    // `Promise.all([ .. ])` 包含一組 Promise,
    // 並返回一個新的 Promise 來等待全部 Promise 執行完畢
    return Promise.all([xPromise, yPromise])

    // 當新 Promise 解析完畢,就能夠同時得到 `x` 和 `y` 的值並相加。
    .then(function(values){
        // `values` 是以前解析 promises 返回的消息數組
        return values[0] + values[1];
    } );
}

// `fetchX()` and `fetchY()` 返回 promise 來取得各自的返回值,這些值返回是無時序的。
sum(fetchX(), fetchY())

// 得到一個計算兩個數和值的 promise,如今,就能夠鏈式調用 `then(...)` 來處理返回的 promise。
.then(function(sum){
    console.log(sum);
});

以上代碼片斷含有兩種層次的 Promise。

fetchX()fetchY() 都是直接調用,它們的返回值(promises!)都被傳入 sum(…) 做爲參數。雖然這些 promises 的 返回值也許會在如今或以後返回,可是不管如何每一個 promise 都具備相同的異步行爲。咱們能夠的推算 xy 是與時間無關的值。暫時稱他們爲將來值。

第二層次的 promise 是由 sum(…) (經過 Promise.all([ ... ]))所建立和返回的,而後經過調用 then(…) 來等待 promise 的返回值。當 sum(…) 運行結束,返回 sum 將來值而後就能夠打印出來。咱們在 sum(…) 內部隱藏了等待將來值 xy 的邏輯。

注意:sum(…) 內部,Promise.all([ … ])建立了一個 promise(在等待 promiseXpromiseY 解析以後)。鏈式調用 .then(…) 建立了另外一個 promise,該 promise 會由代碼 values[0] + values[1] 馬上進行解析(返回相加結果)。所以,在代碼片斷的末尾即 sum(…) 的末尾鏈式調用 then(…)-其實是在操做第二個返回的 promise 而不是第一個由 Promise.all([ ... ]) 建立返回的 promise。一樣地,雖然咱們沒有在第二個then(…) 以後進行鏈式調用,可是它也建立了另外一個 promise,咱們能夠選擇觀察/使用該 promise。咱們將會在本章的隨後內容中進行詳細地探討 promise 的鏈式調用相關。

在 Promises 中,實際上 then(…) 函數能夠傳入兩個函數做爲參數,第一個函數是成功函數,第二個是失敗函數。

sum(fetchX(), fetchY())
.then(
    // 成功句柄
    function(sum) {
        console.log( sum );
    },
    // 拒絕句柄
    function(err) {
        console.error( err ); // bummer!
    }
);

當獲取 x 或者 y 出現錯誤或者計算和值的時候出現錯誤,sum(…) 返回的 promise 將會失敗,傳入 then(…) 做爲第二個參數的回調錯誤處理程序將會接收 promise 的返回值。

由於 Promise 封裝了時間相關的狀態-等待外部的成功或者失敗的返回值,Promise 自己是與時間無關的,這樣就可以以可預測的方式組成(合併) Promise 而不用關心時序或者返回結果。

除此以外,一旦 Promise 解析完成,它就會一直保持不可變的狀態且能夠被隨意觀察。

鏈式調用 promise 真的很管用:

function delay(time) {
    return new Promise(function(resolve, reject){
        setTimeout(resolve, time);
    });
}

delay(1000)
.then(function(){
    console.log("after 1000ms");
    return delay(2000);
})
.then(function(){
    console.log("after another 2000ms");
})
.then(function(){
    console.log("step 4 (next Job)");
    return delay(5000);
})
// ...

調用 delay(2000) 建立一個將在 2 秒後返回成功的 promise,而後,從第一個 then(…) 的成功回調函數中返回該 promise,這會致使第二個 then(…) 返回的 promise 等待 2 秒後返回成功的 promise。

Note:由於一個 promise 一旦解析其狀態就不能夠從外部改變,因爲它的狀態不能夠被隨意修改,因此能夠安全地把狀態值隨意分發給任意第三方。當涉及多方觀察 Promise 的返回結果時候更是如此。一方影響另外一方觀察 Promise 返回結果的能力是不可能。不可變性聽起來像是個晦澀的科學課題,可是,實際上這是 Promise 最根本和重要的方面,你得好好研究研究。

Promise 使用時機

Promise 的一個重要細節即肯定某些值是不是真正的 Promise。換句話說,這個值是否具備 Promise 的行爲。

咱們知道能夠利用 new Promise(…) 語法來建立 Promise,而後,你會認爲使用 p instanceof Promise 來檢測某個對象是不是 Promise 類的實例。然而,並不全然如此。

主要的緣由在於你能夠從另外一個瀏覽器窗口(好比 iframe)得到 Promise 實例,iframe 中的 Promise 不一樣於當前瀏覽器窗口或框架中的 Promise,所以,會致使檢測 Promise 實例失敗。

除此以外,庫或框架或許會選擇使用自身自帶的 Promise 而不是原生的 ES6 實現的 Promise。實際工做中,你可使用庫自帶的 Promise 來兼容不支持 Promise 的老版本瀏覽器。

異常捕獲

若是在建立 Promise 或者是在觀察解析 Promise 返回結果的任意時刻,遇到了諸如 TypeError 或者 ReferenceError 的 JavaScript 錯誤異常,這個異常會被捕獲進而強制 Promise 爲失敗狀態。

好比:

var p = new Promise(function(resolve, reject){
    foo.bar();      // `foo` 未定義,產生錯誤!
    resolve(374); // 永不執行 :(
});

p.then(
    function fulfilled(){
        // 永不執行 :(
    },
    function rejected(err){
        // `err` 會是一個 `TypeError` 異常對象
         // 因爲 `foo.bar()` 代碼行.
    }
);

可是,若是 Promise 成功解析了而在成功解析的監聽函數(then(…) 註冊回調)中拋出 JS 運行錯誤會怎麼樣?仍然能夠捕捉到該異常,但或許你會發現處理這些異常的方式有些讓人奇怪。直到深刻理解其中原理:

var p = new Promise( function(resolve,reject){
    resolve(374);
});

p.then(function fulfilled(message){
    foo.bar();
    console.log(message);   // 永不執行
},
    function rejected(err){
        // 永不執行
    }
);

看起來 foo.bar() 拋出的錯誤異常真的被捕獲到了。然而,事實上並無。然而,深刻理解你會發現咱們沒有監測到其中一些錯誤。p.then(…) 調用自己返回另外一個 promise,該 promise 會返回 TypeError 類型的異常失敗信息。

拓展一下以上的說明,這是原文沒有的。

var p = new Promise( function(resolve,reject){
    resolve(374);
});

p.then(function fulfilled(message){
    foo.bar();
    console.log(message);   // 永不執行
},
    function rejected(err){
        // 永不執行
    }
).then(
function() {},
function(err) { console.log('err', err);}
);

如上代碼所示就能夠真正捕獲到 promise 成功解析回調函數裏面的代碼錯誤。

處理未捕獲的異常

有其它許多聽說更好的處理異常的技巧。

廣泛的作法是爲 Promises 添加 done(..) 回調,本質上這會標記 promise 鏈的狀態爲 "done."。done(…) 並不會建立和返回 promise,所以,當不存在鏈式 promise 的時候,傳入 done(..) 的回調顯然並不會拋出錯誤。

和未捕獲的錯誤情況同樣:任何在 done(..) 失敗處理函數中的異常都將會被拋出爲全局錯誤(基本上是在開發者控制檯)。

var p = Promise.resolve(374);

p.then(function fulfilled(msg){
    // 數字沒有字符類的函數,因此會報錯
    console.log(msg.toLowerCase());
})
.done(null, function() {
    // 若發生錯誤,將會拋出全局錯誤
});

ES8 中的 Async/await

JavaScript ES8 中介紹了 async/await,這使得處理 Promises 更加地容易。咱們將會簡要介紹 async/await 的全部可能姿式並利用其來書寫異步代碼。

那麼,讓咱們瞧瞧 async/await 工做原理。

使用 async 函數定義一個異步函數。該函數會返回異步函數對象。AsyncFunction 對象表示在異步函數中運行其內部代碼。

當調用異步函數的時候,它會返回一個 Promise。異步函數返回值並不是 Promise,在函數過程當中會自動建立一個 Promise 並使用函數的返回值來解析該 Promise。當 async 函數拋出異常,Promise 失敗回調會獲取拋出的異常值。

async 函數能夠包含一個 await 表達式,這樣就能夠暫停函數的執行來等待傳入的 Promise 的返回結果,以後重啓異步函數的執行並返回解析值。

你能夠把 JavaScript 中的 Promise 看做 Java 中的 FutureC# 中的 Task。

async/await 本意是用來簡化 promises 的使用。

看下以下代碼:

// 標準 JavaScript 函數
function getNumber1() {
    return Promise.resolve('374');
}
// 和 getNumber1 同樣
async function getNumber2() {
    return 374;
}

相似地,拋出異常的函數等價於返回失敗的 promises。

function f1() {
    return Promise.reject('Some error');
}
async function f2() {
    throw 'Some error';
}

await 關鍵字只能在 async 函數中使用而且容許你同步等待 Promise。若是在 async 函數外使用 promises,咱們仍然必須使用 then 回調。

async function loadData() {
    // `rp` 是個發起 promise 的函數。
    var promise1 = rp('https://api.example.com/endpoint1');
    var promise2 = rp('https://api.example.com/endpoint2');
   
    // 如今,併發請求兩個 promise,如今咱們必須等待它們結束運行。
    var response1 = await promise1;
    var response2 = await promise2;
    return response1 + ' ' + response2;
}
// 由於再也不使用 `async function`,因此必須使用 `then`。
loadData().then(() => console.log('Done'));

你也可使用異步函數表達式來定義異步函數。異步函數表達式擁有和異步函數語句相近的語法。異步函數表達式和異步函數語句的主要區別在於函數名,異步函數表達式能夠忽略函數名來建立匿名函數。異步函數表達式能夠被用做 IIFE(當即執行函數表達式),能夠在定義的時候當即運行。

像這樣:

var loadData = async function() {
    // `rp` 是個發起 promise 的函數。
    var promise1 = rp('https://api.example.com/endpoint1');
    var promise2 = rp('https://api.example.com/endpoint2');
   
    // 如今,併發請求兩個 promise,如今咱們必須等待它們結束運行。
    var response1 = await promise1;
    var response2 = await promise2;
    return response1 + ' ' + response2;
}

更爲重要的是,全部的主流瀏覽器都支持 async/await。

若是該兼容性不符合你的需求,你可使用諸如 BabelTypeScript 的 JS 轉譯器來轉換爲本身須要的兼容程度。

最後要說的是,不要盲目地使用最新的技術來寫異步代碼。理解 JavaScript 中 async 的內部原理是很是重要的,學習爲何深刻理解所選擇的方法是很重要的。正如編程中的其它東西同樣,每種技術都有其優缺點。

書寫高可用,強壯的異步代碼的 5 條小技巧

1.簡潔:使用 async/await 可讓你寫更少的代碼。每次書寫 async/await 代碼,你均可以跳過書寫一些沒必要要的步驟: 好比不用寫 .then 回調,建立匿名函數來處理返回值,命名回調返回值。

// `rp` 是個發起 promise 的工具函數。
rp(‘https://api.example.com/endpoint1').then(function(data) {
 // …
});

對比:

// `rp` 是個發起 promise 的工具函數
var response = await rp(‘https://api.example.com/endpoint1');

2.錯誤處理:Async/await 容許使用平常的 try/catch 代碼結構體來處理同步和異步錯誤。看下和 Promise 是如何寫的:

function loadData() {
    try { // 捕獲同步錯誤.
        getJSON().then(function(response) {
            var parsed = JSON.parse(response);
            console.log(parsed);
        }).catch(function(e) { // 捕獲異步錯誤.
            console.log(e); 
        });
    } catch(e) {
        console.log(e);
    }
}

對比:

async function loadData() {
    try {
        var data = JSON.parse(await getJSON());
        console.log(data);
    } catch(e) {
        console.log(e);
    }
}

3.條件語句:使用 async/await 來書寫條件語句會更加直觀。

function loadData() {
  return getJSON()
    .then(function(response) {
      if (response.needsAnotherRequest) {
        return makeAnotherRequest(response)
          .then(function(anotherResponse) {
            console.log(anotherResponse)
            return anotherResponse
          })
      } else {
        console.log(response)
        return response
      }
    })
}

對比:

async function loadData() {
  var response = await getJSON();
  if (response.needsAnotherRequest) {
    var anotherResponse = await makeAnotherRequest(response);
    console.log(anotherResponse)
    return anotherResponse
  } else {
    console.log(response);
    return response;    
  }
}

4.堆棧楨:和 async/await 不一樣的是,從鏈式 promise 返回的錯誤堆棧中沒法得知發生錯誤的地方。看以下代碼:

function loadData() {
  return callAPromise()
    .then(callback1)
    .then(callback2)
    .then(callback3)
    .then(() => {
      throw new Error("boom");
    })
}
loadData()
  .catch(function(e) {
    console.log(err);

// Error: boom at callAPromise.then.then.then.then (index.js:8:13)
});

對比:

async function loadData() {
  await callAPromise1()
  await callAPromise2()
  await callAPromise3()
  await callAPromise4()
  await callAPromise5()
  throw new Error("boom");
}
loadData()
  .catch(function(e) {
    console.log(err);
    // output
    // Error: boom at loadData (index.js:7:9)
});

5.調試:若是使用 promise,你就會明白調試它們是一場噩夢。例如,若是你在 .then 代碼塊中設置一個斷點而且使用諸如 "stop-over" 的調試快捷鍵,調試器不會移動到下一個 .then 代碼塊,由於調試器只會步進同步代碼。

使用 async/await 你能夠就像同步代碼那樣步進到下一個 await 調用。

不只是程序自己還有庫,書寫異步 JavaScript 代碼都是至關重要的。

參考資源:

本系列持續更新中,Github 地址請查閱這裏

相關文章
相關標籤/搜索