js的事件循環機制:同步與異步任務(setTimeout,setInterval)宏任務,微任務(Promise,process.nextTick)

javascript是單線程,一切javascript版的"多線程"都是用單線程模擬出來的,經過事件循環(event loop)實現的異步。javascript

javascript事件循環

事件循環中的同步任務,異步任務:html

  • 同步和異步任務在不一樣的執行"場所",同步的進入主線程,異步的進入Event Table執行並註冊函數。前端

  • 當指定的異步事情完成時,Event Table會將這個函數移入Event Queuejava

  • 主線程內的任務執行完畢爲空,會去Event Queue讀取對應的函數,推入主線程執行。node

  • js引擎的monitoring process進程會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去Event Queue那裏檢查是否有等待被調用的函數。上述過程會不斷重複,也就是常說的Event Loop(事件循環)。ajax

用個例子說明上述過程:segmentfault

let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('發送成功!');
    }
})
console.log('代碼執行結束');
  • ajax(異步任務)進入Event Table,註冊回調函數success。promise

  • 執行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】
    })
})

 

參考文章:

這一次,完全弄懂 JavaScript 執行機制

10分鐘理解JS引擎的執行機制

從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理

前端基礎進階(十二):深刻核心,詳解事件循環機制

相關文章
相關標籤/搜索