js異步

first

image.png

輸出結(jié)果是啥

image.png

ps 希望你能從以下的文章中找到答案

同步與異步模式簡介

我們知道,Javascript語言的執(zhí)行環(huán)境是單線程(single thread)的刽肠。

所謂"單線程"溃肪,就是指一次只能完成一件任務(wù)。如果有多個任務(wù)音五,就必須排隊惫撰,前面一個任務(wù)完成,再執(zhí)行后面一個任務(wù)躺涝,以此類推厨钻。

這種模式的好處是實現(xiàn)起來比較簡單,執(zhí)行環(huán)境相對單純坚嗜;壞處是只要有一個任務(wù)耗時很長夯膀,后面的任務(wù)都必須排隊等著,會拖延整個程序的執(zhí)行苍蔬。常見的瀏覽器無響應(yīng)(假死)诱建,往往就是因為某一段Javascript代碼長時間運行(比如死循環(huán)),導(dǎo)致整個頁面卡在這個地方碟绑,其他任務(wù)無法執(zhí)行涂佃。

為了解決這個問題,Javascript語言將任務(wù)的執(zhí)行模式分成兩種:同步(Synchronous)和異步(Asynchronous)蜈敢。

同步模式就是后一個任務(wù)等待前一個任務(wù)結(jié)束辜荠,然后再執(zhí)行,程序的執(zhí)行順序與任務(wù)的排列順序是一致的抓狭、同步的伯病;

異步模式則完全不同,每一個任務(wù)有一個或多個回調(diào)函數(shù)(callback)否过,前一個任務(wù)結(jié)束后午笛,不是執(zhí)行隊列上的后一個任務(wù),而是執(zhí)行回調(diào)函數(shù)苗桂;后一個任務(wù)則是不等前一個任務(wù)的回調(diào)函數(shù)的執(zhí)行而執(zhí)行药磺,所以程序的執(zhí)行順序與任務(wù)的排列順序是不一致的、異步的煤伟。

"異步模式"非常重要癌佩。在瀏覽器端木缝,耗時很長的操作都應(yīng)該異步執(zhí)行,避免瀏覽器失去響應(yīng)围辙,最好的例子就是Ajax操作我碟。在服務(wù)器端,"異步模式"甚至是唯一的模式姚建,因為執(zhí)行環(huán)境是單線程的矫俺,如果允許同步執(zhí)行所有http請求,服務(wù)器性能會急劇下降掸冤,很快就會失去響應(yīng)厘托。

異步任務(wù)隊列

可能有人告訴你,Javascript內(nèi)部存在著先進先出的異步任務(wù)隊列稿湿,僅僅用以存儲異步任務(wù)催烘,與同步任務(wù)分開管理。進程執(zhí)行完全部同步代碼后缎罢,每當(dāng)進程空閑伊群、觸發(fā)回調(diào)或定時器到達規(guī)定的時間,Javascript會從隊列中順序取出符合條件的異步任務(wù)并執(zhí)行之策精。

我們簡單驗證一下舰始,

var timeout1 = setTimeout(function() {
  console.log(2);
}, 0);

console.log(1);

var timeout2 =setTimeout(function() {
  console.log(3);
}, 0);

上面的代碼我們都知道輸出是,1 2 3咽袜,因為setTimeout是異步任務(wù)丸卷,而timeout1又比timeout2先注冊,所以最終輸出了這個結(jié)果询刹。

然而谜嫉,僅僅通過以上代碼我們確定不了同步任務(wù)究竟是不是會優(yōu)先于異步任務(wù)執(zhí)行,因為setTimeout有一個最小的時間間隔限制凹联,在這個時間間隔里語句console.log(1)完全可以執(zhí)行完畢沐兰,我們要想辦法讓同步代碼占用更長時間。

定時器最小時間間隔:在蘋果機上的最小時間間隔是10ms蔽挠,在Windows系統(tǒng)上的最小時間間隔大約是15ms住闯。Firefox中定義的最小時間間隔是10ms,而HTML5規(guī)范中定義的最小時間間隔是4ms澳淑。

