【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

妙堂傳道者發表於2019-01-11

此篇是JavaScript的工作原理的第四篇,其它三篇可以看這裡:

這次我們將通過回顧在單執行緒環境中程式設計的缺點以及如何克服它們來構建令人驚歎的JavaScript UI來擴充套件我們的第一篇文章。按照傳統,在文章的最後,我們將分享有關如何使用async / await編寫更清晰程式碼的5個技巧。

為什麼單執行緒有侷限性?

第一篇文章中,我們提到過一個問題:當呼叫棧中含有需要長時間執行的函式呼叫的時候會發生什麼。
想象一下,例如,當瀏覽器中執行著一個複雜的圖片轉換演算法。
在這個時候,堆疊中正好有函式在執行,瀏覽器此時不能做任何事情。此時,他被阻塞了。這意味著它不能渲染,不能執行其他程式碼,他被卡住了,沒有任何響應。這就帶來了一個問題,你的程式不再是高效的了。
你的程式沒有相應了。
在某些情況下,這沒有什麼大不了的,但是這可能會造成更加嚴重的問題。一旦瀏覽器在呼叫棧中同時執行太多的任務的時候,瀏覽器會很長時間停止響應。在那個時候,大多數瀏覽器會丟擲一個錯誤,詢問是否終止網頁。

這很醜陋且它完全摧毀了程式的使用者體驗。

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',
    successfunction(response{
        // 成功回撥.
    },
    asyncfalse // 同步
});
複製程式碼

我們以 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 程式碼(就像上例討論的setTimeout),但在ES6之前,JavaScript本身實際上從來沒有任何內建非同步的概念,JavaScript引擎在任何給定時刻只執行一個塊

對於更多的JavaScript引擎怎麼工作的,可以看系列文章的第一篇

那麼,是誰告訴JS引擎執行程式的程式碼塊呢?實際上,JS引擎並不是單獨執行的——它是在一個宿主環境中執行的,對於大多數開發人員來說,宿主環境就是典型的web瀏覽器或Node.js。實際上,現在JavaScript被嵌入到各種各樣的裝置中,從機器人到燈泡,每個裝置代表 JS 引擎的不同型別的託管環境。

所有環境中的共同點是一個稱為事件迴圈的內建機制,它每次呼叫JS引擎時都會處理程式的多個塊的執行。

這意味著JS引擎只是任意JS程式碼的按需執行環境,是宿主環境處理事件執行及結果。

例如,當 JavaScript 程式發出 Ajax 請求從伺服器獲取一些資料時,在函式(“回撥”)中設定“response”程式碼,JS引擎告訴宿主環境:"我現在要推遲執行,但當完成那個網路請求時,會返回一些資料,請回撥這個函式並給資料傳給它"。

然後瀏覽器將偵聽來自網路的響應,當監聽到網路請求返回內容時,瀏覽器通過將回撥函式插入事件迴圈來排程要執行的回撥函式。以下是示意圖:

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

您可以在我們之前的文章中閱讀有關記憶體堆和呼叫堆疊的更多資訊。

這些Web api是什麼?從本質上說,它們是無法訪問的執行緒,只能呼叫它們。它們是瀏覽器的併發部分。如果你是一個Node.js開發者,這些就是 c++ 的 Api。

那麼事件迴圈究竟是什麼呢?

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

事件迴圈有一個簡單的工作——監視呼叫堆疊和回撥佇列。如果呼叫堆疊為空,它將從佇列中獲取第一個事件,並將其推送到呼叫堆疊,這將有效地執行它。

這樣的迭代在事件迴圈中稱為 (tick標記),每個事件只是一個函式回撥。

console.log('Hi');
setTimeout(function cb1() { 
    console.log('cb1');
}, 5000);
console.log('Bye');
複製程式碼

讓我們執行這份程式碼看看發生了什麼:

1.初始狀態都為空,瀏覽器console皮膚為空,呼叫堆疊為空。

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

2.console.log('Hi')被新增到呼叫堆疊中。

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

3.console.log(Hi)被執行。

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

4.console.log('Hi')從呼叫堆疊中移除。

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

5.setTimeout(function cb1() { ... })被新增到呼叫堆疊當中

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

6.setTimeout(function cb1() { ... })被執行,瀏覽器通過它的Web APIS建立了一個計時器,為你的程式碼計時。

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

