1、爲什麼要有事件循環?
因爲js是單線程的,事件循環是js的執行機制,也是js實現異步的一種方法。
既然js是單線程,那就像只有一個窗口的銀行,客戶需要排隊一個一個辦理業務,同理js任務也要一個一個順序執行。如果一個任務耗時
過長,那麼後一個任務也必須等着。那麼問題來了,假如我們想瀏覽新聞,但是新聞包含的超清圖片加載很慢,難道我們的網頁要一直卡着
直到圖片完全顯示出來?因此聰明的程序員將任務分爲兩類:
- 同步任務
- 異步任務
當我們打開網站時,網頁的渲染過程就是一大堆同步任務,比如頁面骨架和頁面元素的渲染。而像加載圖片音樂之類佔用資源大耗時久的任務,
就是異步任務。
2、宏任務與微任務
JavaScript中除了廣泛的同步任務和異步任務,我們對任務有更精細的定義:
- macro-task(宏任務): 包括
整體代碼script
,setTimeout
,setInterval
- micro-task(微任務):
Promise
,process.nextTick
不同的類型的任務會進入不同的Event Queue(事件隊列),比如setTimeout、setInterval會進入一個事件隊列,而Promise會進入
另一個事件隊列。
一次事件循環中有宏任務隊列和微任務隊列。事件循環的順序,決定js代碼執行的順序。進入整體代碼(宏任務-<script>包裹的代碼可以
理解爲第一個宏任務),開始第一次循環,接着執行所有的微任務。然後再次從宏任務開始,找到其中一個任務隊列的任務執行完畢,
再執行所有的微任務。如:
<script>
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
/* ----------------------------分析 start--------------------------------- */
1、`<script>`中的整段代碼作爲第一個宏任務,進入主線程。即開啓第一次事件循環
2、遇到setTimeout,將其回調函數放入Event table中註冊,然後分發到宏任務Event Queue中
3、接下來遇到new Promise、Promise,立即執行;將then函數分發到微任務Event Queue中。輸出: promise
4、遇到console.log,立即執行。輸出: console
5、整體代碼作爲第一個宏任務執行結束,此時去微任務隊列中查看有哪些微任務,結果發現了then函數,然後將它推入主線程並執行。
輸出: then
6、第一輪事件循環結束
開啓第二輪事件循環。先從宏任務開始,去宏任務事件隊列中查看有哪些宏任務,在宏任務事件隊列中找到了setTimeout對應的回調函數,
立即執行之。此時宏任務事件隊列中已經沒有事件了,然後去微任務事件隊列中查看是否有事件,結果沒有。此時第二輪事件循環結束;
輸出:setTimeout
/* ----------------------------分析 end--------------------------------- */
</script>
3、分析更復雜的代碼
<script>
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
</script>
一、第一輪事件循環
a)、整段<script>代碼作爲第一個宏任務進入主線程,即開啓第一輪事件循環
b)、遇到console.log,立即執行。輸出:1
c)、遇到setTimeout,將其回調函數放入Event table中註冊,然後分發到宏任務事件隊列中。我們將其標記爲setTimeout1
d)、遇到process.nextTick,其回調函數放入Event table中註冊,然後被分發到微任務事件隊列中。記爲process1
e)、遇到new Promise、Promise,立即執行;then回調函數放入Event table中註冊,然後被分發到微任務事件隊列中。記爲then1。
輸出: 7
f)、遇到setTimeout,將其回調函數放入Event table中註冊,然後分發到宏任務事件隊列中。我們將其標記爲setTimeout2
此時第一輪事件循環宏任務結束,下表是第一輪事件循環宏任務結束時各Event Queue的情況
- | 宏任務事件隊列 | 微任務事件隊列 |
---|---|---|
第一輪事件循環 | (宏任務已結束) | process1、then1 |
第二輪事件循環(未開始) | setTimeout1 | |
第三輪事件循環(未開始) | setTimeout2 |
可以看到第一輪事件循環宏任務結束後微任務事件隊列中還有兩個事件待執行,因此這兩個事件會被推入主線程,然後執行
g)、執行process1。輸出:6
h)、執行then1。輸出:8
第一輪事件循環正式結束!
二、第二輪事件循環
a)、第二輪事件循環從宏任務setTimeout1開始。遇到console.log,立即執行。輸出: 2
b)、遇到process.nextTick,其回調函數放入Event table中註冊,然後被分發到微任務事件隊列中。記爲process2
c)、遇到new Promise,立即執行;then回調函數放入Event table中註冊,然後被分發到微任務事件隊列中。記爲then2。輸出: 5
此時第二輪事件循環宏任務結束,下表是第二輪事件循環宏任務結束時各Event Queue的情況
- | 宏任務事件隊列 | 微任務事件隊列 |
---|---|---|
第一輪事件循環(已結束) | ||
第二輪事件循環 | (宏任務已結束) | process2、then2 |
第三輪事件循環(未開始) | setTimeout2 |
可以看到第二輪事件循環宏任務結束後微任務事件隊列中還有兩個事件待執行,因此這兩個事件會被推入主線程,然後執行
d)、執行process2。輸出:3
e)、執行then2。輸出:5
第二輪事件循環正式結束!
三、第三輪事件循環
a)、第三輪事件循環從宏任務setTimeout2開始。遇到console.log,立即執行。輸出: 9
d)、遇到process.nextTick,其回調函數放入Event table中註冊,然後被分發到微任務事件隊列中。記爲process3
c)、遇到new Promise,立即執行;then回調函數放入Event table中註冊,然後被分發到微任務事件隊列中。記爲then3。輸出: 11
此時第三輪事件循環宏任務結束,下表是第三輪事件循環宏任務結束時各Event Queue的情況
- | 宏任務事件隊列 | 微任務事件隊列 |
---|---|---|
第一輪事件循環(已結束) | ||
第二輪事件循環(已結束) | ||
第三輪事件循環(未開始) | (宏任務已結束) | process3、then3 |
可以看到第二輪事件循環宏任務結束後微任務事件隊列中還有兩個事件待執行,因此這兩個事件會被推入主線程,然後執行
d)、執行process3。輸出:10
e)、執行then3。輸出:12