再閱讀下面代碼比原,

setTimeout(function() {
  console.log(1);
}, 0);

console.log(2);

let end = Date.now() + 1000*5;

while (Date.now() < end) {
}

console.log(3);

end = Date.now() + 1000*5;

while (Date.now() < end) {
}

console.log(4);

輸出順序:2 3 4 1

image

從上面的輸出結(jié)果我們可以確定杠巡,異步代碼是在所有同步代碼執(zhí)行完畢以后才開始執(zhí)行的量窘。并且,兩段代碼的行為也沒有跟我們上述的理解有對不上的地方氢拥。

那我們剛剛對js異步任務(wù)隊列的理解方式是對的嗎蚌铜?底層機制會是這樣的嗎锨侯?

事實上,我們上述對于異步隊列的理解和解釋都是非常淺層和感性的(并且是錯誤的)厘线,雖然跟著上述的理解方式我們可以解釋很多代碼行為识腿,但實際的機制卻遠沒有這么簡單出革,異步模式作為Javascript的重中之重造壮,有很多設(shè)計細節(jié)是我們未知的,我們應(yīng)當(dāng)更加理性和學(xué)術(shù)地去探究學(xué)習(xí)骂束。

再看一段比較復(fù)雜的代碼耳璧,說出它的輸出順序:

setTimeout(function(){
    console.log(2);
},0);

new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});

console.log(6);

setTimeout(function(){
    console.log(7);
},0);

console.log(8);

你認為上述代碼輸出結(jié)果是什么呢?講出理由展箱。

以下為瀏覽器環(huán)境輸出結(jié)果:

image

輸出順序為旨枯,3 4 6 8 5 2 7,跟你事先認為的結(jié)果一樣嗎混驰?為什么結(jié)果會這樣攀隔?

除了注冊順序以外,還有什么因素影響著每個異步任務(wù)在異步隊列中的順序呢栖榨?

我們先一起了解下事件循環(huán)任務(wù)隊列兩個概念昆汹,再回來解答這個問題。

線程婴栽、事件循環(huán)和任務(wù)隊列

Javascript是單線程的满粗,但是卻能執(zhí)行異步任務(wù),這主要是因為 JS 中存在事件循環(huán)(Event Loop)和任務(wù)隊列(Task Queue)愚争。

事件循環(huán):JS 會創(chuàng)建一個類似于 while (true) 的循環(huán)映皆,每執(zhí)行一次循環(huán)體的過程稱之為Tick。每次Tick的過程就是查看是否有待處理事件轰枝,如果有則取出相關(guān)事件及回調(diào)函數(shù)放入執(zhí)行棧中由主線程執(zhí)行捅彻。待處理的事件會存儲在一個任務(wù)隊列中,也就是每次Tick會查看任務(wù)隊列中是否有需要執(zhí)行的任務(wù)鞍陨。

任務(wù)隊列:異步操作會將相關(guān)回調(diào)添加到任務(wù)隊列中沟饥。而不同的異步操作添加到任務(wù)隊列的時機也不同,如onclick, setTimeout,ajax處理的方式都不同湾戳,這些異步操作是由瀏覽器內(nèi)核的webcore來執(zhí)行的贤旷,webcore包含下圖中的3種 webAPI,分別是DOM Binding砾脑、network幼驶、timer模塊。

  • DOM Binding 模塊處理一些DOM綁定事件韧衣,如onclick事件觸發(fā)時盅藻,回調(diào)函數(shù)會立即被webcore添加到任務(wù)隊列中购桑。
  • network 模塊處理Ajax請求,在網(wǎng)絡(luò)請求返回時氏淑,才會將對應(yīng)的回調(diào)函數(shù)添加到任務(wù)隊列中勃蜘。
  • timer 模塊會對setTimeout等計時器進行延時處理,當(dāng)時間到達的時候假残,才會將回調(diào)函數(shù)添加到任務(wù)隊列中缭贡。
image

