歡迎閱讀專門探索 JavaScript 及其構(gòu)建組件的系列文章的第四章漩勤。 在識(shí)別和描述核心元素的過程中掷漱,我們還分享了關(guān)于構(gòu)建?SessionStack 時(shí)需要遵循的一些經(jīng)驗(yàn)法則,一個(gè) JavaScript 應(yīng)用必須是強(qiáng)大且高性能的,才能保持競(jìng)爭(zhēng)力测暗。
你有沒有錯(cuò)過前三章椒舵? 你可以在這里找到它們:
引擎蚂踊,運(yùn)行時(shí)和調(diào)用堆棧的概述
Google 的 V8 引擎里面的 5 個(gè)關(guān)于如何編寫優(yōu)化代碼的技巧
內(nèi)存管理和如何處理 4 個(gè)常見的內(nèi)存泄漏
這一次,我們將通過回顧如何克服在單線程環(huán)境中編程的缺點(diǎn)以及構(gòu)建令人驚嘆的 JavaScript UI 來擴(kuò)展我們的第一篇文章笔宿。按慣例犁钟,在文章的最后我們將會(huì)分享 5 個(gè)關(guān)于如何用 async / await 編寫更簡(jiǎn)潔代碼的技巧棱诱。
為什么說單線程是一種限制?
在我們開始的第一篇文章中涝动,我們思考了在調(diào)用堆棧(Call Stack)中進(jìn)行函數(shù)調(diào)用時(shí)需要處理耗費(fèi)大量時(shí)間的程序時(shí)會(huì)發(fā)生什么情況迈勋。
想象一下,例如醋粟,一個(gè)在瀏覽器中運(yùn)行的復(fù)雜圖像轉(zhuǎn)換算法靡菇。
雖然調(diào)用堆棧具有執(zhí)行的功能,但此時(shí)瀏覽器不能做任何事情? —— 它被停止下來米愿。這意味著瀏覽器無法渲染厦凤,它不能運(yùn)行任何代碼,它卡住了育苟。那么問題來了 - 你的應(yīng)用用戶界面不再高效和令人滿意较鼓。
你的應(yīng)用程序卡住了。
在某些情況下违柏,這可能不是很關(guān)鍵的問題博烂。但是,這是一個(gè)更嚴(yán)重的問題漱竖。一旦你的瀏覽器開始處理調(diào)用堆棧中的太多任務(wù)脖母,它可能會(huì)停止響應(yīng)很長(zhǎng)一段時(shí)間。在這一點(diǎn)上闲孤,許多瀏覽器會(huì)通過拋出錯(cuò)誤來處理上述問題谆级,顯示并詢問是否應(yīng)該終止頁面:
這是很難看的,它完全毀了你的用戶體驗(yàn):
構(gòu)建JavaScript程序模塊
您可能正在將您的JavaScript應(yīng)用程序?qū)懭胍粋€(gè)單獨(dú).js文件讼积,但是肯定的是您的程序由幾個(gè)模塊組成肥照,其中只有一個(gè)將會(huì)立即執(zhí)行,其余的將在稍后執(zhí)行勤众。 最常見的模塊單位是函數(shù)舆绎。
大多數(shù)JavaScript新手開發(fā)者似乎都有這樣的理解,即以后不一定要求立即發(fā)生们颜。 換句話說吕朵,根據(jù)定義,現(xiàn)在無法完成的任務(wù)將以異步的形式完成窥突,這意味著當(dāng)您想到使用異步來處理時(shí)努溃,將不會(huì)遇到上述瀏覽器停止的行為。
我們來看看下面的例子:
//?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
您可能知道標(biāo)準(zhǔn)的Ajax請(qǐng)求并不是同步完成的阻问,這意味著在執(zhí)行代碼的時(shí)候梧税,ajax(..)函數(shù)還沒有任何返回值來分配給用于返回的變量。
一種簡(jiǎn)單的“等待”異步函數(shù)返回結(jié)果的方式是使用callback的函數(shù):
ajax('https://example.com/api',?function(response)?{
console.log(response);?//?`response`?is?now?available
});
需要說明一下:實(shí)際上,您可以創(chuàng)建同步的Ajax請(qǐng)求第队。 但永遠(yuǎn)不要這樣做。 如果您發(fā)出同步的Ajax請(qǐng)求凳谦,則JavaScript應(yīng)用的UI界面將被阻止渲染 - 用戶將無法點(diǎn)擊忆畅,輸入數(shù)據(jù),導(dǎo)航或滾動(dòng)尸执。 這將阻止任何用戶與瀏覽器交互邻眷。 這是一個(gè)可怕的做法。
//?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
});
這是它的樣子剔交,但請(qǐng)不要這樣做 - 不要?dú)У裟愕木W(wǎng)站:我們以一個(gè)Ajax請(qǐng)求為例肆饶。 你可以編寫任何代碼模塊并異步執(zhí)行。
這可以通過使用setTimeout(回調(diào)(callback)岖常,毫秒(milliseconds))函數(shù)來完成驯镊。 setTimeout函數(shù)的作用是設(shè)置一個(gè)在稍后發(fā)生的事件(一個(gè)超時(shí))。 讓我們來看看:
function?first()?{
console.log('first');
}
function?second()?{
console.log('second');
}
function?third()?{
console.log('third');
}
first();
setTimeout(second,?1000);?//?Invoke?`second`?after?1000ms
third();
控制臺(tái)中的輸出如下所示:
first
third
second
分析事件循環(huán)
我們從一個(gè)奇怪的說法開始——盡管允許執(zhí)行異步JavaScript代碼(如我們剛才討論的setTimeout函數(shù))竭鞍,但直到ES6出現(xiàn)板惑,實(shí)際上JavaScript本身從來沒有任何明確的異步概念。 JavaScript引擎從來都只是執(zhí)行單個(gè)程序模塊而不做更多別的事情偎快。
有關(guān)JavaScript引擎如何工作的詳細(xì)信息(特別是Google的V8)冯乘,請(qǐng)查看我們之前關(guān)于該主題的文章。
那么晒夹,誰來告訴JS引擎去執(zhí)行你編寫的一大段程序裆馒?實(shí)際上,JS引擎并不是孤立運(yùn)行丐怯,它運(yùn)行在一個(gè)宿主環(huán)境中喷好,對(duì)于大多數(shù)開發(fā)人員來說,宿主環(huán)境就是一個(gè)典型的Web瀏覽器或Node.js读跷。實(shí)際上梗搅,如今,JavaScript被嵌入到從機(jī)器人到燈泡的各種設(shè)備中效览。每個(gè)設(shè)備都代表一個(gè)包含JS引擎的不同類型的宿主環(huán)境无切。
所有環(huán)境中的共同點(diǎn)是一個(gè)稱為事件循環(huán)的內(nèi)置機(jī)制,它隨著時(shí)間的推移處理程序中多個(gè)模塊的執(zhí)行順序丐枉,并每次調(diào)用JS引擎哆键。
這意味著JS引擎只是任何JS代碼的一個(gè)按需執(zhí)行環(huán)境。并調(diào)度事件的周圍環(huán)境(JS代碼執(zhí)行)矛洞。
所以洼哎,例如,當(dāng)你的JavaScript程序發(fā)出一個(gè)Ajax請(qǐng)求來從服務(wù)器獲取一些數(shù)據(jù)時(shí)沼本,你在一個(gè)函數(shù)(“回調(diào)函數(shù)”)中寫好了“響應(yīng)”代碼噩峦,JS引擎將會(huì)告訴宿主環(huán)境:
“嘿,我現(xiàn)在暫停執(zhí)行抽兆,但是每當(dāng)你完成這個(gè)網(wǎng)絡(luò)請(qǐng)求识补,并且你有一些數(shù)據(jù),請(qǐng)調(diào)用這個(gè)函數(shù)并返回給我辫红。
然后瀏覽器開始監(jiān)聽來自網(wǎng)絡(luò)的響應(yīng)凭涂,當(dāng)響應(yīng)返回給你的時(shí)候,宿主環(huán)境會(huì)將回調(diào)函數(shù)插入到事件循環(huán)中來安排回調(diào)函數(shù)的執(zhí)行順序贴妻。
我們來看下面的圖表:
您可以在我們以前的文章中閱讀更多關(guān)于內(nèi)存堆和調(diào)用棧的信息切油。
這些Web API是什么? 從本質(zhì)上講名惩,它們是你無法訪問的線程澎胡,你僅僅只可以調(diào)用它們。 它們是瀏覽器并行啟動(dòng)的一部分娩鹉。如果你是一個(gè)Node.js開發(fā)者攻谁,那么這些就相當(dāng)于是C ++ API。
那么事件循環(huán)究竟是什么弯予?
Event Loop有一個(gè)簡(jiǎn)單的工作機(jī)制——就是去監(jiān)視Call Stack和Callback Queue戚宦。 如果調(diào)用棧為空,它將從隊(duì)列中取出第一個(gè)事件锈嫩,并將其推送到調(diào)用棧受楼,從而更有效率的運(yùn)行。
這種迭代在事件循環(huán)中被稱為一“刻度(tick)”呼寸。 每個(gè)事件只是一個(gè)函數(shù)回調(diào)那槽。
console.log('Hi');
setTimeout(function?cb1()?{
console.log('cb1');
},?5000);
console.log('Bye');
現(xiàn)在執(zhí)行一下這段代碼,看發(fā)生了什么:
1等舔、狀態(tài)是清晰的骚灸。瀏覽器控制臺(tái)沒有輸出,調(diào)用堆棧是空的慌植。
2甚牲、console.log('Hi') 被添加到調(diào)用堆棧。
3蝶柿、執(zhí)行 console.log('Hi').
4丈钙、console.log('Hi') 從調(diào)用堆棧中刪除。
5交汤、函數(shù) setTimeout(function cb1(){...}) 添加到調(diào)用堆棧
6雏赦、執(zhí)行函數(shù) setTimeout(function cb1(){...}) 劫笙。瀏覽器用 Web API 創(chuàng)建一個(gè)定時(shí)器,定時(shí)器開始倒計(jì)時(shí)星岗。
7填大、函數(shù) setTimeout(function cb1(){...}) 執(zhí)行完成并從調(diào)用堆棧中刪除。
8俏橘、console.log('Bye') 添加到調(diào)用堆棧允华。
9、函數(shù) console.log('Bye') 被執(zhí)行寥掐。
10靴寂、console.log('Bye') 從調(diào)用堆棧中刪除。
11召耘、在至少1500毫秒之后百炬,定時(shí)器結(jié)束并且定時(shí)器將回調(diào)函數(shù) cb1 放入回調(diào)函數(shù)隊(duì)列里面。
12污它、事件循環(huán)從回調(diào)隊(duì)列里面取出 cb1 并將其放入調(diào)用堆棧符喝。
13粪滤、cb1 被執(zhí)行并且 console.log('cb1') 被放入調(diào)用堆棧。
14、函數(shù) console.log('cb1') 被執(zhí)行魔市。
15房资、console.log('cb1') 被從調(diào)用堆棧中刪除淘这。
16穴张、cb1 被從調(diào)用堆棧中刪除。
快速回顧:
有趣的是缝呕,ES6指定了事件循環(huán)應(yīng)該如何工作澳窑,這意味著在技術(shù)上事件循環(huán)被規(guī)定在JS引擎的職責(zé)范圍之內(nèi),不再扮演一個(gè)宿主環(huán)境的角色供常。 這個(gè)變化的一個(gè)主要原因是在ES6中引入了Promises煎源,因?yàn)楹笳咝枰苯诱┖贰⒓?xì)致地控制事件循環(huán)隊(duì)列上的調(diào)度操作(我們將在后面更詳細(xì)地討論它們)慕趴。
setTimeout(...)函數(shù)如何工作
請(qǐng)注意给僵,setTimeout(...)函數(shù)不會(huì)自動(dòng)將您的回調(diào)函數(shù)放在事件循環(huán)隊(duì)列中塔次。它設(shè)置了一個(gè)計(jì)時(shí)器恭取。當(dāng)定時(shí)器到期時(shí),環(huán)境將你的回調(diào)放到事件循環(huán)中熄守,以便將來的某時(shí)拿來執(zhí)行蜈垮『孽耍看看這段代碼:
setTimeout(myCallback,?1000);
這并不意味著myCallback將在1000 ms內(nèi)執(zhí)行,而是在1000 ms內(nèi)將myCallback添加到隊(duì)列中攒发。但是调塌,隊(duì)列中可能還有其他事件已經(jīng)被添加了 - 您的回調(diào)事件將不得不等待執(zhí)行。
市面上有很多關(guān)于開始使用JavaScript中的異步代碼的文章和教程里會(huì)建議您使用setTimeout(callback惠猿,0)羔砾。那么,現(xiàn)在你知道事件循環(huán)是怎么做的以及setTimeout是如何工作的:調(diào)用setTimeout設(shè)置0作為第二個(gè)參數(shù)會(huì)延遲到調(diào)用棧被清除為止才會(huì)被執(zhí)行callback事件偶妖。
看看下面的代碼:
console.log('Hi');
setTimeout(function()?{
console.log('callback');
},?0);
console.log('Bye');
盡管等待時(shí)間設(shè)置為0 ms姜凄,但瀏覽器控制臺(tái)中的結(jié)果如下所示:
Hi
Bye
callback
ES6中的Jobs是什么?
在ES6中引入了一個(gè)名為“Job Queue”的新概念趾访。它是Event Loop隊(duì)列之上的一個(gè)圖層态秧。在處理Promises的異步行為時(shí),你最有可能碰到它(我們也會(huì)談?wù)撍鼈儯?/p>
現(xiàn)在我們將簡(jiǎn)單介紹一下這個(gè)概念扼鞋,以便當(dāng)我們和Promises討論異步行為的時(shí)候申鱼,你就會(huì)明白這些行為是如何被調(diào)度和處理的。
想象一下:Job Queue是一個(gè)連接到Event Loop隊(duì)列中每個(gè)瞬時(shí)末尾的隊(duì)列云头。在事件循環(huán)的瞬時(shí)期間某些異步操作不會(huì)將一個(gè)全新的事件添加到事件循環(huán)隊(duì)列捐友,而是將一個(gè)項(xiàng)目(又名單元作業(yè)(Job))添加到當(dāng)前瞬時(shí)單元作業(yè)(Job)隊(duì)列的末尾。
這意味著您可以添加其他功能以便稍后執(zhí)行溃槐,您可以放心匣砖,它將在執(zhí)行任何其他操作之前立即執(zhí)行。
單元作業(yè)(Job)還可以使更多單元(Jobs)添加到同一隊(duì)列的末尾竿痰。從理論上講脆粥,一個(gè)Job“循環(huán)”(一個(gè)不斷增加的Job)可能無限地循環(huán),從而導(dǎo)致需要的資源進(jìn)入下一個(gè)事件循環(huán)節(jié)點(diǎn)影涉。從概念上講变隔,這和在你的代碼中只是表示長(zhǎng)時(shí)間運(yùn)行或者無限循環(huán)(比如while(true)..)類似。
Jobs有點(diǎn)像setTimeout(callback蟹倾,0)“hack”匣缘,但實(shí)現(xiàn)的方式是它們引入了一個(gè)更加明確和有保證的排序:稍后就會(huì)介紹。
回調(diào)
如您所知鲜棠,回調(diào)是迄今為止在JavaScript程序中展現(xiàn)異步和管理異步的最常見方式肌厨。 事實(shí)上,回調(diào)是JavaScript語言中最基本的異步模式豁陆。 無數(shù)的JS程序柑爸,甚至是非常精密和復(fù)雜的程序,都基本被寫在了回調(diào)之上盒音,而不是在其他異步實(shí)現(xiàn)上表鳍。
除了回調(diào)沒有缺點(diǎn)馅而。 許多開發(fā)人員正在試圖找到更好的異步模式。 但是譬圣,如果你不了解底層實(shí)際情況瓮恭,就不可能有效地使用抽象方法。
在下面的章節(jié)中厘熟,我們將深入探討這些抽象概念屯蹦,以說明為什么更精妙的異步模式(將在后續(xù)的帖子中討論)是必要的甚至是推薦的。
嵌套回調(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è)嵌套在一起的三個(gè)函數(shù)回調(diào)鏈绳姨,每個(gè)代表一個(gè)異步系列中的一個(gè)步驟登澜。
這種代碼通常被稱為“回調(diào)地獄”。 但是“回調(diào)地獄”實(shí)際上與嵌套/縮進(jìn)幾乎沒有任何關(guān)系就缆。 這是一個(gè)更深層次的問題帖渠。
首先谒亦,我們正在等待“click”事件竭宰,然后等待定時(shí)器啟動(dòng),然后等待Ajax響應(yīng)返回份招,此時(shí)可能會(huì)再次重復(fù)切揭。
乍一看,這個(gè)代碼可能似乎將其異步映射到如下的連續(xù)步驟:
listen('click',?function?(e)?{
//?..
});
那么:
setTimeout(function(){
//?..
},?500);
然后:
ajax('https://api.example.com/endpoint',?function?(text){
//?..
});
最后:
if?(text?==?"hello")?{
doSomething();
}
else?if?(text?==?"world")?{
doSomethingElse();
}
那么锁摔,這種表達(dá)異步代碼順序的方式似乎更加自然廓旬,不是嗎? 一定有這樣的方式吧谐腰?
Promises
看看下面的代碼:
var?x?=?1;
var?y?=?2;
console.log(x?+?y);
這非常明顯:它將x和y的值相加并打印到控制臺(tái)孕豹。但是,如果x或y的值缺失而且還有待確定十气,該怎么辦励背?比方說,我們需要從服務(wù)器中檢索x和y的值砸西,然后才能在表達(dá)式中使用它們叶眉。假設(shè)我們有一個(gè)函數(shù)loadX和loadY,它們分別從服務(wù)器載入x和y的值芹枷。然后衅疙,想象一下,我們有一個(gè)求和函數(shù)鸳慈,一旦加載了它們饱溢,就將x和y的值相加。
它可能看起來像這樣(是不是相當(dāng)丑陋):
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);
});
這里有一些非常重要的東西 - 在這個(gè)代碼片段中走芋,我們將x和y作為待定值绩郎,并且我們展示了一個(gè)求和操作sum(...)(從外部)不關(guān)心是x還是y還是待定值絮识。
當(dāng)然,這種粗糙的基于回調(diào)的方法還有很多不足之處嗽上。這只是邁向了解推導(dǎo)待定值的好處而邁出的第一步次舌,而不用擔(dān)心它們何時(shí)可用的。
Promise Value
讓我們簡(jiǎn)單地看看我們?nèi)绾斡肞romises來表示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()被直接調(diào)用彼念,返回值(promises!)傳遞給了sum(...).promises潛在的值可能現(xiàn)在準(zhǔn)備好了或者延時(shí),但是每一個(gè)promise的行為都是嚴(yán)格相同的浅萧。我們以獨(dú)立于時(shí)間的方式分析x和y值逐沙。它們是future values、時(shí)期洼畅。
promise的第二層是sum(...)創(chuàng)造(通過Promise.all([...]))和返回的,然后等待通過調(diào)用then(...).當(dāng)sum(...)的操作完成吩案,我們總的future value準(zhǔn)備好了并且能夠打印輸出。我們?cè)趕um(...)中隱藏了x和y的future value等待邏輯帝簇。
注意:在sum(...)里徘郭,Promise.all([...])創(chuàng)建了一個(gè)promise(它等待promiseX和promiseY解決)。鏈?zhǔn)秸{(diào)用.then(...)創(chuàng)建另一個(gè)promise,立即返回value[0]+value[1]的結(jié)果(加法結(jié)果)丧肴。因此残揉,then(...)在最后調(diào)用了sum(...)——在代碼最后——實(shí)際上執(zhí)行的是第二個(gè)promise的返回值,而不是被Promise.all([...])創(chuàng)建的第一個(gè)芋浮。另外抱环,雖然我們有將第二個(gè)then(...)結(jié)束,他也創(chuàng)建了另外一個(gè)promise纸巷,取決于我們是觀察/使用它镇草。這Promise鏈的內(nèi)容將在本章后面部分進(jìn)行更詳細(xì)地解釋。
隨著Promises, then(...)實(shí)際調(diào)用了兩個(gè)方法瘤旨,第一個(gè)用于實(shí)現(xiàn)(如前面所示)梯啤,第二個(gè)為拒絕:
sum(fetchX(),?fetchY())
.then(
//?fullfillment?handler
function(sum)?{
console.log(?sum?);
},
//?rejection?handler
function(err)?{
console.error(?err?);?//?bummer!
}
);
如果當(dāng)我們?cè)讷@取x或y的時(shí)候出錯(cuò),或者在相加過程中不知何故失敗了裆站,promise的sum(...)返回將會(huì)被拒絕条辟,并且第二個(gè)回調(diào)異常處理器將通過then(...)接收promise的拒絕值。
因?yàn)镻romises封裝時(shí)態(tài)——等待實(shí)現(xiàn)或拒絕的潛在的價(jià)值——從外界宏胯,Promise自身是時(shí)間獨(dú)立的羽嫡,因次Promises可以以可預(yù)測(cè)的方式組成(組合),而不考慮時(shí)間和結(jié)果肩袍。
而且杭棵,一旦Promise得到解決,它就會(huì)永遠(yuǎn)保持下去。它將在某一時(shí)刻變成一個(gè)immutable value——然后可以根據(jù)需要觀察多次魂爪。
鏈?zhǔn)?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) 會(huì)創(chuàng)建一個(gè) Promise 先舷,這個(gè)請(qǐng)求會(huì)在 2000ms 內(nèi)實(shí)現(xiàn)。之后我們會(huì)返回第一個(gè) then(...) 的實(shí)現(xiàn)的調(diào)用滓侍,這又會(huì)引起第二個(gè) then(...)? 的 Promise 蒋川,這個(gè)請(qǐng)求同樣延遲 2000ms 。
注意:因?yàn)?Promise 一旦執(zhí)行完成就是在外部不可變的撩笆。知道它不能被意外或惡意修改之后捺球,我們現(xiàn)在就可以放心地把這個(gè)值傳遞給任何地方。 關(guān)于多方觀察 “Promise” 的解決方案夕冲,尤其如此氮兵。 一方不可能影響另一方遵守 Promise 解決方案的能力。 不變性可能聽起來像是一個(gè)學(xué)術(shù)話題歹鱼,但它實(shí)際上是 Promise 設(shè)計(jì)的最基本和最重要的方面之一泣栈,這不應(yīng)該被忽略。
用還是不用 Promise 弥姻?
Promise 的一個(gè)重要的特性是可以確定是某個(gè)變量是否是一個(gè) Promise 南片,換句話說就是那個(gè)變量的行為是否類似 Promise ?
我們知道 Promises 通過語法 new Promise(...) 構(gòu)造蚁阳,而且你可能覺得 p instanceof Promise 是一個(gè)有效的檢測(cè)方法铃绒,但實(shí)際上這種方式并不是非常有效鸽照。
主要原因是你可能接收到來自其他瀏覽器頁面(例如 iframe )的 Promise 變量螺捐,而且這個(gè)變量可能有自己的 Promise 類型,當(dāng)和當(dāng)前窗口或者 frame 的 Promise 類型不一樣的時(shí)候矮燎,上邊的檢測(cè)方法就可能無法檢測(cè)出該變量是一個(gè) Promise 實(shí)例定血。
此外,一個(gè)庫或者框架可能會(huì)實(shí)現(xiàn)自己的 Promise 并且不使用 ES6 原生的 Promise 诞外。事實(shí)上澜沟,你也可能在早期不支持 Promise 的瀏覽器中通過庫來使用 Promise 。
吞吐異常
如果在構(gòu)造一個(gè) Promise 對(duì)象峡谊,或者監(jiān)控系數(shù)的任一情況下茫虽,拋出了一個(gè) JavaScript 異常錯(cuò)誤,例如拋出一個(gè) TypeError 或者 ReferenceError 既们,那么異常即會(huì)被捕獲濒析,并且它將迫使問題中的 Promise 對(duì)象拒絕訪問。
舉個(gè)例子:
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 對(duì)象已經(jīng)執(zhí)行了 fulfilled() 方法( fulfilled 與方法 fulfilled() 同名)啥纸,那么在監(jiān)控過程中(在 then() 方法注冊(cè)回調(diào)內(nèi))拋出了一個(gè) JS 異常時(shí)又會(huì)發(fā)生什么号杏?即使它不會(huì)丟失,但是你可能會(huì)發(fā)現(xiàn)它們的處理方式有點(diǎn)令人吃驚斯棒。除非你挖的更深一點(diǎn):
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() 的異常確實(shí)被吞噬了盾致。但是主经,其實(shí)并沒有。相反庭惜,更深層次的罩驻、監(jiān)聽不到的東西出錯(cuò)了。p.then() 方法調(diào)用自身來返回了另一個(gè) promise 對(duì)象护赊,并且這個(gè) promise 對(duì)象因拋出 TypeError 異常而拒絕訪問鉴腻。
處理未拋出的異常
有很多人們覺得更好的其他方法。
通常的建議是?Promises 應(yīng)該有一個(gè) done(...) 方法百揭,這本質(zhì)上標(biāo)記了 Promise 鏈上的“已做”爽哎,done() 沒有創(chuàng)建和返回 Promise,因此器一,傳遞到 done(..) 的回調(diào)顯然不會(huì)被鏈接课锌,并將問題提交給一個(gè)不存在的鏈?zhǔn)? Promise 。
在未捕獲的錯(cuò)誤條件下祈秕,它會(huì)被處理:done() 內(nèi)部的任何異常渺贤,都會(huì)將拒絕處理作為全局未捕獲的錯(cuò)誤拋出(在開發(fā)人員的控制臺(tái)上,基本上是這樣的):
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中發(fā)生著什么请毛?異步/等待
JavaScript ES8提出了異步/等待志鞍,使得和Promises一起完成的任務(wù)更加容易了。我們將簡(jiǎn)短地整理下異步/等待所提供的可能性以及如何利用它們?nèi)懏惒酱a方仿。
接下來固棚,我們一起來看看異步/等待是如何工作的。
使用異步函數(shù)聲明定義了一個(gè)異步函數(shù)仙蚜。那么該函數(shù)返回一個(gè)AsyncFunction對(duì)象此洲。這個(gè)AsyncFunction對(duì)象代表了執(zhí)行包含在函數(shù)內(nèi)部代碼的異步函數(shù)。當(dāng)一個(gè)函數(shù)被調(diào)用時(shí)委粉,它返回一個(gè)Promise呜师。當(dāng)異步函數(shù)返回一個(gè)值時(shí),它不是一個(gè)Promise贾节,Promise是會(huì)自動(dòng)被創(chuàng)建汁汗,并和函數(shù)返回值一起被解決。當(dāng)異步函數(shù)出現(xiàn)異常栗涂,Promise將會(huì)和生成的異常值一起被拒收知牌。
異步函數(shù)可以包含一個(gè)等待表達(dá)式,它可以暫停函數(shù)的執(zhí)行并等待上一個(gè)Promise的解決戴差,然后恢復(fù)異步函數(shù)的執(zhí)行并返回被解決的值送爸。
你可以把JavaScript中的Promise看作成java中的Future或C #中的Task。
異步/等待的作用就是簡(jiǎn)化使用Promises的運(yùn)轉(zhuǎn)狀態(tài)。
下面來看一個(gè)實(shí)例:
//?Just?a?standard?JavaScript?function
function?getNumber1()?{
return?Promise.resolve('374');
}
//?This?function?does?the?same?as?getNumber1
async?function?getNumber2()?{
return?374;
}
同樣地袭厂,拋出異常的函數(shù)相當(dāng)于返回被拒絕的Promises的函數(shù):
function?f1()?{
return?Promise.reject('Some?error');
}
async?function?f2()?{
throw?'Some?error';
}
等待關(guān)鍵字只能用于異步函數(shù)并且允許同時(shí)等待Promise墨吓。如果我們?cè)谝粋€(gè)異步函數(shù)以外使用Promises,我們還必須使用回調(diào):
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'));
你也可以通過一個(gè)“異步函數(shù)表達(dá)式”來定義異步功能纹磺。異步函數(shù)表達(dá)式和異步函數(shù)聲明非常相似帖烘,兩者有著幾乎相同的語法。異步函數(shù)表達(dá)式和異步函數(shù)聲明之間的主要區(qū)別在于函數(shù)名橄杨,在異步函數(shù)表達(dá)式中創(chuàng)建匿名函數(shù)時(shí)函數(shù)名是可以省略的秘症。異步函數(shù)表達(dá)式可以被當(dāng)做一個(gè) IIFE(立即調(diào)用函數(shù)表達(dá)式)來使用,即一被定義就可運(yùn)行式矫。
就像這個(gè)例子一樣:
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;
}
更重要的是乡摹,異步/等待被所有主流瀏覽器支持:
最后,其實(shí)重要的事情不是盲目去選擇“最新”的方式來編寫異步代碼采转。理解異步 JavaScript 的內(nèi)部本質(zhì)聪廉,了解它為什么這么重要以及深度理解你所選擇的方法的內(nèi)涵是極為必要的。就像編程中的其他方面一樣故慈,每種方法都有它各自的優(yōu)點(diǎn)和缺點(diǎn)板熊。
5個(gè)小技巧編寫高度可維護(hù),健壯的異步代碼
1.清理代碼:使用async/await可以讓你編寫更少的代碼察绷。每次使用async/await讓你跳過一些不必要的步驟:編寫干签。然后,創(chuàng)建一個(gè)匿名函數(shù)來處理響應(yīng)拆撼,命名該回調(diào)的響應(yīng)容劳。
例如:
//?`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.錯(cuò)誤處理:Async/await可以使用相同的代碼結(jié)構(gòu)——眾所周知的try/catch語句處理同步和異步錯(cuò)誤。讓我們看看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編寫條件代碼更直截了當(dāng):
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.堆椙橛框架:與async/await不同鸭蛙,從promise鏈返回的錯(cuò)誤堆棧不知道發(fā)生錯(cuò)誤的位置〗畹海看看下面的內(nèi)容:
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)試它們是一場(chǎng)噩夢(mèng)晒哄。例如睁宰,如果在.then塊中設(shè)置斷點(diǎn)并使用“stop-over”之類的調(diào)試快捷方式,則調(diào)試器將不會(huì)移動(dòng)到以下位置寝凌,因?yàn)樗煌ㄟ^同步代碼“steps”柒傻。
通過async/await,您可以完全按照正常的同步函數(shù)一步步地等待調(diào)用较木。
編寫異步JavaScript代碼不僅對(duì)于應(yīng)用程序本身而且對(duì)于編寫js庫也很重要红符。
例如,SessionStack庫會(huì)記錄您的Web應(yīng)用程序/網(wǎng)站中的所有內(nèi)容:所有DOM更改,用戶交互预侯,JavaScript異常致开,堆棧跟蹤,網(wǎng)絡(luò)請(qǐng)求失敗以及調(diào)試消息萎馅。
而這一切都必須在您的生產(chǎn)環(huán)境中發(fā)生双戳,而不會(huì)影響任何用戶體驗(yàn)。我們需要大量?jī)?yōu)化我們的代碼糜芳,并盡可能使其異步飒货,以便我們可以增加事件循環(huán)中可以處理的事件的數(shù)量。
而不只是js庫!在SessionStack中重現(xiàn)用戶會(huì)話時(shí)竟秫,我們必須在出現(xiàn)問題時(shí)渲染用戶瀏覽器中發(fā)生的所有事情民泵,并且必須重構(gòu)整個(gè)狀態(tài),以便在會(huì)話時(shí)間線中來回跳轉(zhuǎn)莫辨。為了使這成為可能,我們正在大量使用JavaScript提供的異步機(jī)制來實(shí)現(xiàn)毅访。
這里有一個(gè)免費(fèi)的計(jì)劃沮榜,你可以從這里開始。
資源:
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