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