主線程:JS 只有一個線程,稱之為主線程辉懒。而事件循環(huán)是主線程中執(zhí)行棧里的代碼執(zhí)行完畢之后阳惹,才開始執(zhí)行的。所以眶俩,主線程中要執(zhí)行的代碼時間過長莹汤,會阻塞事件循環(huán)的執(zhí)行,也就會阻塞異步操作的執(zhí)行颠印。只有當(dāng)主線程中執(zhí)行棧為空的時候(即同步代碼執(zhí)行完后)纲岭,才會進行事件循環(huán)來觀察要執(zhí)行的事件回調(diào),當(dāng)事件循環(huán)檢測到任務(wù)隊列中有事件就取出相關(guān)回調(diào)放入執(zhí)行棧中由主線程執(zhí)行线罕。

ES5規(guī)范中對于事件循環(huán)的定義

翻開規(guī)范《ECMAScript? 2015 Language Specification》止潮,找到事件循環(huán) 6.1.4 Event loops

image

規(guī)范中中提到闻坚,一個瀏覽器環(huán)境沽翔,只能有一個事件循環(huán),而一個事件循環(huán)可以多個任務(wù)隊列窿凤,每個任務(wù)都有一個任務(wù)源(Task source)仅偎。

相同任務(wù)源的任務(wù),只能放到一個任務(wù)隊列中雳殊。

不同任務(wù)源的任務(wù)橘沥,可以放到不同任務(wù)隊列中。

又舉了一個例子說夯秃,客戶端可能實現(xiàn)了一個包含鼠標(biāo)鍵盤事件的任務(wù)隊列座咆,還有其他的任務(wù)隊列,而給鼠標(biāo)鍵盤事件的任務(wù)隊列更高優(yōu)先級仓洼,例如75%的可能性執(zhí)行它介陶。這樣就能保證流暢的交互性,而且別的任務(wù)也能執(zhí)行到了色建。同一個任務(wù)隊列中的任務(wù)必須按先進先出的順序執(zhí)行哺呜,但是不保證多個任務(wù)隊列中的任務(wù)優(yōu)先級,具體實現(xiàn)可能會交叉執(zhí)行箕戳。

結(jié)論:一個事件循環(huán)可以有多個任務(wù)隊列某残,隊列之間可有不同的優(yōu)先級国撵,同一隊列中的任務(wù)按先進先出的順序執(zhí)行,但是不保證多個任務(wù)隊列中的任務(wù)優(yōu)先級玻墅,具體實現(xiàn)可能會交叉執(zhí)行介牙。

重新看回開始的代碼:

setTimeout(function(){
    console.log(2);
},0);

new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});

console.log(6);

setTimeout(function(){
    console.log(7);
},0);

console.log(8);

輸出結(jié)果是,3 4 6 8 5 2 7澳厢。為什么setTimeout會后于promise.then執(zhí)行呢环础,原因或許就是它所處的任務(wù)隊列優(yōu)先級較低。

不同任務(wù)隊列的優(yōu)先級

那么接下來赏酥,我們探究一下不同任務(wù)隊列的優(yōu)先級喳整。

實際上谆构,對于任務(wù)隊列的優(yōu)先級的定義裸扶,Promise/A+ 規(guī)范中有作詳細的解釋。

圖靈社區(qū) : 閱讀 : 【翻譯】Promises/A+規(guī)范

我們都知道搬素,一個Promise的當(dāng)前狀態(tài)必須為以下三種狀態(tài)中的一種:等待態(tài)(Pending)呵晨、執(zhí)行態(tài)(Fulfilled)和拒絕態(tài)(Rejected)。

而上面的Promises規(guī)范就規(guī)定了熬尺,實踐中要確保onFulfilledonRejected異步執(zhí)行摸屠,且應(yīng)該在then方法被調(diào)用的那一輪事件循環(huán)以后的新執(zhí)行棧中執(zhí)行

意思就是粱哼,當(dāng)我們調(diào)用resolve()reject()的時候季二,觸發(fā)promise.then(...)實際上是一個異步操作,這個promise.then(...)并不是在resolve()reject()的時候就立刻執(zhí)行的揭措,而也是要重新進入任務(wù)隊列排隊的胯舷,不過能直接在當(dāng)前的事件循環(huán)新的執(zhí)行棧中被取出執(zhí)行(不用等下次事件循環(huán))。

