摘要:函數(shù)會在之后的某個時刻觸發(fā)事件定時器。事件循環(huán)中的這樣一次遍歷被稱為一個。執(zhí)行完畢并出棧。當(dāng)定時器過期,宿主環(huán)境會把回調(diào)函數(shù)添加至事件循環(huán)隊列中,然后,在未來的某個取出并執(zhí)行該事件。
原文請查閱這里,略有改動。
本系列持續(xù)更新中,Github 地址請查閱這里。
這是 JavaScript 工作原理的第四章。
現(xiàn)在,我們將會通過回顧單線程環(huán)境下編程的弊端及如何克服這些困難以創(chuàng)建令人驚嘆的 JavaScript 交互界面來展開第一篇文章。老規(guī)矩,我們將會在本章末尾分享 5 條利用 async/await 編寫更簡潔代碼的小技巧。
單線程的局限性在第一篇文章開頭,我們考慮了一個問題即當(dāng)調(diào)用棧中含有需要長時間運行的函數(shù)調(diào)用的時候會發(fā)生什么。
譬如,試想下,在瀏覽器中運行著一個復(fù)雜的圖片轉(zhuǎn)化算法。
恰好此時調(diào)用棧中有函數(shù)需要執(zhí)行,此時瀏覽器將會被阻塞,它不能夠做其它任何事情。這意味著,瀏覽器會沒有響應(yīng),不能夠進行渲染和運行其它代碼。這將會帶來問題-程序界面將不再高效和令人愉悅。
程序沒有響應(yīng)。
在某些情況下,這或許沒什么大不了的。但是,這可能會造成更加嚴重的問題。一旦瀏覽器在調(diào)用棧中同時運行太多的任務(wù)的時候,瀏覽器會很長時間停止響應(yīng)。到了那個時候,大多數(shù)瀏覽器會拋出一個錯誤,詢問你是否關(guān)閉網(wǎng)頁。
這很丑陋且它完全摧毀了程序的用戶體驗。
JavaScript 程序組件你可能會在單一的 .js 文件中書寫 JavaScript 程序,但是程序是由多個代碼塊組成的,當(dāng)前只有一個代碼塊在運行,其它代碼塊將在隨后運行。最常見的塊狀單元是函數(shù)。
大多數(shù) JavaScript 菜鳥有可能需要理解的問題即之后運行表示的是并不是必須嚴格且立即在現(xiàn)在之后執(zhí)行。換句話說即,根據(jù)定義,現(xiàn)在不能夠運行完畢的任務(wù)將會異步完成,這樣你就不會不經(jīng)意間遇到以上提及的 UI 阻塞行為。
看下如下代碼:
// ajax 為一個庫提供的任意 ajax 函數(shù) var response = ajax("https://example.com/api"); console.log(response); // `response` 將不會有數(shù)據(jù)返回
可能你已經(jīng)知道標(biāo)準的 ajax 請求不會完全同步執(zhí)行完畢,意即在代碼運行階段,ajax(..) 函數(shù)不會返回任何值給 response 變量。
獲得異步函數(shù)返回值的一個簡單方法是使用回調(diào)函數(shù)。
ajax("https://example.com/api", function(response) { console.log(response); // `response` 現(xiàn)在有值 });
只是要注意一點:即使可以也永遠不要發(fā)起同步 ajax 請求。如果發(fā)起同步 ajax 請求,JavaScript 程序的 UI 將會被阻塞-用戶不能夠點擊,輸入數(shù)據(jù),跳轉(zhuǎn)或者滾動。這將會凍結(jié)任何用戶交互體驗。這是非常糟糕。
以下示例代碼,但請別這樣做,這會毀掉網(wǎng)頁:
// 假設(shè)你使用 jQuery jQuery.ajax({ url: "https://api.example.com/endpoint", success: function(response) { // 成功回調(diào). }, async: false // 同步 });
我們以 Ajax 請求為例。你可以異步執(zhí)行任意代碼。
你可以使用 setTimeout(callback, milliseconds) 函數(shù)來異步執(zhí)行代碼。setTimeout 函數(shù)會在之后的某個時刻觸發(fā)事件(定時器)。如下代碼:
function first() { console.log("first"); } function second() { console.log("second"); } function third() { console.log("third"); } first(); setTimeout(second, 1000); // 1 秒后調(diào)用 second 函數(shù) third();
控制臺輸出如下:
first third second事件循環(huán)詳解
我們將會以一個有些讓人費解的問題開始-盡管允許異步執(zhí)行 JavaScript 代碼(比如之前討論的 setTimetout),但是直到 ES6,實際上 JavaScript 本身并沒有集成任何直接的異步編程概念。JavaScript 引擎只允許在任意時刻執(zhí)行單個的程序片段。
可以查看之前的文章來了解 JavaScript 引擎的工作原理。
那么, JS 引擎是如何執(zhí)行程序片段的呢?實際上,JS 引擎并不是隔離運行的-它運行在一個宿主環(huán)境中,對大多數(shù)開發(fā)者來說是典型的 web 瀏覽器或者 Node.js。實際上,現(xiàn)在 JavaScript 廣泛應(yīng)用于從機器到電燈泡的各種設(shè)備之中。每個設(shè)備代表了 JS 引擎的不同類型的宿主環(huán)境。
所有宿主環(huán)境都含有一個被稱為事件循環(huán)的內(nèi)置機制,隨著時間的推移,事件循環(huán)會執(zhí)行程序中多個代碼片段,每次都會調(diào)用 JS 引擎。
這意味著 JS 引擎只是任意 JS 代碼的按需執(zhí)行環(huán)境。這是一個封閉的環(huán)境,在其中進行事件的調(diào)度(運行JS 代碼)。
所以,打個比方,當(dāng) JavaScript 程序發(fā)起 Ajax 請求來從服務(wù)器獲得數(shù)據(jù),你在回調(diào)函數(shù)中書寫 "response" 代碼,JS 引擎會告訴宿主環(huán)境:
"嘿,我現(xiàn)在要掛起執(zhí)行了,現(xiàn)在當(dāng)你完成網(wǎng)絡(luò)請求的時候且返回了數(shù)據(jù),請執(zhí)行回調(diào)函數(shù)。"
之后瀏覽器會監(jiān)聽從網(wǎng)絡(luò)中返回的數(shù)據(jù),當(dāng)有數(shù)據(jù)返回的時候,它會通過把回調(diào)函數(shù)插入事件循環(huán)以便調(diào)度執(zhí)行。
讓我們看下如下圖示:
你可以在之前的文章中閱讀更多關(guān)于動態(tài)內(nèi)存管理和調(diào)用棧的信息。
什么是網(wǎng)頁 API ?本質(zhì)上,你沒有權(quán)限訪問這些線程,你只能夠調(diào)用它們。它們是瀏覽器自帶的,且可以在瀏覽器中進行并發(fā)操作。如果你是個 Node.js 開發(fā)者,這些是 C++ APIs。
說了那么多,事件循環(huán)到底是啥?
事件循環(huán)只有一項簡單的工作-監(jiān)測調(diào)用棧和回調(diào)隊列。如果調(diào)用棧是空的,它會從回調(diào)隊列中取得第一個事件然后入棧,并有效地執(zhí)行該事件。
事件循環(huán)中的這樣一次遍歷被稱為一個 tick。每個事件就是一個回調(diào)函數(shù)。
console.log("Hi"); setTimeout(function cb1() { console.log("cb1"); }, 5000); console.log("Bye");
讓我們執(zhí)行這段代碼,然后看看會發(fā)生什么:
1.空狀態(tài)。瀏覽器控制臺是空的,調(diào)用棧也是空的。
2.console.log("Hi") 入棧。
3.執(zhí)行 console.log("Hi")。
4.console.log("Hi") 出棧
setTimeout(function cb1() { ... }) 入棧。
6.執(zhí)行 setTimeout(function cb1() { ... }),瀏覽器創(chuàng)建定時器作為網(wǎng)頁 API 的一部分并將會為你處理倒計時。
7.setTimeout(function cb1() { ... }) 執(zhí)行完畢并出棧。
8.console.log("Bye") 入棧。
9.執(zhí)行 console.log("Bye")。
10.console.log("Bye") 出棧。
11.至少 5 秒之后,定時器結(jié)束運行并把 cb1 回調(diào)添加到回調(diào)隊列。
12.事件循環(huán)從回調(diào)隊列中獲得 cb1 函數(shù)并且將其入棧。
13.運行 cb1 函數(shù)并將 console.log("cb1") 入棧。
14.執(zhí)行 console.log("cb1")。
15.console.log("cb1") 出棧。
16.cb1 出棧
錄像快速回放:
令人感興趣的是,ES6 規(guī)定事件循環(huán)如何工作的,這意味著從技術(shù)上講,它在 JS 引擎負責(zé)的范圍之內(nèi),而 JS 引擎將不再只是扮演著宿主環(huán)境的角色。ES6 中 Promise 的出現(xiàn)是導(dǎo)致改變的主要原因之一,因為 ES6 要求有權(quán)限直接細粒度地控制事件循環(huán)隊列中的調(diào)度操作(之后會深入探討)。
setTimeout(…) 工作原理需要注意的是 setTimeout(…) 并沒有自動把回調(diào)添加到事件循環(huán)隊列。它創(chuàng)建了一個定時器。當(dāng)定時器過期,宿主環(huán)境會把回調(diào)函數(shù)添加至事件循環(huán)隊列中,然后,在未來的某個 tick 取出并執(zhí)行該事件。查看如下代碼:
setTimeout(myCallback, 1000);
這并不意味著 1 秒之后會執(zhí)行 myCallback 回調(diào)而是在 1 秒后將其添加到回調(diào)隊列。然而,該隊列有可能在之前就添加了其它的事件-所以回調(diào)就會被阻塞。
有相當(dāng)一部分的文章和教程開始會建議你使用 setTimeout(callback, 0) 來書寫 JavaScript 異步代碼。那么,現(xiàn)在你明白了事件循環(huán)和 setTimeout 的原理:調(diào)用 setTimeout 把其第二個參數(shù)設(shè)置為 0 表示延遲執(zhí)行回調(diào)直到調(diào)用棧被清空。
查看如下代碼:
console.log("Hi"); setTimeout(function() { console.log("callback"); }, 0); console.log("Bye");
雖然定時時間設(shè)定為 0, 但是控制臺中的結(jié)果將會如下顯示:
Hi Bye callbackES6 作業(yè)概念
ES6 介紹了一個被稱為『作業(yè)隊列』的概念。它位于事件循環(huán)隊列的頂部。你極有可能在處理 Promises(之后會介紹) 的異步行為的時候無意間接觸到這一概念。
現(xiàn)在我們將會接觸這個概念,以便當(dāng)討論 Promises 的異步行為之后,理解如何調(diào)度和處理這些行為。
像這樣想象一下:作業(yè)隊列是附加于事件循環(huán)隊列中每個 tick 末尾的隊列。事件循環(huán)的一個 tick 所產(chǎn)生的某些異步操作不會導(dǎo)致添加全新的事件到事件循環(huán)隊列中,但是反而會在當(dāng)前 tick 的作業(yè)隊列末尾添加一個作業(yè)項。
這意味著,你可以添加延時運行其它功能并且你可以確保它會在其它任何功能之前立刻執(zhí)行。
一個作業(yè)也可以在同一隊列末尾添加更多的作業(yè)。理論上講,存在著作業(yè)循環(huán)的可能性(比如作業(yè)不停地添加其它作業(yè))。
為了無限循環(huán),就會饑餓程序所需要的資源直到下一個事件循環(huán) tick。從概念上講,這類似于在代碼里面書寫耗時或者死循環(huán)(類似 while(true))。
作業(yè)是有些類似于 setTimeout(callback, 0) 小技巧,但是是以這樣的方式實現(xiàn)的,它們擁有明確定義和有保證的執(zhí)行順序:之后且盡快地執(zhí)行。
回調(diào)正如你已知的那樣,回調(diào)函數(shù)是 JavaScript 程序中用來表示和進行異步操作的最常見方法。的確,回調(diào)是 JavaScript 語言中最為重要的異步模式。無數(shù)的 JS 程序,甚至非常復(fù)雜的那些,都是建立在回調(diào)函數(shù)之上的。
回調(diào)并不是沒有缺點。許多開發(fā)者試圖找到更好的異步模式。然而,如果你不理解底層的原理而想要高效地使用任何抽象化的語法這是不可能的。
在接下來的章節(jié)中,我們將會深入探究這些抽象語法并理解更復(fù)雜的異步模式的必要性。
嵌套回調(diào)查看以下示例:
listen("click", function (e){ setTimeout(function(){ ajax("https://api.example.com/endpoint", function (text){ if (text == "hello") { doSomething(); } else if (text == "world") { doSomethingElse(); } }); }, 500); });
我們有三個鏈式嵌套函數(shù),每個函數(shù)代表一個異步操作。
這類代碼通常被稱為『回調(diào)地獄』。但是,實際上『回調(diào)地獄』和代碼嵌套及縮進沒有任何關(guān)系。這是一個更加深刻的問題。
首先,我們監(jiān)聽點擊事件,然后,等待定時器執(zhí)行,最后等待 Ajax 返回數(shù)據(jù),在 Ajax 返回數(shù)據(jù)的時候,可以重復(fù)執(zhí)行這一過程。
乍一眼看上去,可以上把以上具有異步特性的代碼拆分為按步驟執(zhí)行的代碼,如下所示:
listen("click", function (e) { // .. });
之后:
setTimeout(function(){ // .. }, 500);
再后來:
ajax("https://api.example.com/endpoint", function (text){ // .. });
最后:
if (text == "hello") { doSomething(); } else if (text == "world") { doSomethingElse(); }
因此,以這樣順序執(zhí)行的方式來表示異步代碼看起來一氣呵成,應(yīng)該有這樣的方法吧?
Promises查看如下代碼:
var x = 1; var y = 2; console.log(x + y);
這個很直觀:計算出 x 和 y 的值然后在控制臺打印出來。但是,如果 x 或者 y 的初始值是不存在的且不確定的呢?假設(shè),在表達式中使用 x 和 y 之前,我們需要從服務(wù)器得到 x 和 y 的值。想象下,我們擁有函數(shù) loadX 和 loadY 分別從服務(wù)器獲取 x 和 y 的值。然后,一旦獲得 x 和 y 的值,就可以使用 sum 函數(shù)計算出和值。
類似如下這樣:
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` 值的函數(shù) function fetchX() { // .. } // 同步或異步獲取 `y` 值的函數(shù) function fetchY() { // .. } sum(fetchX, fetchY, function(result) { console.log(result); });
這里需要記住的一點是-在代碼片段中,x 和 y 是未來值,我們用 sum(..)(從外部)來計算和值,但是并沒有關(guān)注 x 和 y 是否馬上同時有值。
當(dāng)然嘍,這個粗糙的基于回調(diào)的技術(shù)還有很多需要改進的地方。這只是理解推出未來值而不用擔(dān)心何時有返回值的好處的一小步。
Promise 值讓我們簡略地看一下如何用 Promises 來表示 x+y :
function sum(xPromise, yPromise) { // `Promise.all([ .. ])` 包含一組 Promise, // 并返回一個新的 Promise 來等待所有 Promise 執(zhí)行完畢 return Promise.all([xPromise, yPromise]) // 當(dāng)新 Promise 解析完畢,就可以同時獲得 `x` 和 `y` 的值并相加。 .then(function(values){ // `values` 是之前解析 promises 返回的消息數(shù)組 return values[0] + values[1]; } ); } // `fetchX()` and `fetchY()` 返回 promise 來取得各自的返回值,這些值返回是無時序的。 sum(fetchX(), fetchY()) // 獲得一個計算兩個數(shù)和值的 promise,現(xiàn)在,就可以鏈式調(diào)用 `then(...)` 來處理返回的 promise。 .then(function(sum){ console.log(sum); });
以上代碼片段含有兩種層次的 Promise。
fetchX() 和 fetchY() 都是直接調(diào)用,它們的返回值(promises!)都被傳入 sum(…) 作為參數(shù)。雖然這些 promises 的 返回值也許會在現(xiàn)在或之后返回,但是無論如何每個 promise 都具有相同的異步行為。我們可以的推算 x 和 y 是與時間無關(guān)的值。暫時稱他們?yōu)槲磥碇怠?/p>
第二層次的 promise 是由 sum(…) (通過 Promise.all([ ... ]))所創(chuàng)建和返回的,然后通過調(diào)用 then(…) 來等待 promise 的返回值。當(dāng) sum(…) 運行結(jié)束,返回 sum 未來值然后就可以打印出來。我們在 sum(…) 內(nèi)部隱藏了等待未來值 x 和 y 的邏輯。
注意:在 sum(…) 內(nèi)部,Promise.all([ … ])創(chuàng)建了一個 promise(在等待 promiseX 和 promiseY 解析之后)。鏈式調(diào)用 .then(…) 創(chuàng)建了另一個 promise,該 promise 會由代碼 values[0] + values[1] 立刻進行解析(返回相加結(jié)果)。因此,在代碼片段的末尾即 sum(…) 的末尾鏈式調(diào)用 then(…)-實際上是在操作第二個返回的 promise 而不是第一個由 Promise.all([ ... ]) 創(chuàng)建返回的 promise。同樣地,雖然我們沒有在第二個then(…) 之后進行鏈式調(diào)用,但是它也創(chuàng)建了另一個 promise,我們可以選擇觀察/使用該 promise。我們將會在本章的隨后內(nèi)容中進行詳細地探討 promise 的鏈式調(diào)用相關(guān)。
在 Promises 中,實際上 then(…) 函數(shù)可以傳入兩個函數(shù)作為參數(shù),第一個函數(shù)是成功函數(shù),第二個是失敗函數(shù)。
sum(fetchX(), fetchY()) .then( // 成功句柄 function(sum) { console.log( sum ); }, // 拒絕句柄 function(err) { console.error( err ); // bummer! } );
當(dāng)獲取 x 或者 y 出現(xiàn)錯誤或者計算和值的時候出現(xiàn)錯誤,sum(…) 返回的 promise 將會失敗,傳入 then(…) 作為第二個參數(shù)的回調(diào)錯誤處理程序?qū)邮?promise 的返回值。
因為 Promise 封裝了時間相關(guān)的狀態(tài)-等待外部的成功或者失敗的返回值,Promise 本身是與時間無關(guān)的,這樣就能夠以可預(yù)測的方式組成(合并) Promise 而不用關(guān)心時序或者返回結(jié)果。
除此之外,一旦 Promise 解析完成,它就會一直保持不可變的狀態(tài)且可以被隨意觀察。
鏈式調(diào)用 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); }) // ...
調(diào)用 delay(2000) 創(chuàng)建一個將在 2 秒后返回成功的 promise,然后,從第一個 then(…) 的成功回調(diào)函數(shù)中返回該 promise,這會導(dǎo)致第二個 then(…) 返回的 promise 等待 2 秒后返回成功的 promise。
Note:因為一個 promise 一旦解析其狀態(tài)就不可以從外部改變,由于它的狀態(tài)不可以被隨意修改,所以可以安全地把狀態(tài)值隨意分發(fā)給任意第三方。當(dāng)涉及多方觀察 Promise 的返回結(jié)果時候更是如此。一方影響另一方觀察 Promise 返回結(jié)果的能力是不可能。不可變性聽起來像是個晦澀的科學(xué)課題,但是,實際上這是 Promise 最根本和重要的方面,你得好好研究研究。
Promise 使用時機Promise 的一個重要細節(jié)即確定某些值是否是真正的 Promise。換句話說,這個值是否具有 Promise 的行為。
我們知道可以利用 new Promise(…) 語法來創(chuàng)建 Promise,然后,你會認為使用 p instanceof Promise 來檢測某個對象是否是 Promise 類的實例。然而,并不全然如此。
主要的原因在于你可以從另一個瀏覽器窗口(比如 iframe)獲得 Promise 實例,iframe 中的 Promise 不同于當(dāng)前瀏覽器窗口或框架中的 Promise,因此,會導(dǎo)致檢測 Promise 實例失敗。
除此之外,庫或框架或許會選擇使用自身自帶的 Promise 而不是原生的 ES6 實現(xiàn)的 Promise。實際工作中,你可以使用庫自帶的 Promise 來兼容不支持 Promise 的老版本瀏覽器。
異常捕獲如果在創(chuàng)建 Promise 或者是在觀察解析 Promise 返回結(jié)果的任意時刻,遇到了諸如 TypeError 或者 ReferenceError 的 JavaScript 錯誤異常,這個異常會被捕獲進而強制 Promise 為失敗狀態(tài)。
比如:
var p = new Promise(function(resolve, reject){ foo.bar(); // `foo` 未定義,產(chǎn)生錯誤! resolve(374); // 永不執(zhí)行 :( }); p.then( function fulfilled(){ // 永不執(zhí)行 :( }, function rejected(err){ // `err` 會是一個 `TypeError` 異常對象 // 由于 `foo.bar()` 代碼行. } );
但是,如果 Promise 成功解析了而在成功解析的監(jiān)聽函數(shù)(then(…) 注冊回調(diào))中拋出 JS 運行錯誤會怎么樣?仍然可以捕捉到該異常,但或許你會發(fā)現(xiàn)處理這些異常的方式有些讓人奇怪。直到深入理解其中原理:
var p = new Promise( function(resolve,reject){ resolve(374); }); p.then(function fulfilled(message){ foo.bar(); console.log(message); // 永不執(zhí)行 }, function rejected(err){ // 永不執(zhí)行 } );
看起來 foo.bar() 拋出的錯誤異常真的被捕獲到了。然而,事實上并沒有。然而,深入理解你會發(fā)現(xiàn)我們沒有監(jiān)測到其中一些錯誤。p.then(…) 調(diào)用本身返回另一個 promise,該 promise 會返回 TypeError 類型的異常失敗信息。
拓展一下以上的說明,這是原文沒有的。
var p = new Promise( function(resolve,reject){ resolve(374); }); p.then(function fulfilled(message){ foo.bar(); console.log(message); // 永不執(zhí)行 }, function rejected(err){ // 永不執(zhí)行 } ).then( function() {}, function(err) { console.log("err", err);} );
如上代碼所示就可以真正捕獲到 promise 成功解析回調(diào)函數(shù)里面的代碼錯誤。
處理未捕獲的異常有其它許多據(jù)說更好的處理異常的技巧。
普遍的做法是為 Promises 添加 done(..) 回調(diào),本質(zhì)上這會標(biāo)記 promise 鏈的狀態(tài)為 "done."。done(…) 并不會創(chuàng)建和返回 promise,因此,當(dāng)不存在鏈式 promise 的時候,傳入 done(..) 的回調(diào)顯然并不會拋出錯誤。
和未捕獲的錯誤狀況一樣:任何在 done(..) 失敗處理函數(shù)中的異常都將會被拋出為全局錯誤(基本上是在開發(fā)者控制臺)。
var p = Promise.resolve(374); p.then(function fulfilled(msg){ // 數(shù)字沒有字符類的函數(shù),所以會報錯 console.log(msg.toLowerCase()); }) .done(null, function() { // 若發(fā)生錯誤,將會拋出全局錯誤 });ES8 中的 Async/await
JavaScript ES8 中介紹了 async/await,這使得處理 Promises 更加地容易。我們將會簡要介紹 async/await 的所有可能姿勢并利用其來書寫異步代碼。
那么,讓我們瞧瞧 async/await 工作原理。
使用 async 函數(shù)定義一個異步函數(shù)。該函數(shù)會返回異步函數(shù)對象。AsyncFunction 對象表示在異步函數(shù)中運行其內(nèi)部代碼。
當(dāng)調(diào)用異步函數(shù)的時候,它會返回一個 Promise。異步函數(shù)返回值并非 Promise,在函數(shù)過程中會自動創(chuàng)建一個 Promise 并使用函數(shù)的返回值來解析該 Promise。當(dāng) async 函數(shù)拋出異常,Promise 失敗回調(diào)會獲取拋出的異常值。
async 函數(shù)可以包含一個 await 表達式,這樣就可以暫停函數(shù)的執(zhí)行來等待傳入的 Promise 的返回結(jié)果,之后重啟異步函數(shù)的執(zhí)行并返回解析值。
你可以把 JavaScript 中的 Promise 看作 Java 中的 Future 或 C# 中的 Task。
async/await 本意是用來簡化 promises 的使用。
看下如下代碼:
// 標(biāo)準 JavaScript 函數(shù) function getNumber1() { return Promise.resolve("374"); } // 和 getNumber1 一樣 async function getNumber2() { return 374; }
類似地,拋出異常的函數(shù)等價于返回失敗的 promises。
function f1() { return Promise.reject("Some error"); } async function f2() { throw "Some error"; }
await 關(guān)鍵字只能在 async 函數(shù)中使用并且允許你同步等待 Promise。如果在 async 函數(shù)外使用 promises,我們?nèi)匀槐仨毷褂?then 回調(diào)。
async function loadData() { // `rp` 是個發(fā)起 promise 的函數(shù)。 var promise1 = rp("https://api.example.com/endpoint1"); var promise2 = rp("https://api.example.com/endpoint2"); // 現(xiàn)在,并發(fā)請求兩個 promise,現(xiàn)在我們必須等待它們結(jié)束運行。 var response1 = await promise1; var response2 = await promise2; return response1 + " " + response2; } // 因為不再使用 `async function`,所以必須使用 `then`。 loadData().then(() => console.log("Done"));
你也可以使用異步函數(shù)表達式來定義異步函數(shù)。異步函數(shù)表達式擁有和異步函數(shù)語句相近的語法。異步函數(shù)表達式和異步函數(shù)語句的主要區(qū)別在于函數(shù)名,異步函數(shù)表達式可以忽略函數(shù)名來創(chuàng)建匿名函數(shù)。異步函數(shù)表達式可以被用作 IIFE(立即執(zhí)行函數(shù)表達式),可以在定義的時候立即運行。
像這樣:
var loadData = async function() { // `rp` 是個發(fā)起 promise 的函數(shù)。 var promise1 = rp("https://api.example.com/endpoint1"); var promise2 = rp("https://api.example.com/endpoint2"); // 現(xiàn)在,并發(fā)請求兩個 promise,現(xiàn)在我們必須等待它們結(jié)束運行。 var response1 = await promise1; var response2 = await promise2; return response1 + " " + response2; }
更為重要的是,所有的主流瀏覽器都支持 async/await。
如果該兼容性不符合你的需求,你可以使用諸如 Babel 和 TypeScript 的 JS 轉(zhuǎn)譯器來轉(zhuǎn)換為自己需要的兼容程度。
最后要說的是,不要盲目地使用最新的技術(shù)來寫異步代碼。理解 JavaScript 中 async 的內(nèi)部原理是非常重要的,學(xué)習(xí)為什么深入理解所選擇的方法是很重要的。正如編程中的其它東西一樣,每種技術(shù)都有其優(yōu)缺點。
書寫高可用,強壯的異步代碼的 5 條小技巧1.簡潔:使用 async/await 可以讓你寫更少的代碼。每次書寫 async/await 代碼,你都可以跳過書寫一些不必要的步驟: 比如不用寫 .then 回調(diào),創(chuàng)建匿名函數(shù)來處理返回值,命名回調(diào)返回值。
// `rp` 是個發(fā)起 promise 的工具函數(shù)。 rp(‘https://api.example.com/endpoint1").then(function(data) { // … });
對比:
// `rp` 是個發(fā)起 promise 的工具函數(shù) var response = await rp(‘https://api.example.com/endpoint1");
2.錯誤處理:Async/await 允許使用日常的 try/catch 代碼結(jié)構(gòu)體來處理同步和異步錯誤。看下和 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 返回的錯誤堆棧中無法得知發(fā)生錯誤的地方。看如下代碼:
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.調(diào)試:如果使用 promise,你就會明白調(diào)試它們是一場噩夢。例如,如果你在 .then 代碼塊中設(shè)置一個斷點并且使用諸如 "stop-over" 的調(diào)試快捷鍵,調(diào)試器不會移動到下一個 .then 代碼塊,因為調(diào)試器只會步進同步代碼。
使用 async/await 你可以就像同步代碼那樣步進到下一個 await 調(diào)用。
不僅是程序本身還有庫,書寫異步 JavaScript 代碼都是相當(dāng)重要的。
參考資源:
https://github.com/getify/You...
https://github.com/getify/You...
http://nikgrozev.com/2017/10/...
本系列持續(xù)更新中,Github 地址請查閱這里。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/94863.html
摘要:事件循環(huán)從回調(diào)隊列中獲取并將其推入調(diào)用堆棧。執(zhí)行從調(diào)用堆棧中移除從調(diào)用堆棧中移除快速回顧值得注意的是,指定了事件循環(huán)應(yīng)該如何工作,這意味著在技術(shù)上它屬于引擎的職責(zé)范圍,不再僅僅扮演宿主環(huán)境的角色。 此篇是 JavaScript是如何工作的第四篇,其它三篇可以看這里: JavaScript是如何工作的:引擎,運行時和調(diào)用堆棧的概述! JavaScript是如何工作的:深入V8引擎&編寫...
摘要:事件循環(huán)從回調(diào)隊列中獲取并將其推送到調(diào)用堆棧。如何工作請注意,不會自動將您的回調(diào)函數(shù)放到事件循環(huán)隊列中。它設(shè)置了一個計時器,當(dāng)計時器到期時,環(huán)境將您的回調(diào)函數(shù)放入事件循環(huán)中,以便將來的某個事件會將其選中并執(zhí)行它。 我們將通過回顧第一篇文章中單線程編程的缺點,然后在討論如何克服它們來構(gòu)建令人驚嘆的JavaScript UI。在文章結(jié)尾處,我們將分享5個關(guān)于如何使用async / awai...
摘要:的翻譯文檔由的維護很多人說,阮老師已經(jīng)有一本關(guān)于的書了入門,覺得看看這本書就足夠了。前端的異步解決方案之和異步編程模式在前端開發(fā)過程中,顯得越來越重要。為了讓編程更美好,我們就需要引入來降低異步編程的復(fù)雜性。 JavaScript Promise 迷你書(中文版) 超詳細介紹promise的gitbook,看完再不會promise...... 本書的目的是以目前還在制定中的ECMASc...
摘要:調(diào)用棧被清空,消息隊列中并無任務(wù),線程停止,事件循環(huán)結(jié)束。不確定的時間點請求返回,將設(shè)定好的回調(diào)函數(shù)放入消息隊列。調(diào)用棧執(zhí)行完畢執(zhí)行消息隊列任務(wù)。請求并發(fā)回調(diào)函數(shù)執(zhí)行順序無法確定。 異步編程 JavaScript中異步編程問題可以說是基礎(chǔ)中的重點,也是比較難理解的地方。首先要弄懂的是什么叫異步? 我們的代碼在執(zhí)行的時候是從上到下按順序執(zhí)行,一段代碼執(zhí)行了之后才會執(zhí)行下一段代碼,這種方式...
閱讀 1280·2021-09-04 16:41
閱讀 2466·2021-09-02 10:18
閱讀 946·2019-08-29 16:40
閱讀 2640·2019-08-29 16:14
閱讀 965·2019-08-26 13:41
閱讀 1327·2019-08-26 12:24
閱讀 759·2019-08-26 10:24
閱讀 2901·2019-08-23 17:54