一、什么是EventLoop?
想要了解event loop我們就要從js的工作原理說起。首先,大家都知道js是單線程的。所謂單線程就是進程中只有一個線程在運行。那么,js為什么是單線程而不是做成多線程的呢?個人理解,js是用來實現瀏覽器與用戶之間的交互的。如果同時要處理用戶點擊,用戶輸入,用戶關閉等操作,瀏覽器無法知道這個時間我到底應該做什么。所以js是從上至下按順序運行下去的
按照單線程的思想,順序執行我們的代碼,那么,如果我們的js中間向后台發送一個ajax請求,就要等到請求等到結果后才會繼續向下執行。如果請求耗時10秒,頁面就要停在這里10秒。這樣的用戶體驗很不好。。。因此,就有了同步任務、異步任務的區別
因為js的event loop機制,所以大家不要認為setTimeOut設置的事件到了延遲時間就是被執行。如果你的執行棧任務沒有被全部執行完,清空。setTimeOut事件執行的時間很有可能是要大於你設置的延時參數
二、宏任務和微任務
異步任務之間是有執行優先級的區別的。不同的異步任務會被分為兩類,微任務和宏任務
宏任務: 需要多次事件循環才能執行完,事件隊列中的每一個事件都是一個宏任務。瀏覽器為了能夠使得js內部宏任務與DOM任務有序的執行,會在一個宏任務執行結束后,在下一個宏執行開始前,對頁面進行重新渲染 (task->渲染->task->…)鼠標點擊會觸發一個事件回調,需要執行一個宏任務,然后解析HTML
微任務: 微任務是一次性執行完的。微任務通常來說是需要在當前task執行結束后立即執行的任務,例如對一些動作做出反饋或者異步執行任務又不需要分配一個新的task,這樣便可以提高一些性能。只要執行棧中沒有其他的js代碼正在執行了,而且每個宏任務都執行完了,微任務隊列會立即執行。如果在微任務執行期間微任務隊列加入了新的微任務,會將新的微任務加入隊列尾部,之后也會被執行。簡單理解,宏任務在下一輪事件循環執行,微任務在本輪事件循環的所有任務結束后執行
在一個事件循環中,異步事件返回結果后會被放到一個任務隊列中。然而,根據這個異步事件的類型,這個事件實際上會被對應的宏任務隊列或者微任務隊列中去。並且在當前執行棧為空的時候,主線程會 查看微任務隊列是否有事件存在。如果不存在,那么再去宏任務隊列中取出一個事件並把對應的回到加入當前執行棧;如果存在,則會依次執行隊列中事件對應的回調,直到微任務隊列為空,然后去宏任務隊列中取出最前面的一個事件,把對應的回調加入當前執行棧...如此反復,進入循環
宏任務:
包括整個代碼script( script中的代碼都屬於宏任務 )
setTimeout
setInterval
微任務:
promise.then()
process.nextTick
事件執行的順序:
先執行宏任務,然后執行微任務
三、案例
console.log("script start"); setTimeout(function(){ console.log("setTimeout"); },0) newPromise(resolve=>{ console.log("promise start"); resolve(); }).then(function(){ console.log("promise1"); }).then(()=>{ console.log("promise2"); }) console.log("script end"); 注意promise不算是一個異步任務它是一個同步任務
console.log(1); setTimeout(()=>{ console.log(2); }) newPromise((resolve)=>{ console.log(4) resolve() }).then(()=>{ setTimeout(()=>{ console.log(5); }) }).then(()=>{ console.log(6) }) console.log(7) setTimeout(() => { console.log(5) • new Promise(resolve => { console.log(6) • setTimeout(() => { console.log(7) }) resolve() }).then(() => { console.log(8) }) }, 500) new Promise(resolve => { console.log(9) resolve() }).then(() => { console.log(10) • setTimeout(() => { console.log(11) }, 0) }) •console.log(12)
四、NodeJS中的EventLoop
其實nodejs與瀏覽器的區別,就是nodejs的 宏任務 分好幾種,而這好幾種又有不同的 任務隊列,而不同的 任務隊列 又有順序區別,而 微任務是穿插在每一種【注意不是每一個!】宏任務 之間的
Timers 類型的宏任務隊列
setTimeout()
setInterval
Check 類型的宏任務隊列
setImmediate()
Close callback 類型的宏任務隊列
socket.on(‘close’, () => {})
Poll 類型的宏任務隊列
除了上面幾種的其他所有回調
五、nodeJs 里面的微任務隊列
process.nextTick()
Promise.then()
process.nextTick()
的優先級高於所有的微任務,每一次清空微任務列表的時候,都是先執行process.nextTick()
六、NodeJS中的EventLoop與瀏覽器的EventLoop之間的區別
瀏覽器
先執行 一個 宏任務,然后執行所有微任務…...循環往復
NodeJS
先執行一種 宏任務 在執行清空微任務 再一種 宏任務 在執行清空微任務 再一種 宏任務 在執行清空微任務 所有種類宏任務結束 … 循環往復
分析瀏覽器和NodeJS執行的順序
setTimeout(() =>{ console.log(‘timer1’) Promise.resolve().then(() =>{ console.log(‘promise1’) }) }, 0) setTimeout(() =>{ console.log(‘timer2’) Promise.resolve().then(() =>{ console.log(‘promise2’) }) }, 0)
七、setTimeout && setImmediate執行順序
Node 並不能保證 timers 在預設時間到了就會立即執行,因為 Node 對 timers 的過期檢查不一定靠譜,它會受機器上其它運行程序影響,或者那個時間點主線程不空閑
雖然 setTimeout 延時為 0,但是一般情況 Node 把 0 會設置為 1ms,所以,當 Node 准備 event loop 的時間大於 1ms 時,進入 timers 階段時,setTimeout 已經到期,則會先執行 setTimeout;反之,若進入 timers 階段用時小於 1ms,setTimeout 尚未到期,則會錯過 timers 階段,先進入 check 階段,而先執行 setImmediate
setTimeout(() =>{ console.log('timeout') }, 0) setImmediate(() =>{ console.log('immediate') })
有一種情況,它們兩者的順序是固定的
constfs=require('fs') fs.readFile('test.txt', () =>{ console.log('readFile') setTimeout(() =>{ console.log('timeout') }, 0) setImmediate(() =>{ console.log('immediate') }) })
此時 setTimeout 和 setImmediate 是寫在 I/O callbacks 中的,這意味着,我們處於 poll 階段,然后是 check 階段,所以這時無論 setTimeout 到期多么迅速,都會先執行 setImmediate。本質上是因為,我們從 poll 階段開始執行
總結
當 setTimeout() 和 setImmediate() 都寫在 main 里面的時候 不一定誰先執行誰后執行
當 setTimeout() 和 setImmediate() 都寫在一個 I/O 回調 或者說一個 poll 類型宏任務的回調里面的時候 一定是先執行 setImmediate() 后執行 setTimeout()
八、Poll 階段的兩個主要功能
setImmediate 的 queue 不為空,則進入 check 階段,然后是 close callbacks 階段……