知道這個以后绊含,我們再看一段代碼桑嘶,這個代碼包含常用的大部分異步操作,我們將借此得出不同任務(wù)隊列的優(yōu)先順序:

(其中setImmediate()process.nextTick()是node的語句)

setImmediate(function(){
    console.log(1);
},0);
setTimeout(function(){
    console.log(2);
},0);
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});
console.log(6);
process.nextTick(function(){
    console.log(7);
});
console.log(8);

NodeJs環(huán)境輸出

image

其中3 4 6 8是同步輸出的躬充。 因為注冊順序:1 > 2 > 5 > 7逃顶,而輸出順序是7 > 5 > 2 > 1

所以可以很容易得到充甚,優(yōu)先級 :process.nextTick > promise.then > setTimeout > setImmediate以政。

而實際上,上述的Promises規(guī)范早已提到異步隊列優(yōu)先級規(guī)定的詳細定義和解釋了伴找,并不需要我們一個一個去測試盈蛮。

在js引擎中,我們可以按性質(zhì)把任務(wù)分為兩類疆瑰,macrotask(宏任務(wù))和 microtask(微任務(wù))眉反。

  • macrotask(按優(yōu)先級順序排列): script(你的全部JS代碼昙啄,“同步代碼”), setTimeout, setInterval, setImmediate, I/O,UI rendering
  • microtask(按優(yōu)先級順序排列):process.nextTick,Promises(這里指瀏覽器原生實現(xiàn)的 Promise), Object.observe, MutationObserver
  • js引擎首先從macrotask queue中取出第一個任務(wù),執(zhí)行完畢后寸五,將microtask queue中的所有任務(wù)取出梳凛,按順序全部執(zhí)行;
  • 然后再從macrotask queue(宏任務(wù)隊列)中取下一個梳杏,執(zhí)行完畢后韧拒,再次將microtask queue(微任務(wù)隊列)中的全部取出;
  • 循環(huán)往復(fù)十性,直到兩個queue中的任務(wù)都取完叛溢。
image

所以,js執(zhí)行任務(wù)的流程是這樣的:

  1. 第一個事件循環(huán)劲适,先執(zhí)行script中的所有同步代碼(即 macrotask 中的第一項任務(wù))
  2. 再取出 microtask 中的全部任務(wù)執(zhí)行
  3. 下一個事件循環(huán)楷掉,再回到 macrotask 取其中的下一項任務(wù)
  4. 再取出 microtask 中的全部任務(wù)執(zhí)行
  5. 反復(fù)執(zhí)行事件循環(huán)…

現(xiàn)在,你可以根據(jù)這個流程再看回前面的代碼霞势,其實一切都很容易理解了…

以上烹植,就是Javascript任務(wù)隊列的順序機制。

小結(jié)

事件循環(huán):JS 會創(chuàng)建一個類似于 while (true) 的循環(huán)愕贡,每執(zhí)行一次循環(huán)體的過程稱之為Tick草雕。每次Tick的過程就是查看是否有待處理事件,如果有則取出相關(guān)事件及回調(diào)函數(shù)放入執(zhí)行棧中由主線程執(zhí)行固以。待處理的事件會存儲在一個任務(wù)隊列中墩虹,也就是每次Tick會查看任務(wù)隊列中是否有需要執(zhí)行的任務(wù)。

任務(wù)隊列:異步操作會將相關(guān)回調(diào)添加到任務(wù)隊列中憨琳。而不同的異步操作添加到任務(wù)隊列的時機也不同诫钓,如onclick, setTimeout,ajax處理的方式都不同,這些異步操作是由瀏覽器內(nèi)核的webcore來執(zhí)行的栽渴,webcore包含下圖中的3種 webAPI尖坤,分別是DOM Bindingnetwork闲擦、timer模塊慢味。

  • DOM Binding 模塊處理一些DOM綁定事件,如onclick事件觸發(fā)時墅冷,回調(diào)函數(shù)會立即被webcore添加到任務(wù)隊列中纯路。
  • network 模塊處理Ajax請求,在網(wǎng)絡(luò)請求返回時寞忿,才會將對應(yīng)的回調(diào)函數(shù)添加到任務(wù)隊列中驰唬。
  • timer 模塊會對setTimeout等計時器進行延時處理,當(dāng)時間到達的時候,才會將回調(diào)函數(shù)添加到任務(wù)隊列中叫编。