7.這個setTimeout(function cb1() { ... })呼叫計時器它本身的函式是已經執行完成,從呼叫堆疊中移除。

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

8.console.log('Bye')被新增到呼叫堆疊中。

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

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

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

10.console.log('Bye')從呼叫堆疊中移除。

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

11.在至少5000ms後,定時器執行完成後,把cb1回撥函式新增到回撥佇列裡面。

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

12.事件迴圈把cb1從回撥佇列中取出,新增到呼叫堆疊中。

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

13.cb1被執行,把console.log('cb1')新增呼叫堆疊中。

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

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

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

15.console.log('cb1')從呼叫堆疊中移除。

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

16.cb1從呼叫堆疊中移除。

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

整體過程回顧:

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式
比較值得注意的是,ES6指定了事件迴圈應該怎麼執行。這意味著在技術範圍內,他是屬於JS引擎的職責範圍內,不再僅僅扮演宿主環境的角色。這種變化的一個主要原因是ES6中引入了Promises,因為後者需要對事件迴圈佇列上的排程操作更直接,控制更細粒度(稍後我們將更詳細地討論它們)

setTimeout(...)怎麼工作的

需要注意的是,setTimeout(…)不會自動將回撥放到事件迴圈佇列中。它設定了一個計時器。當計時器過期時,環境將回撥放到回撥中,以便將來某個標記(tick)將接收並執行它。請看下面的程式碼:

setTimeout(myCallback, 1000);
複製程式碼

這不是意味著myCallback將在1000ms後執行,而是在1000ms後myCallback將被新增到回撥佇列裡面去,這個佇列可能也有其他比較早被新增的事件正在等待,這個時候,你的回撥就必須要等待。

有不少文章和教程說在JavaScript中開始使用非同步程式設計的時候,都建議使用setTimeout(callback,0),那麼現在你知道了事件迴圈的機制和setTimeout怎麼執行的,呼叫setTimeout 0毫秒作為第二個引數只是推遲迴調將它放到回撥佇列中,直到呼叫堆疊是空的。

看下下面的程式碼:

console.log('Hi');
setTimeout(function() {
    console.log('callback');
}, 0);
console.log('Bye');
複製程式碼

儘管等待時間設定成了0ms,這個瀏覽器列印的結果如下:

Hi
Bye
callback
複製程式碼

ES6中的任務佇列是什麼?

在ES6的介紹中有一個新的叫做“任務佇列”的概念,它是事件迴圈佇列上面的一層,最常見的是在promise處理非同步方式的時候。 現在只討論這個概念,以便在討論帶有Promises的非同步行為時,能夠了解 Promises 是如何排程和處理。

想象一下:這個任務佇列是附加到事件迴圈佇列中每個標記(一次從回撥隊裡裡面取到資料後,放到呼叫堆疊執行的過程)末尾的佇列,某些非同步操作可能發生在事件迴圈的一個標記期間,不會導致一個全新的事件被新增到事件迴圈佇列中,而是將一個專案(即任務)新增到當前標記的任務佇列的末尾。

這意味著可以放心新增另一個功能以便稍後執行,它將在其他任何事情之前立即執行。

一個任務還可能建立更多工新增到同一佇列的末尾。理論上,任務“迴圈”(不斷新增其他任務的任等等)可以無限執行,從而使程式無法獲得轉移到下一個事件迴圈標記的必要資源。從概念上講,這類似於在程式碼中表示長時間執行或無限迴圈(如while (true) ..)。

任務有點像 setTimeout(callback, 0) “hack”,但其實現方式是引入一個定義更明確、更有保證的順序:稍後執行,但越快越好。

回撥

正如你已經知道的,回撥是到目前為止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響應返回,此時可能會再次重複所有操作。
乍一看,這段程式碼似乎可以將其非同步過程對應到以下多個函式順序執行的步驟:

listen('click', function (e) {
	// ..
});
複製程式碼

然後:

setTimeout(function(){
    // ..
}, 500);
複製程式碼

再然後:

ajax('https://api.example.com/endpoint', function (text){
    // ..
});
複製程式碼

最後:

if (text == "hello") {
    doSomething();
}
else if (text == "world") {
    doSomethingElse();
}
複製程式碼

所以這種同步的方式去表達你的非同步巢狀程式碼,是不是更自然一些?一定有這樣的方法,對吧?

Promise

看下下面的程式碼:

