解讀 JavaScript 之事件循環(huán)和異步編程

歡迎閱讀專門探索 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

http://nikgrozev.com/2017/10/01/async-await/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末喻粹,一起剝皮案震驚了整個(gè)濱河市蟆融,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌守呜,老刑警劉巖型酥,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異查乒,居然都是意外死亡弥喉,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門玛迄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來由境,“玉大人,你說我怎么就攤上這事蓖议÷步埽” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵勒虾,是天一觀的道長(zhǎng)纺阔。 經(jīng)常有香客問我,道長(zhǎng)修然,這世上最難降的妖魔是什么笛钝? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任质况,我火速辦了婚禮,結(jié)果婚禮上玻靡,老公的妹妹穿的比我還像新娘结榄。我一直安慰自己,他們只是感情好啃奴,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布潭陪。 她就那樣靜靜地躺著,像睡著了一般最蕾。 火紅的嫁衣襯著肌膚如雪依溯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天瘟则,我揣著相機(jī)與錄音黎炉,去河邊找鬼。 笑死醋拧,一個(gè)胖子當(dāng)著我的面吹牛慷嗜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播丹壕,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼庆械,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了菌赖?” 一聲冷哼從身側(cè)響起缭乘,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎琉用,沒想到半個(gè)月后堕绩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡邑时,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年奴紧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晶丘。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡黍氮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出铣口,到底是詐尸還是另有隱情滤钱,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布脑题,位于F島的核電站,受9級(jí)特大地震影響铜靶,放射性物質(zhì)發(fā)生泄漏叔遂。R本人自食惡果不足惜他炊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望已艰。 院中可真熱鬧痊末,春花似錦、人聲如沸哩掺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嚼吞。三九已至盒件,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間舱禽,已是汗流浹背炒刁。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留誊稚,地道東北人翔始。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像里伯,于是被迫代替她去往敵國和親城瞎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Promise 對(duì)象 Promise 的含義 Promise 是異步編程的一種解決方案疾瓮,比傳統(tǒng)的解決方案——回調(diào)函...
    neromous閱讀 8,707評(píng)論 1 56
  • title: promise總結(jié) 總結(jié)在前 前言 下文類似 Promise#then脖镀、Promise#resolv...
    JyLie閱讀 12,244評(píng)論 1 21
  • 一、Promise的含義 Promise在JavaScript語言中早有實(shí)現(xiàn)爷贫,ES6將其寫進(jìn)了語言標(biāo)準(zhǔn)认然,統(tǒng)一了用法...
    Alex灌湯貓閱讀 826評(píng)論 0 2
  • 本文適用的讀者 本文寫給有一定Promise使用經(jīng)驗(yàn)的人,如果你還沒有使用過Promise漫萄,這篇文章可能不適合你卷员,...
    HZ充電大喵閱讀 7,310評(píng)論 6 19
  • 短短的兩天的培訓(xùn)課程,雖然短暫但是對(duì)于我的平時(shí)工作十分有用處腾务,可以說都是我們?nèi)粘5臉I(yè)務(wù)問題毕骡。 票據(jù)真...
    無_e9f0閱讀 234評(píng)論 0 0