摘要:如果執行的準備時間大于了,因為執行同步代碼后,定時器的回調已經被放入隊列,所以會先執行隊列。
JavaScript 是一門單線程語言,之所以說是單線程,是因為在瀏覽器中,如果是多線程,并且兩個線程同時操作了同一個 Dom 元素,那最后的結果會出現問題。所以,JavaScript 是單線程的,但是如果完全由上至下的一行一行執行代碼,假如一個代碼塊執行了很長的時間,后面必須要等待當前執行完畢,這樣的效率是非常低的,所以有了異步的概念,確切的說,JavaScript 的主線程是單線程的,但是也有其他的線程去幫我們實現異步操作,比如定時器線程、事件線程、Ajax 線程。
在瀏覽器中執行 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(已廢棄)。
案例 1
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 是宏任務,晚于同步代碼執行。
案例 2
setTimeout(() => console.log(1), 2000); setTimeout(() => console.log(2), 1000); console.log(3); // 3 // 2 // 1
上面代碼可以看出其實 setTimeout 并不是在同步代碼執行的時候就放入了異步隊列,而是等待時間到達時才會放入異步隊列,所以才會有了上面的結果。
案例 3
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 的事件輪詢機制,并深刻的體會到了他們之間的相同和不同。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/98286.html
摘要:回調函數任務完成的時候,需要執行哪段代碼來處理呢當然是回調函數了。事件處理器和回調函數類似。但是特定的事件處理器在瀏覽器進入異步事件驅動階段時就會針對特定的事件注冊。當事件對象返回到執行線程時,事件處理器也會同時進入執行棧中執行。 Event Loop(事件輪詢)機制是一個經常把人搞暈的東東。我不敢說我完全明白,只是在此談談我的淺見。 事件的處理 瀏覽器是一個事件驅動(event-dr...
摘要:基本原理函數監視的文件描述符分類,分別是和。具體模型如下與模式相比,在完成數據讀取之后,將業務處理過程交由一個線程池來完成,主線程直接返回進行下一次循環操作,效率大大提升。是提供的最高效的網絡模型。 I/O多路復用 IO多路復用就是通過一種機制,一個進程可以監聽多個文件描述符,一個某個描述符就緒(一般是讀就緒或寫就緒),就能夠通知程序進行相應的讀寫操作。select、poll、epol...
摘要:之所以是單線程,取決于它的實際使用,例如不可能同添加一個和刪除這個,所以它只能是單線程的。所以,這個新標準并沒有改變單線程的本質。 原文博客地址:https://finget.github.io/2018/05/21/async/ 異步 什么是單線程,和異步有什么關系 什么是event-loop 是否用過jQuery的Deferred Promise的基本使用和原理 介紹一下asyn...
摘要:阻塞請求結果返回之前,當前線程被掛起。也就是說在異步中,不會對用戶線程產生任何阻塞。當前線程在拿到此次請求結果的過程中,可以做其它事情。事實上,可以只用一個線程處理所有的通道。 準備知識 同步、異步、阻塞、非阻塞 同步和異步說的是服務端消息的通知機制,阻塞和非阻塞說的是客戶端線程的狀態。已客戶端一次網絡請求為例做簡單說明: 同步同步是指一次請求沒有得到結果之前就不返回。 異步請求不會...
閱讀 1132·2021-11-24 09:38
閱讀 3239·2021-11-19 09:56
閱讀 2962·2021-11-18 10:02
閱讀 733·2019-08-29 12:50
閱讀 2572·2019-08-28 18:30
閱讀 866·2019-08-28 18:10
閱讀 3672·2019-08-26 11:36
閱讀 2646·2019-08-23 18:23