var x = 1;
var y = 2;
console.log(x + y);
複製程式碼

非常的直觀,這個xy相加之和通過console.log列印出來。如果,xy的值還沒有賦上,仍然需要求值,怎麼辦?
例如,需要從伺服器取回x和y的值,然後才能在表示式中使用它們。假設我們有一個函式loadX和loadY,它們分別從伺服器載入xyy的值。然後,一旦xy都被載入,我們有一個函式sum,它對xy的值進行求和。 它可能是這樣的:

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);
        }
    });
}
// A sync or async function that retrieves the value of `x`
//獲取到x值得方法
function fetchX() {
    // ..
}


// A sync or async function that retrieves the value of `y`
//獲取到y值得方法
function fetchY() {
    // ..
}

//呼叫
sum(fetchX, fetchY, function(result) {
    console.log(result);
});
複製程式碼

這段程式碼中有一些非常重要的東西,我們將x和y作為非同步獲取的值,並且執行了一個函式sum(…)(從外部),它不關心x或y,也不關心它們是否立即可用。

當然,這種基於回撥的粗略方法還有很多不足之處。 這只是一個我們不必判斷對於非同步請求的值的處理方式一個小步驟而已。

Promise Value

簡單的看一下,我們怎麼用promise表達x+y

function sum(xPromise, yPromise) {

	// `Promise.all([ .. ])` takes an array of promises,
	// and returns a new promise that waits on them
	// all to finish
	//`Promise.all([ .. ])` 傳入一個promise陣列,
	//通過返回一個新的promise,這個promise將等待所有的返回
	return Promise.all([xPromise, yPromise])

	// when that promise is resolved, let's take the
	// received `X` and `Y` values and add them together.
	//當promise是被resolved了,就返回這個x和y的值,執行加法
	.then(function(values){
		// `values` is an array of the messages from the
		// previously resolved promises
		//`values` 是上一個promise.all執行結果的陣列
		return values[0] + values[1];
	} );
}

// `fetchX()` and `fetchY()` return promises for
// their respective values, which may be ready
// *now* or *later*.
//`fetchX()` 和 `fetchY()`返回各自的promise
sum(fetchX(), fetchY())

// we get a promise back for the sum of those
// two numbers.
// now we chain-call `then(...)` to wait for the
// resolution of that returned promise.
//我們得到兩個promise之和的值,等待這個promise執行成功
.then(function(sum){
    console.log(sum);
});
複製程式碼

在這個程式碼中有兩層promise。
fetchX()fetchY() 直接被呼叫,他們返回的值(promise)傳入到了sum(...)。這個promise所代表的基礎值無論是現在或者將來都可以準備就緒。但每個promise都會將其行為規範化,我們以與時間無關的方式推理xy的值。某一段時間內,他們是一個將來的值。

這第二層promise是sum(...)創造的(通過 Promise.all([ ... ])),然後返回promise。通過呼叫then(…)來等待。當 sum(…) 操作完成時,sum 傳入的兩個 Promise 都執行完後,可以列印出來了。這裡隱藏了在sum(…)中等待x和y未來值的邏輯。

注意: 在這個sum(...)裡面,這個Promise.all([...])呼叫建立一個 promise(等待 promiseX 和 promiseY 它們resolve)。然後鏈式呼叫 .then(...)方法裡再的建立了另一個 Promise,然後把(values[0] + values[1]) 進行求和並返回。

因此,我們在sum(...)末尾呼叫then(...)方法——實際上是在返回的第二個 Promise 上的執行,而不是由Promise.all([ ... ])建立的Promise。此外,雖然沒有在第二個 Promise 結束時再呼叫 then方法 ,其時這裡也建立一個 Promise。

Promise.then(…) 實際上可以使用兩個函式,第一個函式用於執行成功的操作,第二個函式用於處理失敗的操作:
如果在獲取x或y時出現錯誤,或者在新增過程中出現某種失敗,sum(…) 返回的 Promise將被拒絕,傳遞給then(…)的第二個回撥錯誤處理程式將從 Promise 接收失敗的資訊。

從外部看,由於 Promise 封裝了依賴於時間的狀態(等待底層值的完成或拒絕,Promise 本身是與時間無關的),它可以按照可預測的方式組成,不需要開發者關心時序或底層的結果。 Promise一旦resolve,此刻在外部他就成了不可變的值——然後就可以根據需求多次觀察。

