first
輸出結(jié)果是啥
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
。
從上面的輸出結(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é)果:
輸出順序為旨枯,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ù)隊列中缭贡。
主線程: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。
規(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ī)定了熬尺,實踐中要確保onFulfilled
和onRejected
異步執(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)境輸出:
其中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ù)都取完叛溢。
所以,js執(zhí)行任務(wù)的流程是這樣的:
- 第一個事件循環(huán)劲适,先執(zhí)行
script
中的所有同步代碼(即 macrotask 中的第一項任務(wù)) - 再取出 microtask 中的全部任務(wù)執(zhí)行
- 下一個事件循環(huán)楷掉,再回到 macrotask 取其中的下一項任務(wù)
- 再取出 microtask 中的全部任務(wù)執(zhí)行
- 反復(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 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ù)隊列中叫编。
主線程: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ù)都取完柱衔。