主線程:JS 只有一個線程辖佣,稱之為主線程。而事件循環(huán)是主線程中執(zhí)行棧里的代碼執(zhí)行完畢之后搓逾,才開始執(zhí)行的卷谈。所以,主線程中要執(zhí)行的代碼時間過長霞篡,會阻塞事件循環(huán)的執(zhí)行世蔗,也就會阻塞異步操作的執(zhí)行。只有當(dāng)主線程中執(zhí)行棧為空的時候(即同步代碼執(zhí)行完后)朗兵,才會進行事件循環(huán)來觀察要執(zhí)行的事件回調(diào)污淋,當(dāng)事件循環(huán)檢測到任務(wù)隊列中有事件就取出相關(guān)回調(diào)放入執(zhí)行棧中由主線程執(zhí)行。

一個事件循環(huán)可以有多個任務(wù)隊列余掖,隊列之間可有不同的優(yōu)先級寸爆,同一隊列中的任務(wù)按先進先出的順序執(zhí)行,但是不保證多個任務(wù)隊列中的任務(wù)優(yōu)先級浊吏,具體實現(xiàn)可能會交叉執(zhí)行而昨。

在js引擎中救氯,我們可以按性質(zhì)把任務(wù)分為兩類找田,macrotask(宏任務(wù))和 microtask(微任務(wù))。

  • macrotask(按優(yōu)先級順序排列): script(你的全部JS代碼着憨,“同步代碼”), setTimeout, setInterval, setImmediate, I/O,UI rendering
  • microtask(按優(yōu)先級順序排列):process.nextTick,Promises(這里指瀏覽器原生實現(xiàn)的 Promise), Object.observe, MutationObserver
  • js引擎首先從macrotask queue中取出第一個任務(wù)墩衙,執(zhí)行完畢后,將microtask queue中的所有任務(wù)取出甲抖,按順序全部執(zhí)行漆改;
  • 然后再從macrotask queue(宏任務(wù)隊列)中取下一個,執(zhí)行完畢后准谚,再次將microtask queue(微任務(wù)隊列)中的全部取出挫剑;
  • 循環(huán)往復(fù),直到兩個queue中的任務(wù)都取完柱衔。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末樊破,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子唆铐,更是在濱河造成了極大的恐慌哲戚,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件艾岂,死亡現(xiàn)場離奇詭異顺少,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門脆炎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來梅猿,“玉大人,你說我怎么就攤上這事秒裕×C唬” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵簇爆,是天一觀的道長癞松。 經(jīng)常有香客問我,道長入蛆,這世上最難降的妖魔是什么响蓉? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮哨毁,結(jié)果婚禮上枫甲,老公的妹妹穿的比我還像新娘。我一直安慰自己扼褪,他們只是感情好想幻,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著话浇,像睡著了一般脏毯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上幔崖,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天食店,我揣著相機與錄音,去河邊找鬼赏寇。 笑死吉嫩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的嗅定。 我是一名探鬼主播自娩,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼渠退!你這毒婦竟也來了忙迁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤智什,失蹤者是張志新(化名)和其女友劉穎动漾,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荠锭,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡旱眯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片删豺。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡共虑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出呀页,到底是詐尸還是另有隱情妈拌,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布蓬蝶,位于F島的核電站尘分,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏丸氛。R本人自食惡果不足惜培愁,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望缓窜。 院中可真熱鬧定续,春花似錦、人聲如沸禾锤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恩掷。三九已至倡鲸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間螃成,已是汗流浹背旦签。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留寸宏,地道東北人。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓偿曙,卻偏偏與公主長得像氮凝,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子望忆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349

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