鏈式呼叫對於你來說是真的有用:

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)建立一個2000ms後將被實現(fulfill)的promise,然後通過第一個then(...)來接收回撥訊號,在這裡面也返回一個promise,通過第二個then(...)的promise來等待2000ms的promise。

注意: 因為一個Promise一旦被resolved,在外面看來就成了不可變了,所以現在可以把它安全的傳遞到程式的任何地方。因為它不能被意外地或惡意地修改,這一點在多個地方觀察一個promise時尤其正確。一方不可能影響另一方觀察promise結果的能力,不變性聽起來像是一個學術話題,但它實際上是promise設計最基本和最重要的方面之一,不應該被隨意忽略。

用不用Promise

關於 Promise 的一個重要細節是要確定某個值是否是一個實際的Promise。換句話說,它是否具有像Promise一樣行為?

我們知道 Promise 是由new Promise(…)語法構造的,你可能認為p instanceof Promise是一個足夠可以判斷的型別,嗯,不完全是!

這主要是因為可以從另一個瀏覽器視窗(例如iframe)接收Promise值,而該視窗或框架具有自己的Promise值,與當前視窗或框架中的Promise 值不同,所以該檢查將無法識別 Promise 例項。

此外,庫或框架可以選擇性的封裝自己的Promise,而不使用原生 ES6 的Promise 來實現。事實上,很可能在老瀏覽器的庫中沒有 Promise。

捕獲錯誤和異常

如果在 Promise 建立中,出現了一個javascript異常錯誤(TypeError或者ReferenceError),這個異常會被捕捉,並且使這個 promise 被拒絕。
比如:

var p = new Promise(function(resolve, reject){
    foo.bar();	  // `foo` is not defined, so error!'foo'沒有定義
    resolve(374); // never gets here :( 不會到達這兒
});

p.then(
    function fulfilled(){
        // never gets here :(不會到達這兒
    },
    function rejected(err){
        // `err` will be a `TypeError` exception object
	// from the `foo.bar()` line.
    }
);
複製程式碼

但是,如果在呼叫 then(…)方法中出現了JS異常錯誤,那麼會發生什麼情況呢?即使它不會丟失,你可能會發現它們的處理方式有點令人吃驚,直到你挖得更深一點:

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

p.then(function fulfilled(message){
    foo.bar();
    console.log(message);   // never reached不會到達這兒
},
    function rejected(err){
        // never reached 不會到達這兒
    }
);
複製程式碼

看起來foo.bar()中的異常確實被吞噬了,不過,它不是。然而,還有一些更深層次的問題,我們沒有注意到。 p.then(…) 呼叫本身返回另一個 Promise,該 Promise 將被 TypeError 異常拒絕。

處理未捕獲異常

許多人會說,還有其他更好的方法。

一個常見的建議是,Promise 應該新增一個 done(…),這實際上是將 Promise 鏈標記為 “done”。done(…)不會建立並返回 Promise ,因此傳遞給 done(..) 的回撥顯然不會將問題報告給不存在的連結 Promise 。

Promise 物件的回撥鏈,不管以then方法或catch方法結尾,要是最後一個方法丟擲錯誤,都有可能無法捕捉到(因為Promise內部的錯誤不會冒泡到全域性)。因此,我們可以提供一個 done 方法,總是處於回撥鏈的尾端,保證丟擲任何可能出現的錯誤。

var p = Promise.resolve(374);

p.then(function fulfilled(msg){
    // numbers don't have string functions,
    // so will throw an error
    console.log(msg.toLowerCase());
})
.done(null, function() {
    // If an exception is caused here, it will be thrown globally 
});
複製程式碼

ES8中有什麼變化 ?Async/await (非同步/等待)

JavaScript ES8引入了async/await,這使得使用Promise的工作更容易。這裡將簡要介紹async/await 提供的可能性以及如何利用它們編寫非同步程式碼。

使用 async 宣告非同步函式。這個函式返回一個AsyncFunction 物件。AsyncFunction 物件表示該函式中包含的程式碼是非同步函式。

呼叫使用 async 宣告函式時,它返回一個Promise。當這個函式返回一個值時,這個值只是一個普通值而已,這個函式內部將自動建立一個promise,並使用函式返回的值進行解析。當這個函式丟擲異常時,Promise 將被丟擲的值拒絕。

