JS的代碼執行是基於一種事件循環的機制,之因此稱做事件循環,MDN給出的解釋爲css
由於它常常被用於相似以下的方式來實現html
while (queue.waitForMessage()) { queue.processNextMessage(); }若是當前沒有任何消息
queue.waitForMessage
會等待同步消息到達html5
咱們能夠把它當成一種程序結構的模型,處理的方案。更詳細的描述能夠查看 這篇文章node
而JS的運行環境主要有兩個:瀏覽器、Node。git
在兩個環境下的Event Loop實現是不同的,在瀏覽器中基於 規範 來實現,不一樣瀏覽器可能有小小區別。在Node中基於 libuv 這個庫來實現github
JS是單線程執行的,而基於事件循環模型,造成了基本沒有阻塞(除了alert或同步XHR等操做)的狀態web
根據 規範,每一個線程都有一個事件循環(Event Loop),在瀏覽器中除了主要的頁面執行線程 外,Web worker是在一個新的線程中運行的,因此能夠將其獨立看待。segmentfault
每一個事件循環有至少一個任務隊列(Task Queue,也能夠稱做Macrotask宏任務),各個任務隊列中放置着不一樣來源(或者不一樣分類)的任務,可讓瀏覽器根據本身的實現來進行優先級排序api
以及一個微任務隊列(Microtask Queue),主要用於處理一些狀態的改變,UI渲染工做以前的一些必要操做(能夠防止屢次無心義的UI渲染)promise
主線程的代碼執行時,會將執行程序置入執行棧(Stack)中,執行完畢後出棧,另外有個堆空間(Heap),主要用於存儲對象及一些非結構化的數據
一開始
宏任務與微任務隊列裏的任務隨着:任務進棧、出棧、任務出隊、進隊之間交替着進行
從macrotask隊列中取出一個任務處理,處理完成以後(此時執行棧應該是空的),從microtask隊列中一個個按順序取出全部任務進行處理,處理完成以後進入UI渲染後續工做
須要注意的是:microtask並非在macrotask完成以後纔會觸發,在回調函數以後,只要執行棧是空的,就會執行microtask。也就是說,macrotask執行期間,執行棧多是空的(好比在冒泡事件的處理時)
而後循環繼續
常見的macrotask有:
run <script>(同步的代碼執行)
setInterval
setImmediate (Node環境中)
requestAnimationFrame
I/O
UI rendering
常見的microtask有:
process.nextTick (Node環境中)
Promise callback
Object.observe (基本上已經廢棄)
MutationObserver
macrotask種類不少,還有 dispatch event事件派發等
run <script>這個可能看起來比較奇怪,能夠把它當作一段代碼(針對單個<script>標籤)的同步順序執行,主要用來描述執行程序的第一步執行
dispatch event主要用來描述事件觸發以後的執行任務,好比用戶點擊一個按鈕,觸發的onClick回調函數。須要注意的是,事件的觸發是同步的,這在下文有例子說明
注:
固然,也可認爲 run <script>不屬於macrotask,畢竟規範也沒有這樣的說明,也能夠將其視爲主線程上的同步任務,不在主線程上的其餘部分爲異步任務
先來看看這段蠻複雜的代碼,思考一下會輸出什麼
console.log('start'); var intervalA = setInterval(() => { console.log('intervalA'); }, 0); setTimeout(() => { console.log('timeout'); clearInterval(intervalA); }, 0); var intervalB = setInterval(() => { console.log('intervalB'); }, 0); var intervalC = setInterval(() => { console.log('intervalC'); }, 0); new Promise((resolve, reject) => { console.log('promise'); for (var i = 0; i < 10000; ++i) { i === 9999 && resolve(); } console.log('promise after for-loop'); }).then(() => { console.log('promise1'); }).then(() => { console.log('promise2'); clearInterval(intervalB); }); new Promise((resolve, reject) => { setTimeout(() => { console.log('promise in timeout'); resolve(); }); console.log('promise after timeout'); }).then(() => { console.log('promise4'); }).then(() => { console.log('promise5'); clearInterval(intervalC); }); Promise.resolve().then(() => { console.log('promise3'); }); console.log('end');
上述代碼結合了常規執行代碼,setTimeout,setInterval,Promise
答案爲
在解釋爲何以前,先看一個更簡單的例子
console.log('start'); setTimeout(() => { console.log('timeout'); }, 0); Promise.resolve().then(() => { console.log('promise'); }); console.log('end');
大概的步驟,文字有點多
1. 運行時(runtime)識別到log方法爲通常的函數方法,將其入棧,而後執行輸出 start 再出棧
2. 識別到setTimeout爲特殊的異步方法(macrotask),將其交由其餘內核模塊處理,setTimeout的匿名回調函數被放入macrotask隊列中,並設置了一個 0ms的當即執行標識(提供後續模塊的檢查)
3. 識別到Promise的resolve方法爲通常的方法,將其入棧,而後執行 再出棧
4. 識別到then爲Promise的異步方法(microtask),將其交由其餘內核模塊處理,匿名回調函數被放入microtask隊列中
5. 識別到log方法爲通常的函數方法,將其入棧,而後執行輸出 end 再出棧
6. 主線程執行完畢,棧爲空,隨即從microtask隊列中取出隊首的項,
這裏隊首爲匿名函數,匿名函數裏面有 console的log方法,也將其入棧(若是執行過程當中識別到特殊的方法,就在這時交給其餘模塊處理到對應隊列尾部),
輸出 promise後出棧,並將這一項從隊列中移除
7. 繼續檢查microtask隊列,當前隊列爲空,則將當前macrotask出隊,進入下一步(若是不爲空,就繼續取下一個microtask執行)
8.檢查是否須要進行UI從新渲染等,進行渲染...
9. 進入下一輪事件循環,檢查macrotask隊列,取出一項進行處理
因此最終的結果是
再看上面那個例子,對比起來只是代碼多了點,混入了setInterval,多個setTimeout與promise的函數部分,按照上面的思路,應該不難理解
須要注意的三點:
1. clearInterval(intervalA); 運行的時候,實際上已經執行了 intervalA 的macrotask了
2. promise函數內部是同步處理的,不會放到隊列中,放入隊列中的是它的then或catch回調
3. promise的then返回的仍是promise,因此在輸出promise4後,繼續檢測到後續的then方法,立刻放到microtask隊列尾部,再繼續取出執行,立刻輸出promise5;
而輸出promise1以後,爲何沒有立刻輸出promise2呢?由於此時promise1所在任務以後是promise3的任務,1和3在promise函數內部返回後就添加至隊列中,2在1執行以後才添加
再來看個例子,就有點微妙了
<script> console.log('start'); setTimeout(() => { console.log('timeout1'); }, 0); Promise.resolve().then(() => { console.log('promise1'); }); </script> <script> setTimeout(() => { console.log('timeout2'); }, 0); requestAnimationFrame(() => { console.log('requestAnimationFrame'); }); Promise.resolve().then(() => { console.log('promise2'); }); console.log('end'); </script>
輸出結果
requestAnimationFrame是在setTimeout以前執行的,start以後並非直接輸出end,也許這兩個<script>標籤被獨立處理了
來看一個關於DOM操做的例子,Tasks, microtasks, queues and schedules
<style type="text/css"> .outer { width: 100px; background: #eee; height: 100px; margin-left: 300px; margin-top: 150px; display: flex; align-items: center; justify-content: center; } .inner { width: 50px; height: 50px; background: #ddd; } </style> <script> var outer = document.querySelector('.outer'), inner = document.querySelector('.inner'), clickTimes = 0; new MutationObserver(() => { console.log('mutate'); }).observe(outer, { attributes: true }); function onClick() { console.log('click'); setTimeout(() => { console.log('timeout'); }, 0); Promise.resolve().then(() => { console.log('promise'); }); outer.setAttribute('data-click', clickTimes++); } inner.addEventListener('click', onClick); outer.addEventListener('click', onClick); // inner.click(); // console.log('done'); </script>
點擊內部的inner塊,會輸出什麼呢?
MutationObserver優先級比promise高,雖然在一開始就被定義,但其實是觸發以後纔會被添加到microtask隊列中,因此先輸出了promise
兩個timeout回調都在最後才觸發,由於click事件冒泡了,事件派發這個macrotask任務包括了先後兩個onClick回調,兩個回調函數都執行完以後,纔會執行接下來的 setTimeout任務
期間第一個onClick回調完成後執行棧爲空,就立刻接着執行microtask隊列中的任務
若是把代碼的註釋去掉,使用代碼自動 click(),思考一下,會輸出什麼?
能夠看到,事件處理是同步的,done在連續輸出兩個click以後才輸出
而mutate只有一個,是由於當前執行第二個onClick回調的時候,microtask隊列中已經有一個MutationObserver,它是第一個回調的,由於事件同步的緣由沒有被及時執行。瀏覽器會對MutationObserver進行優化,不會重複添加監聽回調
在Node環境中,macrotask部分主要多了setImmediate,microtask部分主要多了process.nextTick,而這個nextTick是獨立出來自成隊列的,優先級高於其餘microtask
不過事件循環的的實現就不太同樣了,能夠參考 Node事件文檔 libuv事件文檔
Node中的事件循環有6個階段
setTimeout()
和 setInterval()
中到期的callbacksocket.on("close",func)
每一輪事件循環都會通過六個階段,在每一個階段後,都會執行microtask
比較特殊的是在poll階段,執行程序同步執行poll隊列裏的回調,直到隊列爲空或執行的回調達到系統上限
接下來再檢查有無預設的setImmediate,若是有就轉入check階段,沒有就先查詢最近的timer的距離,以其做爲poll階段的阻塞時間,若是timer隊列是空的,它就一直阻塞下去
而nextTick並不在這些階段中執行,它在每一個階段以後都會執行
看一個例子
setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4)); console.log(5);
根據以上知識,應該很快就能知道輸出結果是 5 3 4 1 2
修改一下
process.nextTick(() => console.log(1)); Promise.resolve().then(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => { process.nextTick(() => console.log(0)); console.log(4); });
輸出爲 1 3 2 4 0,由於nextTick隊列優先級高於同一輪事件循環中其餘microtask隊列
修改一下
process.nextTick(() => console.log(1)); console.log(0); setTimeout(()=> { console.log('timer1'); Promise.resolve().then(() => { console.log('promise1'); }); }, 0); process.nextTick(() => console.log(2)); setTimeout(()=> { console.log('timer2'); process.nextTick(() => console.log(3)); Promise.resolve().then(() => { console.log('promise2'); }); }, 0);
輸出爲
與在瀏覽器中不一樣,這裏promise1並非在timer1以後輸出,由於在setTimeout執行的時候是出於timer階段,會先一併處理timer回調
setTimeout是優先於setImmediate的,但接下來這個例子卻不必定是先執行setTimeout的回調
setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });
由於在Node中識別不了0ms的setTimeout,至少也得1ms.
因此,若是在進入該輪事件循環的時候,耗時不到1ms,則setTimeout會被跳過,進入check階段執行setImmediate回調,先輸出 immediate
若是超過1ms,timer階段中就能夠立刻處理這個setTimeout回調,先輸出 timeout
修改一下代碼,讀取一個文件讓事件循環進入IO文件讀取的poll階段
let fs = require('fs'); fs.readFile('./event.html', () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
這麼一來,輸出結果確定就是 先 immediate 後 timeout
知道JS的事件循環是怎麼樣的了,就須要知道怎麼才能把它用好
1. 在microtask中不要放置複雜的處理程序,防止阻塞UI的渲染
2. 可使用process.nextTick處理一些比較緊急的事情
3. 能夠在setTimeout回調中處理上輪事件循環中UI渲染的結果
4. 注意不要濫用setInterval和setTimeout,它們並非能夠保證可以按時處理的,setInterval甚至還會出現丟幀的狀況,可考慮使用 requestAnimationFrame
5. 一些可能會影響到UI的異步操做,可放在promise回調中處理,防止多一輪事件循環致使重複執行UI的渲染
6. 在Node中使用immediate來可能會獲得更多的保證
7. 不要糾結