前言
眾所周知,為了與瀏覽器進行交互,Javascript是一門非阻塞單線程腳本語言。
-
為何單線程? 因為如果在DOM操作中,有兩個線程一個添加節點,一個刪除節點,瀏覽器並不知道以哪個為准,所以只能選擇一個主線程來執行代碼,以防止沖突。雖然如今添加了webworker等新技術,但其依然只是主線程的子線程,並不能執行諸如I/O類的操作。長期來看,JS將一直是單線程。
-
為何非阻塞?因為單線程意味着任務需要排隊,任務按順序執行,如果一個任務很耗時,下一個任務不得不等待。所以為了避免這種阻塞,我們需要一種非阻塞機制。這種非阻塞機制是一種異步機制,即需要等待的任務不會阻塞主執行棧中同步任務的執行。這種機制是如下運行的:
- 所有同步任務都在主線程上執行,形成一個
執行棧(execution context stack)
- 等待任務的回調結果進入一種
任務隊列(task queue)
。 - 當主執行棧中的同步任務執行完畢后才會讀取
任務隊列
,任務隊列中的異步任務(即之前等待任務的回調結果)會塞入主執行棧, - 異步任務執行完畢后會再次進入下一個循環。此即為今天文章的主角
事件循環(Event Loop)
用一張圖展示這個過程:
Markdown - 所有同步任務都在主線程上執行,形成一個
正文
1.macro task與micro task
在實際情況中,上述的任務隊列(task queue)
中的異步任務分為兩種:微任務(micro task)
和宏任務(macro task)
。
- micro task事件:
Promises(瀏覽器實現的原生Promise)
、MutationObserver
、process.nextTick
<br /> - macro task事件:
setTimeout
、setInterval
、setImmediate
、I/O
、UI rendering
這里注意:script(整體代碼)
即一開始在主執行棧中的同步代碼本質上也屬於macrotask,屬於第一個執行的task
microtask和macotask執行規則:
- macrotask按順序執行,瀏覽器的ui繪制會插在每個macrotask之間
- microtask按順序執行,會在如下情況下執行:
- 每個callback之后,只要沒有其他的JS在主執行棧中
- 每個macrotask結束時
下面來個簡單例子:
console.log(1); setTimeout(function() { console.log(2); }, 0); new Promise(function(resolve,reject){ console.log(3) resolve() }).then(function() { console.log(4); }).then(function() { console.log(5); }); console.log(6);
一步一步分析如下:
- 1.同步代碼作為第一個macrotask,按順序輸出:1 3 6
- 2.microtask按順序執行:4 5
- 3.microtask清空后執行下一個macrotask:2
再來一個復雜的例子:
// Let's get hold of those elements var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // Let's listen for attribute changes on the // outer element new MutationObserver(function() { console.log('mutate'); }).observe(outer, { attributes: true }); // Here's a click listener… function onClick() { console.log('click'); setTimeout(function() { console.log('timeout'); }, 0); Promise.resolve().then(function() { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } // …which we'll attach to both elements inner.addEventListener('click', onClick); outer.addEventListener('click', onClick);
假設我們創建一個有里外兩部分的正方形盒子,里外都綁定了點擊事件,此時點擊內部,代碼會如何執行?一步一步分析如下:
- 1.觸發內部click事件,同步輸出:click
- 2.將setTimeout回調結果放入macrotask隊列
- 3.將promise回調結果放入microtask
- 4.將Mutation observers放入microtask隊列,主執行棧中onclick事件結束,主執行棧清空
- 5.依序執行microtask隊列中任務,輸出:promise mutate
- 6.注意此時事件冒泡,外部元素再次觸發onclick回調,所以按照前5步再次輸出:click promise mutate(我們可以注意到事件冒泡甚至會在microtask中的任務執行之后,microtask優先級非常高)
- 7.macrotask中第一個任務執行完畢,依次執行macrotask中剩下的任務輸出:timeout timeout