使用 async 宣告函式時可以包含一個await符號,await暫停這個函式的執行並等待傳遞的 Promise 的解析完成,然後恢復這個函式的執行並返回解析後的值。

async/wait 的目的是簡化使用promise的行為

看下下面的列子:

// Just a standard JavaScript function
//標準的js寫法
function getNumber1() {
    return Promise.resolve('374');
}
// This function does the same as getNumber1
//這個函式做了相同的事情,返回一個promise
async function getNumber2() {
    return 374;
}
複製程式碼

類似地,函式丟擲異常相當於函式返回的promise被reject了:

//這兩個函式一樣
function f1() {
    return Promise.reject('Some error');
}
async function f2() {
    throw 'Some error';
}
複製程式碼

await關鍵詞只能使用在async函式中,允許去同步等待一個promise執行。如果在async外面使用promise,仍然需要使用then回撥。

async function loadData() {
    // `rp` is a request-promise function.
    //`rp` 是一個請求promise函式
    var promise1 = rp('https://api.example.com/endpoint1');
    var promise2 = rp('https://api.example.com/endpoint2');
   
    // Currently, both requests are fired, concurrently and
    // now we'll have to wait for them to finish
    //現在,兩個請求都被執行,必須等到他們執行完成
    var response1 = await promise1;
    var response2 = await promise2;
    return response1 + ' ' + response2;
}
// Since, we're not in an `async function` anymore
// we have to use `then`.
//由於不再非同步函式當中,我們必須使用`then`
loadData().then(() => console.log('Done'));
複製程式碼

還可以使用“非同步函式表示式”定義非同步函式。非同步函式表示式與非同步函式語句非常相似,語法也幾乎相同。非同步函式表示式和非同步函式語句之間的主要區別是函式名,可以在非同步函式表示式中省略函式名來建立匿名函式。非同步函式表示式可以用作宣告(立即呼叫的函式表示式),一旦定義它就會執行。

就像這樣:

var loadData = async function() {
    // `rp` is a request-promise function.
    var promise1 = rp('https://api.example.com/endpoint1');
    var promise2 = rp('https://api.example.com/endpoint2');
   
    // Currently, both requests are fired, concurrently and
    // now we'll have to wait for them to finish
    var response1 = await promise1;
    var response2 = await promise2;
    return response1 + ' ' + response2;
}
複製程式碼

更重要的是,在所有主流的瀏覽器都支援 async/await:

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式
最後,重要的是不要盲目選擇編寫非同步程式碼的“最新”方法。理解非同步 JavaScript 的內部結構非常重要,瞭解為什麼非同步JavaScript如此關鍵,並深入理解所選擇的方法的內部結構。與程式設計中的其他方法一樣,每種方法都有優點和缺點。

編寫高度可維護、穩定的非同步程式碼

1.簡化程式碼

使用 async/await 可以編寫更少的程式碼。每次使用async/await時,都會跳過一些不必·要的步驟:使用.then,建立一個匿名函式來處理響應:

// `rp` is a request-promise function.
rp('https://api.example.com/endpoint1').then(function(data) {
 // …
});
複製程式碼

與:

// `rp` is a request-promise function.
var response = await rp(‘https://api.example.com/endpoint1');
複製程式碼

2.錯誤處理

Async/wait 可以使用相同的程式碼結構(眾所周知的try/catch語句)處理同步和非同步錯誤。看看它是如何與 Promise 結合的:

function loadData() {
    try { // Catches synchronous errors.
        getJSON().then(function(response) {
            var parsed = JSON.parse(response);
            console.log(parsed);
        }).catch(function(e) { // Catches asynchronous errors
            console.log(e); 
        });
    } catch(e) {
        console.log(e);
    }
}
view raw
複製程式碼

與:

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

3.條件處理

用async/ wait編寫條件程式碼要簡單得多:

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,那麼你知道除錯它們是一場噩夢。例如,如果在一個程式中設定了一個斷點,然後阻塞並使用除錯快捷方式(如“停止”),偵錯程式將不會移動到下面,因為它只“逐步”執行同步程式碼。使用async/wait,您可以逐步完成wait呼叫,就像它們是正常的同步函式一樣。

後續文件翻譯會陸續跟進!!

歡迎關注玄說前端公眾號:

【譯】JavaScript的工作原理:事件迴圈及非同步程式設計的出現和 5 種更好的 async/await 程式設計方式

相關文章