歡迎閱讀專門探索 JavaScript 及其構建組件的系列文章的第四章。 在識別和描述核心元素的過程中,我們還分享了關于構建?SessionStack 時需要遵循的一些經驗法則,一個 JavaScript 應用必須是強大且高性能的,才能保持競爭力。
你有沒有錯過前三章? 你可以在這里找到它們:
Google 的 V8 引擎里面的 5 個關于如何編寫優化代碼的技巧
這一次,我們將通過回顧如何克服在單線程環境中編程的缺點以及構建令人驚嘆的 JavaScript UI 來擴展我們的第一篇文章。按慣例,在文章的最后我們將會分享 5 個關于如何用 async / await 編寫更簡潔代碼的技巧。
為什么說單線程是一種限制?
在我們開始的第一篇文章中,我們思考了在調用堆棧(Call Stack)中進行函數調用時需要處理耗費大量時間的程序時會發生什么情況。
想象一下,例如,一個在瀏覽器中運行的復雜圖像轉換算法。
雖然調用堆棧具有執行的功能,但此時瀏覽器不能做任何事情? —— 它被停止下來。這意味著瀏覽器無法渲染,它不能運行任何代碼,它卡住了。那么問題來了 - 你的應用用戶界面不再高效和令人滿意。
你的應用程序卡住了。
在某些情況下,這可能不是很關鍵的問題。但是,這是一個更嚴重的問題。一旦你的瀏覽器開始處理調用堆棧中的太多任務,它可能會停止響應很長一段時間。在這一點上,許多瀏覽器會通過拋出錯誤來處理上述問題,顯示并詢問是否應該終止頁面:
這是很難看的,它完全毀了你的用戶體驗:
構建JavaScript程序模塊
您可能正在將您的JavaScript應用程序寫入一個單獨.js文件,但是肯定的是您的程序由幾個模塊組成,其中只有一個將會立即執行,其余的將在稍后執行。 最常見的模塊單位是函數。
大多數JavaScript新手開發者似乎都有這樣的理解,即以后不一定要求立即發生。 換句話說,根據定義,現在無法完成的任務將以異步的形式完成,這意味著當您想到使用異步來處理時,將不會遇到上述瀏覽器停止的行為。
我們來看看下面的例子:
//?ajax(..)?is?some?arbitrary?Ajax?function?given?by?a?library
var?response?=?ajax('https://example.com/api');
console.log(response);
//?`response`?won't?have?the?response
您可能知道標準的Ajax請求并不是同步完成的,這意味著在執行代碼的時候,ajax(..)函數還沒有任何返回值來分配給用于返回的變量。
一種簡單的“等待”異步函數返回結果的方式是使用callback的函數:
ajax('https://example.com/api',?function(response)?{
console.log(response);?//?`response`?is?now?available
});
需要說明一下:實際上,您可以創建同步的Ajax請求。 但永遠不要這樣做。 如果您發出同步的Ajax請求,則JavaScript應用的UI界面將被阻止渲染 - 用戶將無法點擊,輸入數據,導航或滾動。 這將阻止任何用戶與瀏覽器交互。 這是一個可怕的做法。
//?This?is?assuming?that?you're?using?jQuery
jQuery.ajax({
url:?'https://api.example.com/endpoint',
success:?function(response)?{
//?This?is?your?callback.
},
async:?false?//?And?this?is?a?terrible?idea
});
這是它的樣子,但請不要這樣做 - 不要毀掉你的網站:我們以一個Ajax請求為例。 你可以編寫任何代碼模塊并異步執行。
這可以通過使用setTimeout(回調(callback),毫秒(milliseconds))函數來完成。 setTimeout函數的作用是設置一個在稍后發生的事件(一個超時)。 讓我們來看看:
function?first()?{
console.log('first');
}
function?second()?{
console.log('second');
}
function?third()?{
console.log('third');
}
first();
setTimeout(second,?1000);?//?Invoke?`second`?after?1000ms
third();
控制臺中的輸出如下所示:
first
third
second
分析事件循環
我們從一個奇怪的說法開始——盡管允許執行異步JavaScript代碼(如我們剛才討論的setTimeout函數),但直到ES6出現,實際上JavaScript本身從來沒有任何明確的異步概念。 JavaScript引擎從來都只是執行單個程序模塊而不做更多別的事情。
有關JavaScript引擎如何工作的詳細信息(特別是Google的V8),請查看我們之前關于該主題的文章。
那么,誰來告訴JS引擎去執行你編寫的一大段程序?實際上,JS引擎并不是孤立運行,它運行在一個宿主環境中,對于大多數開發人員來說,宿主環境就是一個典型的Web瀏覽器或Node.js。實際上,如今,JavaScript被嵌入到從機器人到燈泡的各種設備中。每個設備都代表一個包含JS引擎的不同類型的宿主環境。
所有環境中的共同點是一個稱為事件循環的內置機制,它隨著時間的推移處理程序中多個模塊的執行順序,并每次調用JS引擎。
這意味著JS引擎只是任何JS代碼的一個按需執行環境。并調度事件的周圍環境(JS代碼執行)。
所以,例如,當你的JavaScript程序發出一個Ajax請求來從服務器獲取一些數據時,你在一個函數(“回調函數”)中寫好了“響應”代碼,JS引擎將會告訴宿主環境:
“嘿,我現在暫停執行,但是每當你完成這個網絡請求,并且你有一些數據,請調用這個函數并返回給我。
然后瀏覽器開始監聽來自網絡的響應,當響應返回給你的時候,宿主環境會將回調函數插入到事件循環中來安排回調函數的執行順序。
我們來看下面的圖表:
您可以在我們以前的文章中閱讀更多關于內存堆和調用棧的信息。
這些Web API是什么? 從本質上講,它們是你無法訪問的線程,你僅僅只可以調用它們。 它們是瀏覽器并行啟動的一部分。如果你是一個Node.js開發者,那么這些就相當于是C ++ API。
那么事件循環究竟是什么?
Event Loop有一個簡單的工作機制——就是去監視Call Stack和Callback Queue。 如果調用棧為空,它將從隊列中取出第一個事件,并將其推送到調用棧,從而更有效率的運行。
這種迭代在事件循環中被稱為一“刻度(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') 從調用堆棧中刪除。
5、函數 setTimeout(function cb1(){...}) 添加到調用堆棧
6、執行函數 setTimeout(function cb1(){...}) 。瀏覽器用 Web API 創建一個定時器,定時器開始倒計時。
7、函數 setTimeout(function cb1(){...}) 執行完成并從調用堆棧中刪除。
8、console.log('Bye') 添加到調用堆棧。
9、函數 console.log('Bye') 被執行。
10、console.log('Bye') 從調用堆棧中刪除。
11、在至少1500毫秒之后,定時器結束并且定時器將回調函數 cb1 放入回調函數隊列里面。
12、事件循環從回調隊列里面取出 cb1 并將其放入調用堆棧。
13、cb1 被執行并且 console.log('cb1') 被放入調用堆棧。
14、函數 console.log('cb1') 被執行。
15、console.log('cb1') 被從調用堆棧中刪除。
16、cb1 被從調用堆棧中刪除。
快速回顧:
有趣的是,ES6指定了事件循環應該如何工作,這意味著在技術上事件循環被規定在JS引擎的職責范圍之內,不再扮演一個宿主環境的角色。 這個變化的一個主要原因是在ES6中引入了Promises,因為后者需要直接、細致地控制事件循環隊列上的調度操作(我們將在后面更詳細地討論它們)。
setTimeout(...)函數如何工作
請注意,setTimeout(...)函數不會自動將您的回調函數放在事件循環隊列中。它設置了一個計時器。當定時器到期時,環境將你的回調放到事件循環中,以便將來的某時拿來執行??纯催@段代碼:
setTimeout(myCallback,?1000);
這并不意味著myCallback將在1000 ms內執行,而是在1000 ms內將myCallback添加到隊列中。但是,隊列中可能還有其他事件已經被添加了 - 您的回調事件將不得不等待執行。
市面上有很多關于開始使用JavaScript中的異步代碼的文章和教程里會建議您使用setTimeout(callback,0)。那么,現在你知道事件循環是怎么做的以及setTimeout是如何工作的:調用setTimeout設置0作為第二個參數會延遲到調用棧被清除為止才會被執行callback事件。
看看下面的代碼:
console.log('Hi');
setTimeout(function()?{
console.log('callback');
},?0);
console.log('Bye');
盡管等待時間設置為0 ms,但瀏覽器控制臺中的結果如下所示:
Hi
Bye
callback
ES6中的Jobs是什么?
在ES6中引入了一個名為“Job Queue”的新概念。它是Event Loop隊列之上的一個圖層。在處理Promises的異步行為時,你最有可能碰到它(我們也會談論它們)。
現在我們將簡單介紹一下這個概念,以便當我們和Promises討論異步行為的時候,你就會明白這些行為是如何被調度和處理的。
想象一下:Job Queue是一個連接到Event Loop隊列中每個瞬時末尾的隊列。在事件循環的瞬時期間某些異步操作不會將一個全新的事件添加到事件循環隊列,而是將一個項目(又名單元作業(Job))添加到當前瞬時單元作業(Job)隊列的末尾。
這意味著您可以添加其他功能以便稍后執行,您可以放心,它將在執行任何其他操作之前立即執行。
單元作業(Job)還可以使更多單元(Jobs)添加到同一隊列的末尾。從理論上講,一個Job“循環”(一個不斷增加的Job)可能無限地循環,從而導致需要的資源進入下一個事件循環節點。從概念上講,這和在你的代碼中只是表示長時間運行或者無限循環(比如while(true)..)類似。
Jobs有點像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);
});
我們有一個嵌套在一起的三個函數回調鏈,每個代表一個異步系列中的一個步驟。
這種代碼通常被稱為“回調地獄”。 但是“回調地獄”實際上與嵌套/縮進幾乎沒有任何關系。 這是一個更深層次的問題。
首先,我們正在等待“click”事件,然后等待定時器啟動,然后等待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的值,然后才能在表達式中使用它們。假設我們有一個函數loadX和loadY,它們分別從服務器載入x和y的值。然后,想象一下,我們有一個求和函數,一旦加載了它們,就將x和y的值相加。
它可能看起來像這樣(是不是相當丑陋):
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`
function?fetchX()?{
//?..
}
//?A?sync?or?async?function?that?retrieves?the?value?of?`y`
function?fetchY()?{
//?..
}
sum(fetchX,?fetchY,?function(result)?{
console.log(result);
});
這里有一些非常重要的東西 - 在這個代碼片段中,我們將x和y作為待定值,并且我們展示了一個求和操作sum(...)(從外部)不關心是x還是y還是待定值。
當然,這種粗糙的基于回調的方法還有很多不足之處。這只是邁向了解推導待定值的好處而邁出的第一步,而不用擔心它們何時可用的。
Promise Value
讓我們簡單地看看我們如何用Promises來表示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
return?Promise.all([xPromise,?yPromise])
//?when?that?promise?is?resolved,?let's?take?the
//?received?`X`?and?`Y`?values?and?add?them?together.
.then(function(values){
//?`values`?is?an?array?of?the?messages?from?the
//?previously?resolved?promises
return?values[0]?+?values[1];
}?);
}
//?`fetchX()`?and?`fetchY()`?return?promises?for
//?their?respective?values,?which?may?be?ready
//?*now*?or?*later*.
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.
.then(function(sum){
console.log(sum);
});
在代碼中Promises有兩層。
fetchX()和fetchY()被直接調用,返回值(promises!)傳遞給了sum(...).promises潛在的值可能現在準備好了或者延時,但是每一個promise的行為都是嚴格相同的。我們以獨立于時間的方式分析x和y值。它們是future values、時期。
promise的第二層是sum(...)創造(通過Promise.all([...]))和返回的,然后等待通過調用then(...).當sum(...)的操作完成,我們總的future value準備好了并且能夠打印輸出。我們在sum(...)中隱藏了x和y的future value等待邏輯。
注意:在sum(...)里,Promise.all([...])創建了一個promise(它等待promiseX和promiseY解決)。鏈式調用.then(...)創建另一個promise,立即返回value[0]+value[1]的結果(加法結果)。因此,then(...)在最后調用了sum(...)——在代碼最后——實際上執行的是第二個promise的返回值,而不是被Promise.all([...])創建的第一個。另外,雖然我們有將第二個then(...)結束,他也創建了另外一個promise,取決于我們是觀察/使用它。這Promise鏈的內容將在本章后面部分進行更詳細地解釋。
隨著Promises, then(...)實際調用了兩個方法,第一個用于實現(如前面所示),第二個為拒絕:
sum(fetchX(),?fetchY())
.then(
//?fullfillment?handler
function(sum)?{
console.log(?sum?);
},
//?rejection?handler
function(err)?{
console.error(?err?);?//?bummer!
}
);
如果當我們在獲取x或y的時候出錯,或者在相加過程中不知何故失敗了,promise的sum(...)返回將會被拒絕,并且第二個回調異常處理器將通過then(...)接收promise的拒絕值。
因為Promises封裝時態——等待實現或拒絕的潛在的價值——從外界,Promise自身是時間獨立的,因次Promises可以以可預測的方式組成(組合),而不考慮時間和結果。
而且,一旦Promise得到解決,它就會永遠保持下去。它將在某一時刻變成一個immutable value——然后可以根據需要觀察多次。
鏈式 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) 會創建一個 Promise ,這個請求會在 2000ms 內實現。之后我們會返回第一個 then(...) 的實現的調用,這又會引起第二個 then(...)? 的 Promise ,這個請求同樣延遲 2000ms 。
注意:因為 Promise 一旦執行完成就是在外部不可變的。知道它不能被意外或惡意修改之后,我們現在就可以放心地把這個值傳遞給任何地方。 關于多方觀察 “Promise” 的解決方案,尤其如此。 一方不可能影響另一方遵守 Promise 解決方案的能力。 不變性可能聽起來像是一個學術話題,但它實際上是 Promise 設計的最基本和最重要的方面之一,這不應該被忽略。
用還是不用 Promise ?
Promise 的一個重要的特性是可以確定是某個變量是否是一個 Promise ,換句話說就是那個變量的行為是否類似 Promise ?
我們知道 Promises 通過語法 new Promise(...) 構造,而且你可能覺得 p instanceof Promise 是一個有效的檢測方法,但實際上這種方式并不是非常有效。
主要原因是你可能接收到來自其他瀏覽器頁面(例如 iframe )的 Promise 變量,而且這個變量可能有自己的 Promise 類型,當和當前窗口或者 frame 的 Promise 類型不一樣的時候,上邊的檢測方法就可能無法檢測出該變量是一個 Promise 實例。
此外,一個庫或者框架可能會實現自己的 Promise 并且不使用 ES6 原生的 Promise 。事實上,你也可能在早期不支持 Promise 的瀏覽器中通過庫來使用 Promise 。
吞吐異常
如果在構造一個 Promise 對象,或者監控系數的任一情況下,拋出了一個 JavaScript 異常錯誤,例如拋出一個 TypeError 或者 ReferenceError ,那么異常即會被捕獲,并且它將迫使問題中的 Promise 對象拒絕訪問。
舉個例子:
var?p?=?new?Promise(function(resolve,?reject){
foo.bar(); ??//?`foo`?is?not?defined,?so?error!
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.
}
);
如果 Promise 對象已經執行了 fulfilled() 方法( fulfilled 與方法 fulfilled() 同名),那么在監控過程中(在 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 異常而拒絕訪問。
處理未拋出的異常
有很多人們覺得更好的其他方法。
通常的建議是?Promises 應該有一個 done(...) 方法,這本質上標記了 Promise 鏈上的“已做”,done() 沒有創建和返回 Promise,因此,傳遞到 done(..) 的回調顯然不會被鏈接,并將問題提交給一個不存在的鏈式? 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
});
view?raw
在ES8中發生著什么?異步/等待
JavaScript ES8提出了異步/等待,使得和Promises一起完成的任務更加容易了。我們將簡短地整理下異步/等待所提供的可能性以及如何利用它們去寫異步代碼。
接下來,我們一起來看看異步/等待是如何工作的。
使用異步函數聲明定義了一個異步函數。那么該函數返回一個AsyncFunction對象。這個AsyncFunction對象代表了執行包含在函數內部代碼的異步函數。當一個函數被調用時,它返回一個Promise。當異步函數返回一個值時,它不是一個Promise,Promise是會自動被創建,并和函數返回值一起被解決。當異步函數出現異常,Promise將會和生成的異常值一起被拒收。
異步函數可以包含一個等待表達式,它可以暫停函數的執行并等待上一個Promise的解決,然后恢復異步函數的執行并返回被解決的值。
你可以把JavaScript中的Promise看作成java中的Future或C #中的Task。
異步/等待的作用就是簡化使用Promises的運轉狀態。
下面來看一個實例:
//?Just?a?standard?JavaScript?function
function?getNumber1()?{
return?Promise.resolve('374');
}
//?This?function?does?the?same?as?getNumber1
async?function?getNumber2()?{
return?374;
}
同樣地,拋出異常的函數相當于返回被拒絕的Promises的函數:
function?f1()?{
return?Promise.reject('Some?error');
}
async?function?f2()?{
throw?'Some?error';
}
等待關鍵字只能用于異步函數并且允許同時等待Promise。如果我們在一個異步函數以外使用Promises,我們還必須使用回調:
async?function?loadData()?{
//?`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;
}
//?Since,?we're?not?in?an?`async?function`?anymore
//?we?have?to?use?`then`.
loadData().then(()?=>?console.log('Done'));
你也可以通過一個“異步函數表達式”來定義異步功能。異步函數表達式和異步函數聲明非常相似,兩者有著幾乎相同的語法。異步函數表達式和異步函數聲明之間的主要區別在于函數名,在異步函數表達式中創建匿名函數時函數名是可以省略的。異步函數表達式可以被當做一個 IIFE(立即調用函數表達式)來使用,即一被定義就可運行。
就像這個例子一樣:
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;
}
更重要的是,異步/等待被所有主流瀏覽器支持:
最后,其實重要的事情不是盲目去選擇“最新”的方式來編寫異步代碼。理解異步 JavaScript 的內部本質,了解它為什么這么重要以及深度理解你所選擇的方法的內涵是極為必要的。就像編程中的其他方面一樣,每種方法都有它各自的優點和缺點。
5個小技巧編寫高度可維護,健壯的異步代碼
1.清理代碼:使用async/await可以讓你編寫更少的代碼。每次使用async/await讓你跳過一些不必要的步驟:編寫。然后,創建一個匿名函數來處理響應,命名該回調的響應。
例如:
//?`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/await可以使用相同的代碼結構——眾所周知的try/catch語句處理同步和異步錯誤。讓我們看看Promises的樣子:
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);
}
}
與:
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
}
})
}
view?raw
與:
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.堆??蚣埽?/b>與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”之類的調試快捷方式,則調試器將不會移動到以下位置,因為它只通過同步代碼“steps”。
通過async/await,您可以完全按照正常的同步函數一步步地等待調用。
編寫異步JavaScript代碼不僅對于應用程序本身而且對于編寫js庫也很重要。
例如,SessionStack庫會記錄您的Web應用程序/網站中的所有內容:所有DOM更改,用戶交互,JavaScript異常,堆棧跟蹤,網絡請求失敗以及調試消息。
而這一切都必須在您的生產環境中發生,而不會影響任何用戶體驗。我們需要大量優化我們的代碼,并盡可能使其異步,以便我們可以增加事件循環中可以處理的事件的數量。
而不只是js庫!在SessionStack中重現用戶會話時,我們必須在出現問題時渲染用戶瀏覽器中發生的所有事情,并且必須重構整個狀態,以便在會話時間線中來回跳轉。為了使這成為可能,我們正在大量使用JavaScript提供的異步機制來實現。
這里有一個免費的計劃,你可以從這里開始。
資源:
https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch2.md
https://github.com/getify/You-Dont-Know-JS/blob/master/async%20%26%20performance/ch3.md