JavaScript 是一門單線程語言,之因此說是單線程,是由於在瀏覽器中,若是是多線程,而且兩個線程同時操做了同一個 Dom 元素,那最後的結果會出現問題。因此,JavaScript 是單線程的,可是若是徹底由上至下的一行一行執行代碼,假如一個代碼塊執行了很長的時間,後面必需要等待當前執行完畢,這樣的效率是很是低的,因此有了異步的概念,確切的說,JavaScript 的主線程是單線程的,可是也有其餘的線程去幫咱們實現異步操做,好比定時器線程、事件線程、Ajax 線程。git
在瀏覽器中執行 JavaScript 有兩個區域,一個是咱們平時所說的同步代碼執行,是在棧中執行,原則是先進後出,而在執行異步代碼的時候分爲兩個隊列,macro-task
(宏任務)和 micro-task
(微任務),遵循先進先出的原則。瀏覽器
// 做用域鏈 function one() { console.log(1); function two() { console.log(2); function three() { console.log(3); } three(); } two(); } one(); // 1 // 2 // 3
上面的代碼都是同步的代碼,在執行的時候先將全局做用域放入棧中,執行全局做用域中的代碼,解析了函數 one
,當執行函數調用 one()
的時候將 one
的做用域放入棧中,執行 one
中的代碼,打印了 1
,解析了 two
,執行 two()
,將 two
放入棧中,執行 two
,打印了 2
,解析了 three
,執行了 three()
,將 three
放入棧中,執行 three
,打印了 3
。多線程
在函數執行完釋放的過程當中,由於全局做用域中有 one
正在執行,one
中有 two
正在執行,two
中有 three
正在執行,因此釋放內存時必須由內層向外層釋放,three
執行後釋放,此時 three
再也不佔用 two
的執行環境,將 two
釋放,two
再也不佔用 one
的執行環境,將 one
釋放,one
再也不佔用全局做用域的執行環境,最後釋放全局做用域,這就是在棧中執行同步代碼時的先進後出原則,更像是一個杯子,先放進去的在最下面,須要最後取出。異步
而異步隊列更像時一個管道,有兩個口,從入口進,從出口出,因此是先進先出,在宏任務隊列中表明的有 setTimeout
、setInterval
、setImmediate
、MessageChannel
,微任務的表明爲 Promise 的 then
方法、MutationObserve
(已廢棄)。函數
案例 1post
let messageChannel = new MessageChannel(); let prot2 = messageChannel.port2; messageChannel.port1.postMessage("I love you"); console.log(1); prot2.onmessage = function(e) { console.log(e.data); }; console.log(2); // 1 // 2 // I love you
從上面案例中能夠看出,MessageChannel
是宏任務,晚於同步代碼執行。ui
案例 2spa
setTimeout(() => console.log(1), 2000); setTimeout(() => console.log(2), 1000); console.log(3); // 3 // 2 // 1
上面代碼能夠看出其實 setTimeout
並非在同步代碼執行的時候就放入了異步隊列,而是等待時間到達時纔會放入異步隊列,因此纔會有了上面的結果。線程
案例 3code
setImmediate(function() { console.log("setImmediate"); }); setTimeout(function() { console.log("setTimeout"); }, 0); console.log(1); // 1 // setTimeout // setImmediate
同爲宏任務,setImmediate
在 setTimeout
延遲時間爲 0
時是晚於 setTimeout
被放入異步隊列的,這裏須要注意的是 setImmediate
在瀏覽器端,到目前爲止只有 IE 實現了。
上面的案例都是關於宏任務,下面咱們舉一個有微任務的案例來看一看微任務和宏任務的執行機制,在瀏覽器端微任務的表明其實就是 Promise 的 then
方法。
案例 4
setTimeout(() => { console.log("setTimeout1"); Promise.resolve().then(data => { console.log("Promise1"); }); }, 0); Promise.resolve().then(data => { console.log("Promise2"); setTimeout(() => { console.log("setTimeout2"); }, 0); }); // Promise2 // setTimeout1 // Promise1 // setTimeout2
從上面的執行結果其實能夠看出,同步代碼在棧中執行完畢後會先去執行微任務隊列,將微任務隊列執行完畢後,會去執行宏任務隊列,宏任務隊列執行一個宏任務之後,會去看看有沒有產生新的微任務,若是有則清空微任務隊列後再執行下一個宏任務,依次輪詢,直到清空整個異步隊列。
在 Node 中的事件輪詢機制與瀏覽器類似又不一樣,類似的是,一樣先在棧中執行同步代碼,一樣是先進後出,不一樣的是 Node 有本身的多個處理不一樣問題的階段和對應的隊列,也有本身內部實現的微任務 process.nextTick
,Node 的整個事件輪詢機制是 Libuv 庫實現的。
Node 中事件輪詢的流程以下圖:
從圖中能夠看出,在 Node 中有多個隊列,分別執行不一樣的操做,而每次在隊列切換的時候都去執行一次微任務隊列,反覆的輪詢。
案例 1
setTimeout(function() { console.log("setTimeout"); }, 0); setImmediate(function() { console.log("setInmediate"); });
默認狀況下 setTimeout
和 setImmediate
是不知道哪個先執行的,順序不固定,Node 執行的時候有準備的時間,setTimeout
延遲時間設置爲 0
實際上是大概 4ms
,假設 Node 準備時間在 4ms
以內,開始執行輪詢,定時器沒到時間,因此輪詢到下一隊列,此時要等再次循環到 timer
隊列後執行定時器,因此會先執行 check
隊列的 setImmediate
。
若是 Node 執行的準備時間大於了 4ms
,由於執行同步代碼後,定時器的回調已經被放入 timer
隊列,因此會先執行 timer
隊列。
案例 2
setTimeout(() => { console.log("setTimeout1"); Promise.resolve().then(() => { console.log("Promise1"); }); }, 0); setTimeout(() => { console.log("setTimeout2"); }, 0); console.log(1); // 1 // setTimeout1 // setTimeout2 // Promise1
Node 事件輪詢中,輪詢到每個隊列時,都會將當前隊列任務清空後,在切換下一隊列以前清空一次微任務隊列,這是與瀏覽器端不同的。
瀏覽器端會在宏任務隊列當中執行一個任務後插入執行微任務隊列,清空微任務隊列後,再回到宏任務隊列執行下一個宏任務。
上面案例在 Node 事件輪詢中,會將 timer
隊列清空後,在輪詢下一個隊列以前執行微任務隊列。
案例 3
setTimeout(() => { console.log("setTimeout1"); }, 0); setTimeout(() => { console.log("setTimeout2"); }, 0); Promise.resolve().then(() => { console.log("Promise1"); }); console.log(1); // 1 // Promise1 // setTimeout1 // setTimeout2
上面代碼的執行過程是,先執行棧,棧執行時打印 1
,Promise.resolve()
產生微任務,棧執行完畢,從棧切換到 timer
隊列以前,執行微任務隊列,再去執行 timer
隊列。
案例 4
setImmediate(() => { console.log("setImmediate1"); setTimeout(() => { console.log("setTimeout1"); }, 0); }); setTimeout(() => { console.log("setTimeout2"); setImmediate(() => { console.log("setImmediate2"); }); }, 0); //結果1 // setImmediate1 // setTimeout2 // setTimeout1 // setImmediate2 // 結果2 // setTimeout2 // setImmediate1 // setImmediate2 // setTimeout1
setImmediate
和 setTimeout
執行順序不固定,假設 check
隊列先執行,會執行 setImmediate
打印 setImmediate1
,將遇到的定時器放入 timer
隊列,輪詢到 timer
隊列,由於在棧中執行同步代碼已經在 timer
隊列放入了一個定時器,因此按前後順序執行兩個 setTimeout
,執行第一個定時器打印 setTimeout2
,將遇到的 setImmediate
放入 check
隊列,執行第二個定時器打印 setTimeout1
,再次輪詢到 check
隊列執行新加入的 setImmediate
,打印 setImmediate2
,產生結果 1
。
假設 timer
隊列先執行,會執行 setTimeout
打印 setTimeout2
,將遇到的 setImmediate
放入 check
隊列,輪詢到 check
隊列,由於在棧中執行同步代碼已經在 check
隊列放入了一個 setImmediate
,因此按前後順序執行兩個 setImmediate
,執行第一個 setImmediate
打印 setImmediate1
,將遇到的 setTimeout
放入 timer
隊列,執行第二個 setImmediate
打印 setImmediate2
,再次輪詢到 timer
隊列執行新加入的 setTimeout
,打印 setTimeout1
,產生結果 2
。
案例 5
setImmediate(() => { console.log("setImmediate1"); setTimeout(() => { console.log("setTimeout1"); }, 0); }); setTimeout(() => { process.nextTick(() => console.log("nextTick")); console.log("setTimeout2"); setImmediate(() => { console.log("setImmediate2"); }); }, 0); //結果1 // setImmediate1 // setTimeout2 // setTimeout1 // nextTick // setImmediate2 // 結果2 // setTimeout2 // nextTick // setImmediate1 // setImmediate2 // setTimeout1
這與上面一個案例相似,不一樣的是在 setTimeout
執行的時候產生了一個微任務 nextTick
,咱們只要知道,在 Node 事件輪詢中,在切換隊列時要先去執行微任務隊列,不管是 check
隊列先執行,仍是 timer
隊列先執行,都會很容易分析出上面的兩個結果。
案例 6
const fs = require("fs"); fs.readFile("./.gitignore", "utf8", function() { setTimeout(() => { console.log("timeout"); }, 0); setImmediate(function() { console.log("setImmediate"); }); }); // setImmediate // timeout
上面案例的 setTimeout
和 setImmediate
的執行順序是固定的,前面都是不固定的,這是爲何?
由於前面的不固定是在棧中執行同步代碼時就遇到了 setTimeout
和 setImmediate
,由於沒法判斷 Node 的準備時間,不肯定準備結束定時器是否到時並加入 timer
隊列。
而上面代碼明顯能夠看出 Node 準備結束後會直接執行 poll
隊列進行文件的讀取,在回調中將 setTimeout
和 setImmediate
分別加入 timer
隊列和 check
隊列,Node 隊列的輪詢是有順序的,在 poll
隊列後應該先切換到 check
隊列,而後再從新輪詢到 timer
隊列,因此獲得上面的結果。
案例 7
Promise.resolve().then(() => console.log("Promise")); process.nextTick(() => console.log("nextTick")); // nextTick // Promise
在 Node 中有兩個微任務,Promise
的 then
方法和 process.nextTick
,從上面案例的結果咱們能夠看出,在微任務隊列中 process.nextTick
是優先執行的。
上面內容就是瀏覽器與 Node 在事件輪詢的規則,相信在讀完之後應該已經完全弄清了瀏覽器的事件輪詢機制和 Node 的事件輪詢機制,並深入的體會到了他們之間的相同和不一樣。