javascript是單線程,一切javascript版的"多線程"都是用單線程模擬出來的,通過事件循環(event loop)實現的異步。
javascript事件循環
事件循環中的同步任務,異步任務:
-
同步和異步任務在不同的執行"場所",同步的進入主線程,異步的進入Event Table執行並注冊函數。
-
當指定的異步事情完成時,Event Table會將這個函數移入Event Queue。
-
主線程內的任務執行完畢為空,會去Event Queue讀取對應的函數,推入主線程執行。
-
js引擎的monitoring process進程會持續不斷的檢查主線程執行棧是否為空,一旦為空,就會去Event Queue那里檢查是否有等待被調用的函數。上述過程會不斷重復,也就是常說的Event Loop(事件循環)。
用個例子說明上述過程:
let data = []; $.ajax({ url:www.javascript.com, data:data, success:() => { console.log('發送成功!'); } }) console.log('代碼執行結束');
-
ajax(異步任務)進入Event Table,注冊回調函數success。
-
執行console.log('代碼執行結束')。(同步任務在主線程執行)
-
ajax事件完成,回調函數success進入Event Queue。
-
主線程從Event Queue讀取回調函數success並執行。
setTimeout和setInterval中的執行時間
console.log('先執行這里'); setTimeout(() => { console.log('執行啦') },3000);
//先執行這里
// ... 3s later 3秒到了,計時事件timeout完成,定時器回調進入Event Queue,主線程執行已執行完,此時執行定時器回調。
// 執行啦
setTimeout明明寫的延時3秒,實際卻5,6秒才執行函數,這咋回事?
setTimeout(() => { task() },3000) sleep(10000000)
上述代碼在控制台執行task()需要的時間遠遠超過3秒,執行過程:
-
task()進入Event Table並注冊,計時開始。
-
執行sleep函數,很慢,非常慢,計時仍在繼續。
-
3秒到了,計時事件timeout完成,task()進入Event Queue,但是sleep也太慢了吧,還沒執行完,只好等着。
-
sleep終於執行完了,task()終於從Event Queue進入了主線程執行。
上述的流程走完,setTimeout這個函數是經過指定時間后,把要執行的任務(本例中為task())加入到Event Queue中,又因為是單線程任務要一個一個執行,如果前面的任務需要的時間太久,那么只能等着,導致真正的延遲時間遠遠大於3秒。
重點來了:定時器的毫秒,不是指過ms秒執行一次fn,而是過ms秒,會有fn進入Event Queue。
setTimeout(fn,0)的含義是,指定某個任務在主線程最早可得的空閑時間執行,意思就是不用再等多少秒了,只要主線程執行棧內的同步任務全部執行完成,棧為空就馬上執行。
關於setTimeout要補充的是,即便主線程為空,0毫秒實際上也是達不到的。根據HTML的標准,最低是4毫秒。
對於執行順序來說,setInterval會每隔指定的時間將注冊的函數置入Event Queue,如果前面的任務耗時太久,那么同樣需要等待。一旦setInterval的回調函數fn執行時間超過了延遲時間ms,那么就完全看不出來有時間間隔了。
宏任務和微任務
除了廣義的同步任務和異步任務,我們對任務有更精細的定義:
-
macro-task(宏任務):包括整體代碼script,setTimeout,setInterval
-
micro-task(微任務):Promise,process.nextTick
不同類型的任務會進入對應的Event Queue,比如setTimeout和setInterval會進入相同的Event Queue。
在宏任務和微任務概念中的事件循環機制:
主任務(宏任務)完——所有微任務——宏任務(找到宏任務其中一個任務隊列執行,其中如果又有微任務,該任務隊列執行完就執行微任務)——宏任務中另外一個任務隊列(里面偶微任務就再執行微任務)。
總的來說就是在宏任務和微任務之間來回切。下面列子執行過程:
第一輪:主線程輸出:【1,7】,添加宏任務【set1,set2】,添加微任務【6,8】。執行完主線程,然后執行微任務輸出【6,8】
第二輪:執行宏任務其中一個任務隊列set1:輸出【2,4】,執行任務的過程,碰到有微任務,所以在微任務隊列添加輸出【3,5】的微任務,在set1宏任務執行完就執行該微任務,第二輪總輸出:【2,4,3,5】
第三輪:執行任務另一個任務隊列set2:輸出【9,11】,執行任務的過程,碰到有微任任務,所以在微任務隊列添加輸出【10,12】的微任務,在set2宏任務執行完就執行該微任務,第三輪總輸出:【9,11,10,12】
整段代碼,共進行了三次事件循環,完整的輸出為1,7,6,8,2,4,3,5,9,11,10,12。(請注意,node環境下的事件監聽依賴libuv與前端環境不完全相同,輸出順序可能會有誤差)
console.log('1'); //第一輪主線程【1】
setTimeout(function() { //碰到set異步,丟入宏任務隊列【set1】:我將它命名為set1
console.log('2');//第二輪宏任務執行,輸出【2】
process.nextTick(function() {//第二輪宏任務執行,碰到process,丟入微任務隊列,【3】
console.log('3');
})
new Promise(function(resolve) {//第二輪宏任務執行,輸出【2,4】
console.log('4');
resolve();
}).then(function() {
console.log('5')//第二輪宏任務執行,碰到then丟入微任務隊列,【3,5】
})
})
process.nextTick(function() { //碰到process,丟入微任務隊列【6】
console.log('6'); //第一輪微任務執行
})
new Promise(function(resolve) {
console.log('7'); //new的同時執行代碼,第一輪主線程此時輸出【1,7】
resolve();
}).then(function() {
console.log('8') //第一輪主線程中promise的then丟入微任務隊列,此時微任務隊列為【6,8】。當第一輪微任務執行,順序輸出【6,8】
})
setTimeout(function() { //碰到set異步丟入宏任務隊列,此時宏任務隊列【set1.set2】:我將它命名為set2
console.log('9');//第三輪宏任務執行,輸出【9】
process.nextTick(function() { //第三輪宏中執行過程中添加到微任務【10】
console.log('10');
})
new Promise(function(resolve) {
console.log('11');//第三輪宏任務執行,宏任務累計輸出【9,11】
resolve();
}).then(function() {
console.log('12') //第三輪宏中執行過程中添加到微任務【10,12】
})
})
參考文章: