本文介紹JavaScript運行機制,這一部分比較抽象,咱們先從一道面試題入手:javascript
console.log(1); setTimeout(function(){ console.log(3); },0); console.log(2);
請問數字打印順序是什麼?
題目的答案是依次輸出1 2 3 css
再看一道題html
本文介紹JavaScript運行機制,這一部分比較抽象,咱們先從一道面試題入手:javascript
console.log(1); setTimeout(function(){ console.log(3); },0); console.log(2);
請問數字打印順序是什麼?
題目的答案是依次輸出1 2 3 css
再看一道題html
<div class="test">測試內容</div>
<script>
$('.test').text('內容改變') alert($('.test').text()) // 頁面加載後首先時alert彈出,alert的內容爲 ‘內容改變’,而dom此時還未發生變化,dom的內容是‘測試內容’ // 而後會一直堵塞,直到咱們點擊確認,alert消失時,頁面內容纔會變成‘內容改變’ // 因此alert prompt confirm等提示框都會跳過頁面渲染先執行 </script>
解決這個問題以前先了解一下它是怎麼致使的,而要了解它須要從 JavaScript 的線程模型提及。前端
JavaScript 引擎是單線程運行的,瀏覽器不管在何時都只且只有一個線程在運行 JavaScript 程序,初衷是爲了減小 DOM 等共享資源的衝突。但是單線程永遠會面臨着一個問題,那就是某一段代碼阻塞會致使後續全部的任務都延遲。又因爲 JavaScript 常常須要操做頁面 DOM 和發送 HTTP 請求,這些 I/O 操做耗時通常都比較長,一旦阻塞,就會給用戶很是差的使用體驗。vue
因而便有了事件循環(event loop)的產生,JavaScript 將一些異步操做或 有I/O 阻塞的操做全都放到一個事件隊列,先順序執行同步 CPU代碼,等到 JavaScript 引擎沒有同步代碼,CPU 空閒下來再讀取事件隊列的異步事件來依次
執行。html5
這些事件包括:java
setTimeout()
設置的異步延遲事件;明白了原理, 再解決這個問題就有了方向,咱們來分析這個問題:node
alert()
是 window 的內置函數,被認爲是同步 CPU代碼;由上述緣由,致使了詭異的 「Alert執行順序問題」。 咱們沒法將頁面渲染變成同步操做,那麼只好把 alert()
變爲異步代碼,從而才能在頁面渲染以後執行。git
解決方法:github
setTimeout()
使用它,能夠延遲執行某些代碼。而對於延遲執行的代碼,JavaScript 引擎老是把這些代碼放到事件隊列裏去,再去檢查是否已經到了執行時間,再適時執行。代碼進入事件隊列,就意味着代碼變成和頁面渲染事件同樣異步了。因爲事件隊列是有序
的,咱們若是用 setTimeout 延時執行,就能夠實如今頁面渲染以後執行 alert 的功能了。
setTimeout 的函數原型爲 setTimeout(code, msec)
,code 是要變爲異步的代碼或函數,msec 是要延時的時間,單位爲毫秒。這裏咱們不須要它延時,只須要它變爲異步就好了,因此能夠將 msec 設置爲 0;
<div class="test">測試內容</div>
<script>
$('.test').text('內容改變') setTimeout(function(){ alert($('.test').text()) },0) // 因爲使用setTimeout操做,使alert被放入到事件隊列 , // 同時 $('.test').text('內容改變')也是在事件隊列 // 因此 dom和alert的內容都是 ‘內容改變’
下面重點介紹下JavaScript線程機制與事件機制
進程是指程序的一次執行,它佔有一片獨有的內存空間,能夠經過windows任務管理器查看進程(以下圖)。同一個時間裏,同一個計算機系統中容許兩個或兩個以上的進程處於並行狀態,這是多進程。好比電腦同時運行微信,QQ,以及各類瀏覽器等。瀏覽器運行是有些是單進程,如firefox和老版IE,有些是多進程,如chrome和新版IE。
有些進程還不止同時幹一件事,好比Word,它能夠同時進行打字、拼寫檢查、打印等事情。在一個進程內部,要同時幹多件事,就須要同時運行多個「子任務」,咱們把進程內的這些「子任務」稱爲線程(Thread)。
線程是指CPU的基本調度單位,是程序執行的一個完整流程,是進程內的一個獨立執行單元。多線程是指在一個進程內, 同時有多個線程運行。瀏覽器運行是多線程。好比用瀏覽器一邊下載,一邊聽歌,一邊看視頻。另外咱們須要知道JavaScript語言的一大特色就是單線程,爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。
因爲每一個進程至少要幹一件事,因此,一個進程至少有一個線程。固然,像Word這種複雜的進程能夠有多個線程,多個線程能夠同時執行,多線程的執行方式和多進程是同樣的,也是由操做系統在多個線程之間快速切換,讓每一個線程都短暫地交替運行,看起來就像同時執行同樣。固然,真正地同時執行多線程須要多核CPU纔可能實現。
應用程序必須運行在某個進程的某個線程上
一個進程中至少有一個運行的線程: 主線程, 進程啓動後自動建立
一個進程中若是同時運行多個線程, 那這個程序是多線程運行的
一個進程的內存空間是共享的,每一個線程均可以使用這些共享內存。
多個進程之間的數據是不能直接共享的
用官方術語描述:
進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)
線程是cpu調度的最小單位(線程是創建在進程的基礎上的一次程序運行單位,一個進程中能夠有多個線程)
進程和線程能夠形象比喻爲:
進程是一個工廠,工廠有它的獨立資源-工廠之間相互獨立-線程是工廠中的工人,多個工人協做完成任務-工廠內有一個或多個工人-工人之間共享空間
線程和進程區分不清,是不少新手都會犯的錯誤,沒有關係。這很正常。先看看下面這個形象的比喻:
- 進程是一個工廠,工廠有它的獨立資源 - 工廠之間相互獨立 - 線程是工廠中的工人,多個工人協做完成任務 - 工廠內有一個或多個工人 - 工人之間共享空間
再完善完善概念:
- 工廠的資源 -> 系統分配的內存(獨立的一塊內存) - 工廠之間的相互獨立 -> 進程之間相互獨立 - 多個工人協做完成任務 -> 多個線程在進程中協做完成任務 - 工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成 - 工人之間共享空間 -> 同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)
而後再鞏固下:
若是是windows電腦中,能夠打開任務管理器,能夠看到有一個後臺進程列表。對,那裏就是查看進程的地方,並且能夠看到每一個進程的內存資源信息以及cpu佔有率。
因此,應該更容易理解了:進程是cpu資源分配的最小單位(系統會給它分配內存)
最後,再用較爲官方的術語描述一遍:
tips
單線程的優勢:順序編程簡單易懂
單線程的缺點:效率低
多線程的優勢:能有效提高CPU的利用率
多線程的缺點:
瀏覽器的內核是指支持瀏覽器運行的最核心的程序,分爲兩個部分的,一是渲染引擎,另外一個是JS引擎。如今JS引擎比較獨立,內核更加傾向於說渲染引擎。
雖然JS運行在瀏覽器中,是單線程的,每一個window一個JS線程,但瀏覽器不是單線程的,例如Webkit或是Gecko引擎,均可能有以下線程:
不少童鞋搞不清,若是js是單線程的,那麼誰去輪詢大的Event loop事件隊列?答案是瀏覽器會有單獨的線程去處理這個隊列。
重點來了,咱們能夠看到,上面提到了這麼多的進程,那麼,對於普通的前端操做來講,最終要的是什麼呢?答案是渲染進程
能夠這樣理解,頁面的渲染,JS的執行,事件的循環,都在這個進程內進行。接下來重點分析這個進程
請牢記,瀏覽器的渲染進程是多線程的(這點若是不理解,請回頭看進程和線程的區分)
終於到了線程這個概念了?,好親切。那麼接下來看看它都包含了哪些線程(列舉一些主要常駐線程):
GUI渲染線程
JS引擎線程
事件觸發線程
注意,因爲JS的單線程關係,因此這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時纔會去執行)
定時觸發器線程
setInterval
與setTimeout
所在線程異步http請求線程
咱們先來看個例子,試問定時器會保證200ms後執行嗎?
document.getElementById('btn').onclick = function () { var start = Date.now() console.log('啓動定時器前...') setTimeout(function () { console.log('定時器執行了', Date.now() - start) }, 200) console.log('啓動定時器後...') // 作一個長時間的工做 for (var i = 0; i < 1000000000; i++) { } }
事實上,通過了625ms後定時器才執行。定時器並不能保證真正定時執行,通常會延遲一丁點,也有可能延遲很長時間(好比上面的例子)
定時器回調函數在主線程執行的, 具體實現方式下文會介紹。
JavaScript語言的一大特色就是單線程,也就是說,同一個時間只能作一件事。即單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就不得不一直等着。
那麼,爲何JavaScript不能有多個線程呢?這樣能提升效率啊。
JavaScript的單線程,與它的用途有關。做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?
因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。
爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。
JavaScript語言的設計者意識到單線程意味着排隊,同一時間只能作一件事,因此將全部任務分紅兩種,一種是同步任務(synchronous),另外一種是異步任務(asynchronous)如各類瀏覽器事件、定時器和Ajax等。
同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;
例如:
console.log("A"); while(true){ } console.log("B"); 請問最後的輸出結果是什麼?
若是你的回答是A,恭喜你答對了,由於這是同步任務,程序由上到下執行,遇到while()死循環,下面語句就沒辦法執行。
異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。
例如:
console.log("A"); setTimeout(function(){ console.log("B"); },0); while(true){} 請問最後的輸出結果是什麼?若是你的答案是A,恭喜你如今對js運行機制已經有個粗淺的認識了!題目中的setTimeout()就是個異步任務。在全部同步任務執行完以前,任何的異步任務是不會執行的
具體來講,異步執行的運行機制以下。(同步執行也是如此,由於它能夠被視爲沒有異步任務的異步執行。)
(1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。
(2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。
(4)主線程不斷重複上面的第三步
ps:
js執行棧又可稱事件循環:(事件循環是指主線程重複從消息隊列中取消息、執行的過程)
js任務隊列又可稱消息隊列(消息隊列是一個先進先出的隊列,它裏面存放着各類消息,消息可簡單理解爲回調函數)
主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)
通常來講,有如下四種會放入異步任務隊列:
1.setTimeout()
設置的異步延遲事件;2.DOM 操做相關如佈局和繪製事件;
3.網絡 I/O 如 AJAX 請求事件;
4.用戶操做事件,如鼠標點擊、鍵盤敲擊。
上圖Event Loop大體描述就是:
下面這個例子很好闡釋事件循環:
setTimeout(function () { console.log('timeout 2222') alert('22222222') }, 2000) setTimeout(function () { console.log('timeout 1111') alert('1111111') }, 1000) setTimeout(function () { console.log('timeout() 00000') }, 0)//當指定的值小於 4 毫秒,則增長到 4ms(4ms 是 HTML5 標準指定的,對於 2010 年及以前的瀏覽器則是 10ms) function fn() { console.log('fn()') } fn() console.log('alert()以前') alert('------') //暫停當前主線程的執行, 同時暫停計時, 點擊肯定後, 恢復程序執行和計時 console.log('alert()以後')
有兩點咱們須要注意下:
總結:異步任務(各類瀏覽器事件、定時器和Ajax等)都是先添加到「任務隊列」(定時器則到達其指定參數時)。當 Stack 棧(JavaScript 主線程)爲空時,就會讀取 Queue 隊列(任務隊列)的第一個任務(隊首),最後執行。
<script> for(var i=0;i<5;i++){ setTimeout(function(){ console.log(i) },1000) } //結果輸出5個5 </script>
for循環一次碰到一個 setTimeout(),並非立刻把setTimeout()拿到異步隊列中,而要等到一秒後,纔將其放到任務隊列裏面,一旦"執行棧"中的全部同步任務執行完畢(即for循環結束,此時i已經爲5),系統就會讀取已經存放"任務隊列"的setTimeout()(有五個),因而答案是輸出5個5。
由於 for 循環會先執行完(同步優先於異步優先於回調),這時五個 setTimeout 的回調所有塞入了事件隊列中,而後 1 秒後一塊兒執行了。
由於 setTimeout 的 console.log(i); 的i是 var 定義的,因此是函數級的做用域,不屬於 for 循環體,屬於 global。等到 for 循環結束,i 已經等於 5 了,這個時候再執行 setTimeout 的五個回調函數(參考上面對事件機制的闡述),裏面的 console.log(i); 的 i 去向上找做用域,只能找到 global下 的 i,即 5。因此輸出都是 5。
解決辦法:人爲給 console.log(i); 創造做用域,保存i的值。
解決方法1:使用let
<script> for(let i=0;i<5;i++){ setTimeout(function(){ console.log(i) },1000) } </script>
let 爲代碼塊的做用域,因此每一次 for 循環,console.log(i); 都引用到 for 代碼塊做用域下的i,由於這樣被引用,因此 for 循環結束後,這些做用域在 setTimeout 未執行前都不會被釋放。
解決方法2:使用當即執行函數
<script> for(var i=0;i<5;i++){ (function(i){ setTimeout(function(){ console.log(i) },1000) })(i) } </script>
裏用到馬上執行函數。這樣 console.log(i); 中的i就保存在每一次循環生成的馬上執行函數中的做用域裏了。
解決方法3:閉包
for(var i = 1;i < 5;i++){ var a = function(){ var j = i; setTimeout(function(){ console.log(j); },1000) } a(); }
以異步AJAX爲例,說明Event loop執行機制
假設存在以下的代碼
$.ajax('http://segmentfault.com', function(resp) { console.log('我是響應:', resp); }); // 其餘代碼 ... ... ...
那麼,
再次:
主線程在發起AJAX請求後,會繼續執行其餘代碼。AJAX線程負責請求segmentfault.com,拿到響應後,它會把響應封裝成一個JavaScript對象,往任務隊列裏添加一個事件:
// 任務隊列中的事件可想象成函數 var message = function () { callbackFn(response); }
其中的callbackFn就是前面代碼中獲得成功響應時的回調函數。主線程在執行完當前循環中的全部代碼後,就會到任務隊列取出這個事件(也就是message函數),並執行它。到此爲止,就完成了工做線程對主線程的通知,回調函數也就獲得了執行。
用圖表示這個過程就是:
咱們上面提到異步任務分爲宏任務和微任務,宏任務隊列能夠有多個,微任務隊列只有一個。
當執行棧中的全部同步任務執行完畢時,是先執行宏任務仍是微任務呢?
一句話歸納上面的流程圖:當某個宏任務隊列的中的任務所有執行完之後,會查看是否有微任務隊列。若是有,先執行微任務隊列中的全部任務,若是沒有,就查看是否有其餘宏任務隊列。
接下來咱們看兩道例子來介紹上面流程:
Promise.resolve().then(()=>{ console.log('Promise1') setTimeout(()=>{ console.log('setTimeout2') },0) }) setTimeout(()=>{ console.log('setTimeout1') Promise.resolve().then(()=>{ console.log('Promise2') }) },0)
最後輸出結果是Promise1,setTimeout1,Promise2,setTimeout2
console.log('----------------- start -----------------'); setTimeout(() => { console.log('setTimeout'); }, 0) new Promise((resolve, reject) =>{ for (var i = 0; i < 5; i++) { console.log(i); } resolve(); // 修改promise實例對象的狀態爲成功的狀態 }).then(() => { console.log('promise實例成功回調執行'); }) console.log('----------------- end -----------------');
將上題調換順序
正如上面所提到,JavaScript是單線程。當一個頁面加載一個複雜運算的 js 文件時,用戶界面可能會短暫地「凍結」,不能再作其餘操做。好比下面這個例子:
<input type="text" placeholder="數值" id="number"> <button id="btn">計算</button> <script type="text/javascript"> // 1 1 2 3 5 8 f(n) = f(n-1) + f(n-2) function fibonacci(n) { return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2) //遞歸調用 } var input = document.getElementById('number') document.getElementById('btn').onclick = function () { var number = input.value var result = fibonacci(number) alert(result) } </script>
很顯然遇到這種頁面堵塞狀況,很影響用戶體驗的,有沒有啥辦法能夠改進這種情形?----Web Worker就應運而生了!
Web Worker 的做用,就是爲 JavaScript 創造多線程環境,容許主線程建立 Worker 線程,將一些任務分配給後者運行。在主線程運行的同時,Worker 線程在後臺運行,二者互不干擾。等到 Worker 線程完成計算任務,再把結果返回給主線程。這樣的好處是,一些計算密集型或高延遲的任務,被 Worker 線程負擔了,主線程(一般負責 UI 交互)就會很流暢,不會被阻塞或拖慢。其原理圖以下:
主線程
var worker = new Worker('work.js');
var input = document.getElementById('number') document.getElementById('btn').onclick = function () { var number = input.value //建立一個Worker對象 var worker = new Worker('worker.js') // 綁定接收消息的監聽 worker.onmessage = function (event) { console.log('主線程接收分線程返回的數據: '+event.data) alert(event.data) } // 向分線程發送消息 worker.postMessage(number) console.log('主線程向分線程發送數據: '+number) } console.log(this) // window
Worker 線程
//worker.js文件 function fibonacci(n) { return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2) //遞歸調用 } console.log(this)//[object DedicatedWorkerGlobalScope] this.onmessage = function (event) { var number = event.data console.log('分線程接收到主線程發送的數據: '+number) //計算 var result = fibonacci(number) postMessage(result) console.log('分線程向主線程返回數據: '+result) // alert(result) alert是window的方法, 在分線程不能調用 // 分線程中的全局對象再也不是window, 因此在分線程中不可能更新界面 }
這樣當分線程在計算時,用戶界面還能夠操做,並且更早拿到計算後數據,響應速度更快了。
若是須要源代碼,請猛戳Web Workers
若是以爲文章對你有些許幫助,歡迎在個人GitHub博客點贊和關注,感激涕零!
Node 中的 Event Loop 和瀏覽器中Event Loop的是徹底不相同的東西。Node.js採用V8做爲js的解析引擎,而I/O處理方面使用了本身設計的libuv,libuv是一個基於事件驅動的跨平臺抽象層,封裝了不一樣操做系統一些底層特性,對外提供統一的API,事件循環機制也是它裏面的實現(下文會詳細介紹)。
Node.js的運行機制以下:
其中libuv引擎中的事件循環分爲 6 個階段,它們會按照順序反覆運行。每當進入某一個階段的時候,都會從對應的回調隊列中取出函數去執行。當隊列爲空或者執行的回調函數數量到達系統設定的閾值,就會進入下一階段。
從上圖中,大體看出node中的事件循環的順序:
外部輸入數據-->輪詢階段(poll)-->檢查階段(check)-->關閉事件回調階段(close callback)-->定時器檢測階段(timer)-->I/O事件回調階段(I/O callbacks)-->閒置階段(idle, prepare)-->輪詢階段(按照該順序反覆運行)...
注意:上面六個階段都不包括 process.nextTick()(下文會介紹)
接下去咱們詳細介紹timers
、poll
、check
這3個階段,由於平常開發中的絕大部分異步任務都是在這3個階段處理的。
timers 階段會執行 setTimeout 和 setInterval 回調,而且是由 poll 階段控制的。 一樣,在 Node 中定時器指定的時間也不是準確時間,只能是儘快執行。
poll 是一個相當重要的階段,這一階段中,系統會作兩件事情
1.回到 timer 階段執行回調
2.執行 I/O 回調
而且在進入該階段時若是沒有設定了 timer 的話,會發生如下兩件事情
固然設定了 timer 的話且 poll 隊列爲空,則會判斷是否有 timer 超時,若是有的話會回到 timer 階段執行回調。
setImmediate()的回調會被加入check隊列中,從event loop的階段圖能夠知道,check階段的執行順序在poll階段以後。 咱們先來看個例子:
console.log('start') setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(() => { console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) Promise.resolve().then(function() { console.log('promise3') }) console.log('end') //start=>end=>promise3=>timer1=>timer2=>promise1=>promise2 複製代碼
Node端事件循環中的異步隊列也是這兩種:macro(宏任務)隊列和 micro(微任務)隊列。
兩者很是類似,區別主要在於調用時機不一樣。
setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); }); 複製代碼
但當兩者在異步i/o callback內部調用時,老是先執行setImmediate,再執行setTimeout
const fs = require('fs') fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0) setImmediate(() => { console.log('immediate') }) }) // immediate // timeout 複製代碼
在上述代碼中,setImmediate 永遠先執行。由於兩個代碼寫在 IO 回調中,IO 回調是在 poll 階段執行,當回調執行完畢後隊列爲空,發現存在 setImmediate 回調,因此就直接跳轉到 check 階段去執行回調了。
這個函數實際上是獨立於 Event Loop 以外的,它有一個本身的隊列,當每一個階段完成後,若是存在 nextTick 隊列,就會清空隊列中的全部回調函數,而且優先於其餘 microtask 執行。
setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') process.nextTick(() => { console.log('nextTick') }) }) }) }) // nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1 複製代碼
瀏覽器環境下,microtask的任務隊列是每一個macrotask執行完以後執行。而在Node.js中,microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行microtask隊列的任務。
接下咱們經過一個例子來講明二者區別:
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) 複製代碼
瀏覽器端運行結果:timer1=>promise1=>timer2=>promise2
瀏覽器端的處理過程以下:
Node端運行結果分兩種狀況:
timer1=>promise1=>timer2=>promise2
timer1=>promise1=>timer2=>promise2
timer1=>timer2=>promise1=>promise2
(下文過程解釋基於這種狀況下)1.全局腳本(main())執行,將2個timer依次放入timer隊列,main()執行完畢,調用棧空閒,任務隊列開始執行;
2.首先進入timers階段,執行timer1的回調函數,打印timer1,並將promise1.then回調放入microtask隊列,一樣的步驟執行timer2,打印timer2;
3.至此,timer階段執行結束,event loop進入下一個階段以前,執行microtask隊列的全部任務,依次打印promise一、promise2
Node端的處理過程以下:
瀏覽器和Node 環境下,microtask 任務隊列的執行時機不一樣
文章於2019.1.16晚,對最後一個例子在node運行結果,從新修改!再次特別感謝zy445566的精彩點評,因爲node版本更新到11,Event Loop運行原理髮生了變化,一旦執行一個階段裏的一個宏任務(setTimeout,setInterval和setImmediate)就馬上執行微任務隊列,這點就跟瀏覽器端一致。
看法有限,若有描述不當之處,請幫忙及時指出,若有錯誤,會及時修正。
----------超長文+多圖預警,須要花費很多時間。----------
若是看完本文後,還對進程線程傻傻分不清,不清楚瀏覽器多進程、瀏覽器內核多線程、JS單線程、JS運行機制的區別。那麼請回復我,必定是我寫的還不夠清晰,我來改。。。
----------正文開始----------
最近發現有很多介紹JS單線程運行機制的文章,可是發現不少都僅僅是介紹某一部分的知識,並且各個地方的說法還不統一,容易形成困惑。
所以準備梳理這塊知識點,結合已有的認知,基於網上的大量參考資料,
從瀏覽器多進程到JS單線程,將JS引擎的運行機制系統的梳理一遍。
展示形式:因爲是屬於系統梳理型,就沒有由淺入深了,而是從頭至尾的梳理知識體系,
重點是將關鍵節點的知識點串聯起來,而不是僅僅剖析某一部分知識。
內容是:從瀏覽器進程,再到瀏覽器內核運行,再到JS引擎單線程,再到JS事件循環機制,從頭至尾系統的梳理一遍,擺脫碎片化,造成一個知識體系
目標是:看完這篇文章後,對瀏覽器多進程,JS單線程,JS事件循環機制這些都能有必定理解,
有一個知識體系骨架,而不是似懂非懂的感受。
另外,本文適合有必定經驗的前端人員,新手請規避,避免受到過多的概念衝擊。能夠先存起來,有了必定理解後再看,也能夠分紅多批次觀看,避免過分疲勞。
瀏覽器是多進程的
梳理瀏覽器內核中線程之間的關係
簡單梳理下瀏覽器渲染流程
從Event Loop談JS的運行機制
線程和進程區分不清,是不少新手都會犯的錯誤,沒有關係。這很正常。先看看下面這個形象的比喻:
- 進程是一個工廠,工廠有它的獨立資源 - 工廠之間相互獨立 - 線程是工廠中的工人,多個工人協做完成任務 - 工廠內有一個或多個工人 - 工人之間共享空間
再完善完善概念:
- 工廠的資源 -> 系統分配的內存(獨立的一塊內存) - 工廠之間的相互獨立 -> 進程之間相互獨立 - 多個工人協做完成任務 -> 多個線程在進程中協做完成任務 - 工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成 - 工人之間共享空間 -> 同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)
而後再鞏固下:
若是是windows電腦中,能夠打開任務管理器,能夠看到有一個後臺進程列表。對,那裏就是查看進程的地方,並且能夠看到每一個進程的內存資源信息以及cpu佔有率。
因此,應該更容易理解了:進程是cpu資源分配的最小單位(系統會給它分配內存)
最後,再用較爲官方的術語描述一遍:
tips
理解了進程與線程了區別後,接下來對瀏覽器進行必定程度上的認識:(先看下簡化理解)
關於以上幾點的驗證,請再第一張圖:
圖中打開了Chrome
瀏覽器的多個標籤頁,而後能夠在Chrome的任務管理器
中看到有多個進程(分別是每個Tab頁面有一個獨立的進程,以及一個主進程)。
感興趣的能夠自行嘗試下,若是再多打開一個Tab頁,進程正常會+1以上
注意:在這裏瀏覽器應該也有本身的優化機制,有時候打開多個tab頁後,能夠在Chrome任務管理器中看到,有些進程被合併了
(因此每個Tab標籤對應一個進程並不必定是絕對的)
知道了瀏覽器是多進程後,再來看看它到底包含哪些進程:(爲了簡化理解,僅列舉主要進程)
Browser進程:瀏覽器的主進程(負責協調、主控),只有一個。做用有
瀏覽器渲染進程(瀏覽器內核)(Renderer進程,內部是多線程的):默認每一個Tab頁面一個進程,互不影響。主要做用爲
強化記憶:在瀏覽器中打開一個網頁至關於新起了一個進程(進程內有本身的多線程)
固然,瀏覽器有時會將多個進程合併(譬如打開多個空白標籤頁後,會發現多個空白標籤頁被合併成了一個進程),如圖
另外,能夠經過Chrome的更多工具 -> 任務管理器
自行驗證
相比於單進程瀏覽器,多進程有以下優勢:
簡單點理解:若是瀏覽器是單進程,那麼某個Tab頁崩潰了,就影響了整個瀏覽器,體驗有多差;同理若是是單進程,插件崩潰了也會影響整個瀏覽器;並且多進程還有其它的諸多優點。。。
固然,內存等資源消耗也會更大,有點空間換時間的意思。
重點來了,咱們能夠看到,上面提到了這麼多的進程,那麼,對於普通的前端操做來講,最終要的是什麼呢?答案是渲染進程
能夠這樣理解,頁面的渲染,JS的執行,事件的循環,都在這個進程內進行。接下來重點分析這個進程
請牢記,瀏覽器的渲染進程是多線程的(這點若是不理解,請回頭看進程和線程的區分)
終於到了線程這個概念了?,好親切。那麼接下來看看它都包含了哪些線程(列舉一些主要常駐線程):
GUI渲染線程
JS引擎線程
事件觸發線程
注意,因爲JS的單線程關係,因此這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時纔會去執行)
定時觸發器線程
setInterval
與setTimeout
所在線程異步http請求線程
看到這裏,若是以爲累了,能夠先休息下,這些概念須要被消化,畢竟後續將提到的事件循環機制就是基於事件觸發線程
的,因此若是僅僅是看某個碎片化知識,
可能會有一種似懂非懂的感受。要完成的梳理一遍才能快速沉澱,不易遺忘。放張圖鞏固下吧:
再說一點,爲何JS引擎是單線程的?額,這個問題其實應該沒有標準答案,譬如,可能僅僅是由於因爲多線程的複雜性,譬如多線程操做通常要加鎖,所以最初設計時選擇了單線程。。。
看到這裏,首先,應該對瀏覽器內的進程和線程都有必定理解了,那麼接下來,再談談瀏覽器的Browser進程(控制進程)是如何和內核通訊的,
這點也理解後,就能夠將這部分的知識串聯起來,從頭至尾有一個完整的概念。
若是本身打開任務管理器,而後打開一個瀏覽器,就能夠看到:任務管理器中出現了兩個進程(一個是主控進程,一個則是打開Tab頁的渲染進程),
而後在這前提下,看下整個的過程:(簡化了不少)
Renderer進程的Renderer接口收到消息,簡單解釋後,交給渲染線程,而後開始渲染
這裏繪一張簡單的圖:(很簡化)
看完這一整套流程,應該對瀏覽器的運做有了必定理解了,這樣有了知識架構的基礎後,後續就方便往上填充內容。
這塊再往深處講的話就涉及到瀏覽器內核源碼解析了,不屬於本文範圍。
若是這一塊要深挖,建議去讀一些瀏覽器內核源碼解析文章,或者能夠先看看參考下來源中的第一篇文章,寫的不錯
到了這裏,已經對瀏覽器的運行有了一個總體的概念,接下來,先簡單梳理一些概念
因爲JavaScript是可操縱DOM的,若是在修改這些元素屬性同時渲染界面(即JS線程和UI線程同時運行),那麼渲染線程先後得到的元素數據就可能不一致了。
所以爲了防止渲染出現不可預期的結果,瀏覽器設置GUI渲染線程與JS引擎爲互斥的關係,當JS引擎執行時GUI線程會被掛起,
GUI更新則會被保存在一個隊列中等到JS引擎線程空閒時當即被執行。
從上述的互斥關係,能夠推導出,JS若是執行時間過長就會阻塞頁面。
譬如,假設JS引擎正在進行巨量的計算,此時就算GUI有更新,也會被保存到隊列中,等待JS引擎空閒後執行。
而後,因爲巨量計算,因此JS引擎極可能好久好久後才能空閒,天然會感受到巨卡無比。
因此,要儘可能避免JS執行時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞的感受。
前文中有提到JS引擎是單線程的,並且JS執行時間過長會阻塞頁面,那麼JS就真的對cpu密集型計算無能爲力麼?
因此,後來HTML5中支持了Web Worker
。
MDN的官方解釋是:
Web Worker爲Web內容在後臺線程中運行腳本提供了一種簡單的方法。線程能夠執行任務而不干擾用戶界面
一個worker是使用一個構造函數建立的一個對象(e.g. Worker()) 運行一個命名的JavaScript文件
這個文件包含將在工做線程中運行的代碼; workers 運行在另外一個全局上下文中,不一樣於當前的window 所以,使用 window快捷方式獲取當前全局的範圍 (而不是self) 在一個 Worker 內將返回錯誤
這樣理解下:
因此,若是有很是耗時的工做,請單獨開一個Worker線程,這樣裏面無論如何翻天覆地都不會影響JS引擎主線程,
只待計算出結果後,將結果通訊給主線程便可,perfect!
並且注意下,JS引擎是單線程的,這一點的本質仍然未改變,Worker能夠理解是瀏覽器給JS引擎開的外掛,專門用來解決那些大量計算問題。
其它,關於Worker的詳解就不是本文的範疇了,所以再也不贅述。
既然都到了這裏,就再提一下SharedWorker
(避免後續將這兩個概念搞混)
WebWorker只屬於某個頁面,不會和其餘頁面的Render進程(瀏覽器內核進程)共享
SharedWorker是瀏覽器全部頁面共享的,不能採用與Worker一樣的方式實現,由於它不隸屬於某個Render進程,能夠爲多個Render進程共享使用
看到這裏,應該就很容易明白了,本質上就是進程和線程的區別。SharedWorker由獨立的進程管理,WebWorker只是屬於render進程下的一個線程
原本是直接計劃開始談JS運行機制的,但想了想,既然上述都一直在談瀏覽器,直接跳到JS可能再突兀,所以,中間再補充下瀏覽器的渲染流程(簡單版本)
爲了簡化理解,前期工做直接省略成:(要展開的或徹底能夠寫另外一篇超長文)
- 瀏覽器輸入url,瀏覽器主進程接管,開一個下載線程, 而後進行 http請求(略去DNS查詢,IP尋址等等操做),而後等待響應,獲取內容, 隨後將內容經過RendererHost接口轉交給Renderer進程 - 瀏覽器渲染流程開始
瀏覽器器內核拿到內容後,渲染大概能夠劃分紅如下幾個步驟:
全部詳細步驟都已經略去,渲染完畢後就是load
事件了,以後就是本身的JS邏輯處理了
既然略去了一些詳細的步驟,那麼就提一些可能須要注意的細節把。
這裏重繪參考來源中的一張圖:(參考來源第一篇)
上面提到,渲染完畢後會觸發load
事件,那麼你能分清楚load
事件與DOMContentLoaded
事件的前後麼?
很簡單,知道它們的定義就能夠了:
(譬如若是有async加載的腳本就不必定完成)
(渲染完畢了)
因此,順序是:DOMContentLoaded -> load
這裏說的是頭部引入css的狀況
首先,咱們都知道:css是由單獨的下載線程異步下載的。
而後再說下幾個現象:
這可能也是瀏覽器的一種優化機制。
由於你加載css的時候,可能會修改下面DOM節點的樣式,
若是css加載不阻塞render樹渲染的話,那麼當css加載完以後,
render樹可能又得從新重繪或者回流了,這就形成了一些沒有必要的損耗。
因此乾脆就先把DOM樹的結構先解析完,把能夠作的工做作完,而後等你css加載完以後,
在根據最終的樣式來渲染render樹,這種作法性能方面確實會比較好一點。
渲染步驟中就提到了composite
概念。
能夠簡單的這樣理解,瀏覽器渲染的圖層通常包含兩大類:普通圖層
以及複合圖層
首先,普通文檔流內能夠理解爲一個複合圖層(這裏稱爲默認複合層
,裏面無論添加多少元素,其實都是在同一個複合圖層中)
其次,absolute佈局(fixed也同樣),雖然能夠脫離普通文檔流,但它仍然屬於默認複合層
。
而後,能夠經過硬件加速
的方式,聲明一個新的複合圖層
,它會單獨分配資源
(固然也會脫離普通文檔流,這樣一來,無論這個複合圖層中怎麼變化,也不會影響默認複合層
裏的迴流重繪)
能夠簡單理解下:GPU中,各個複合圖層是單獨繪製的,因此互不影響,這也是爲何某些場景硬件加速效果一級棒
能夠Chrome源碼調試 -> More Tools -> Rendering -> Layer borders
中看到,黃色的就是複合圖層信息
以下圖。能夠驗證上述的說法
如何變成複合圖層(硬件加速)
將該元素變成一個複合圖層,就是傳說中的硬件加速技術
translate3d
、translateZ
opacity
屬性/過渡動畫(須要動畫執行的過程當中纔會建立合成層,動畫沒有開始或結束後元素還會回到以前的狀態)will-chang
屬性(這個比較偏僻),通常配合opacity與translate使用(並且經測試,除了上述能夠引起硬件加速的屬性外,其它屬性並不會變成複合層),做用是提早告訴瀏覽器要變化,這樣瀏覽器會開始作一些優化工做(這個最好用完後就釋放)
<video><iframe><canvas><webgl>
等元素absolute和硬件加速的區別
能夠看到,absolute雖然能夠脫離普通文檔流,可是沒法脫離默認複合層。
因此,就算absolute中信息改變時不會改變普通文檔流中render樹,
可是,瀏覽器最終繪製時,是整個複合層繪製的,因此absolute中信息的改變,仍然會影響整個複合層的繪製。
(瀏覽器會重繪它,若是複合層中內容多,absolute帶來的繪製信息變化過大,資源消耗是很是嚴重的)
而硬件加速直接就是在另外一個複合層了(另起爐竈),因此它的信息改變不會影響默認複合層
(固然了,內部確定會影響屬於本身的複合層),僅僅是引起最後的合成(輸出視圖)
複合圖層的做用?
通常一個元素開啓硬件加速後會變成複合圖層,能夠獨立於普通文檔流中,改動後能夠避免整個頁面重繪,提高性能
可是儘可能不要大量使用複合圖層,不然因爲資源消耗過分,頁面反而會變的更卡
硬件加速時請使用index
使用硬件加速時,儘量的使用index,防止瀏覽器默認給後續的元素建立複合層渲染
具體的原理時這樣的:
**webkit CSS3中,若是這個元素添加了硬件加速,而且index層級比較低,
那麼在這個元素的後面其它元素(層級比這個元素高的,或者相同的,而且releative或absolute屬性相同的),
會默認變爲複合層渲染,若是處理不當會極大的影響性能**
簡單點理解,其實能夠認爲是一個隱式合成的概念:若是a是一個複合圖層,並且b在a上面,那麼b也會被隱式轉爲一個複合圖層,這點須要特別注意
另外,這個問題能夠在這個地址看到重現(原做者分析的挺到位的,直接上連接):
到此時,已是屬於瀏覽器頁面初次渲染完畢後的事情,JS引擎的一些運行機制分析。
注意,這裏不談可執行上下文
,VO
,scop chain
等概念(這些徹底能夠整理成另外一篇文章了),這裏主要是結合Event Loop
來談JS代碼是如何執行的。
讀這部分的前提是已經知道了JS引擎是單線程,並且這裏會用到上文中的幾個概念:(若是不是很理解,能夠回頭溫習)
而後再理解一個概念:
執行棧
任務隊列
,只要異步任務有了運行結果,就在任務隊列
之中放置一個事件。執行棧
中的全部同步任務執行完畢(此時JS引擎空閒),系統就會讀取任務隊列
,將可運行的異步任務添加到可執行棧中,開始執行。看圖:
看到這裏,應該就能夠理解了:爲何有時候setTimeout推入的事件不能準時執行?由於可能在它推入到事件列表時,主線程還不空閒,正在執行其它代碼,
因此天然有偏差。
這裏就直接引用一張圖片來協助理解:(參考自Philip Roberts的演講《Help, I'm stuck in an event-loop》)
上圖大體描述就是:
棧中的代碼調用某些api時,它們會在事件隊列中添加各類事件(當知足觸發條件後,如ajax請求完畢)
上述事件循環機制的核心是:JS引擎線程和事件觸發線程
但事件上,裏面還有一些隱藏細節,譬如調用setTimeout
後,是如何等待特定時間後才添加到事件隊列中的?
是JS引擎檢測的麼?固然不是了。它是由定時器線程控制(由於JS引擎本身都忙不過來,根本無暇分身)
爲何要單獨的定時器線程?由於JavaScript引擎是單線程的, 若是處於阻塞線程狀態就會影響記計時的準確,所以頗有必要單獨開一個線程用來計時。
何時會用到定時器線程?當使用setTimeout
或setInterval
時,它須要定時器線程計時,計時完成後就會將特定的事件推入事件隊列中。
譬如:
setTimeout(function(){ console.log('hello!'); }, 1000);
這段代碼的做用是當1000
毫秒計時完畢後(由定時器線程計時),將回調函數推入事件隊列中,等待主線程執行
setTimeout(function(){ console.log('hello!'); }, 0); console.log('begin');
這段代碼的效果是最快的時間內將回調函數推入事件隊列中,等待主線程執行
注意:
begin
後hello!
(不過也有一說是不一樣瀏覽器有不一樣的最小時間設定)
begin
(由於只有可執行棧內空了後纔會主動讀取事件隊列)用setTimeout模擬按期計時和直接用setInterval是有區別的。
由於每次setTimeout計時到後就會去執行,而後執行一段時間後纔會繼續setTimeout,中間就多了偏差
(偏差多少與代碼執行時間有關)
而setInterval則是每次都精確的隔一段時間推入一個事件
(可是,事件的實際執行時間不必定就準確,還有多是這個事件還沒執行完畢,下一個事件就來了)
並且setInterval有一些比較致命的問題就是:
就會致使定時器代碼連續運行好幾回,而之間沒有間隔。
就算正常間隔執行,多個setInterval的代碼執行時間可能會比預期小(由於代碼執行須要必定時間)
它會把setInterval的回調函數放在隊列中,等瀏覽器窗口再次打開時,一瞬間所有執行時
因此,鑑於這麼多但問題,目前通常認爲的最佳方案是:用setTimeout模擬setInterval,或者特殊場合直接用requestAnimationFrame
補充:JS高程中有提到,JS引擎會對setInterval進行優化,若是當前事件隊列中有setInterval的回調,不會重複添加。不過,仍然是有不少問題。。。
這段參考了參考來源中的第2篇文章(英文版的),(加了下本身的理解從新描述了下),
強烈推薦有英文基礎的同窗直接觀看原文,做者描述的很清晰,示例也很不錯,以下:
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
上文中將JS事件循環機制梳理了一遍,在ES5的狀況是夠用了,可是在ES6盛行的如今,仍然會遇到一些問題,譬以下面這題:
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end');
嗯哼,它的正確執行順序是這樣子的:
script start script end promise1 promise2 setTimeout
爲何呢?由於Promise裏有了一個一個新的概念:microtask
或者,進一步,JS中分爲兩種任務類型:macrotask
和microtask
,在ECMAScript中,microtask稱爲jobs
,macrotask可稱爲task
它們的定義?區別?簡單點能夠按以下理解:
macrotask(又稱之爲宏任務),能夠理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)
(`task->渲染->task->...`)
microtask(又稱爲微任務),能夠理解是在當前 task 執行結束後當即執行的任務
分別很麼樣的場景會造成macrotask和microtask呢?
__補充:在node環境下,process.nextTick的優先級高於Promise__,也就是能夠簡單理解爲:在宏任務結束後會先執行微任務隊列中的nextTickQueue部分,而後纔會執行微任務中的Promise部分。
參考:https://segmentfault.com/q/1010000011914016
再根據線程來理解下:
(這點由本身理解+推測得出,由於它是在主線程下無縫執行的)
因此,總結下運行機制:
如圖:
另外,請注意下Promise
的polyfill
與官方版本的區別:
注意,有一些瀏覽器執行結果不同(由於它們可能把microtask當成macrotask來執行了),
可是爲了簡單,這裏不描述一些不標準的瀏覽器下的場景(但記住,有些瀏覽器可能並不標準)
20180126補充:使用MutationObserver實現microtask
MutationObserver能夠用來實現microtask
(它屬於microtask,優先級小於Promise,
通常是Promise不支持時纔會這樣作)
它是HTML5中的新特性,做用是:監聽一個DOM變更,
當DOM對象樹發生任何變更時,Mutation Observer會獲得通知
像之前的Vue源碼中就是利用它來模擬nextTick的,
具體原理是,建立一個TextNode並監聽內容變化,
而後要nextTick的時候去改一下這個節點的文本內容,
以下:(Vue的源碼,未修改)
var counter = 1 var observer = new MutationObserver(nextTickHandler) var textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) }
不過,如今的Vue(2.5+)的nextTick實現移除了MutationObserver的方式(聽說是兼容性緣由),
取而代之的是使用MessageChannel
(固然,默認狀況仍然是Promise,不支持才兼容的)。
MessageChannel屬於宏任務,優先級是:MessageChannel->setTimeout
,
因此Vue(2.5+)內部的nextTick與2.4及以前的實現是不同的,須要注意下。
這裏不展開,能夠看下http://www.javashuo.com/article/p-uvlubpli-gp.html
看到這裏,不知道對JS的運行機制是否是更加理解了,從頭至尾梳理,而不是就某一個碎片化知識應該是會更清晰的吧?
同時,也應該注意到了JS根本就沒有想象的那麼簡單,前端的知識也是無窮無盡,層出不窮的概念、N多易忘的知識點、各式各樣的框架、
底層原理方面也是能夠無限的往下深挖,而後你就會發現,你知道的太少了。。。
另外,本文也打算先告一段落,其它的,如JS詞法解析,可執行上下文以及VO等概念就不繼續在本文中寫了,後續能夠考慮另開新的文章。
最後,喜歡的話,就請給個贊吧!
初次發佈2018.01.21
於我我的博客上面
http://www.dailichun.com/2018/01/21/js_singlethread_eventloop.html
Event Loop
即事件循環,是指瀏覽器或Node
的一種解決javaScript
單線程運行時不會阻塞的一種機制,也就是咱們常用異步的原理。
是要增長本身技術的深度,也就是懂得JavaScript
的運行機制。
如今在前端領域各類技術層出不窮,掌握底層原理,可讓本身以不變,應萬變。
應對各大互聯網公司的面試,懂其原理,題目任其發揮。
堆是一種數據結構,是利用徹底二叉樹維護的一組數據,堆分爲兩種,一種爲最大堆,一種爲最小堆,將根節點最大的堆叫作最大堆或大根堆,根節點最小的堆叫作最小堆或小根堆。
堆是線性數據結構,至關於一維數組,有惟一後繼。
如最大堆
棧在計算機科學中是限定僅在表尾進行插入或刪除操做的線性表。 棧是一種數據結構,它按照後進先出的原則存儲數據,先進入的數據被壓入棧底,最後的數據在棧頂,須要讀數據的時候從棧頂開始彈出數據。
棧是隻能在某一端插入和刪除的特殊線性表。
特殊之處在於它只容許在表的前端(front
)進行刪除操做,而在表的後端(rear
)進行插入操做,和棧同樣,隊列是一種操做受限制的線性表。
進行插入操做的端稱爲隊尾,進行刪除操做的端稱爲隊頭。 隊列中沒有元素時,稱爲空隊列。
隊列的數據元素又稱爲隊列元素。在隊列中插入一個隊列元素稱爲入隊,從隊列中刪除一個隊列元素稱爲出隊。由於隊列只容許在一端插入,在另外一端刪除,因此只有最先進入隊列的元素才能最早從隊列中刪除,故隊列又稱爲先進先出(FIFO—first in first out
)
在JavaScript
中,任務被分爲兩種,一種宏任務(MacroTask
)也叫Task
,一種叫微任務(MicroTask
)。
script
所有代碼、setTimeout
、setInterval
、setImmediate
(瀏覽器暫時不支持,只有IE10支持,具體可見MDN
)、I/O
、UI Rendering
。Process.nextTick(Node獨有)
、Promise
、Object.observe(廢棄)
、MutationObserver
(具體使用方式查看這裏)Javascript
有一個 main thread
主線程和 call-stack
調用棧(執行棧),全部的任務都會被放到調用棧等待主線程執行。
JS調用棧採用的是後進先出的規則,當函數執行的時候,會被添加到棧的頂部,當執行棧執行完成後,就會從棧頂移出,直到棧內被清空。
Javascript
單線程任務被分爲同步任務和異步任務,同步任務會在調用棧中按照順序等待主線程依次執行,異步任務會在異步任務有告終果後,將註冊的回調函數放入任務隊列中等待主線程空閒的時候(調用棧被清空),被讀取到棧內等待主線程的執行。
任務隊列
Task Queue
,即隊列,是一種先進先出的一種數據結構。
null
,則執行跳轉到微任務(MicroTask
)的執行步驟。microtask
執行不爲空時:選擇一個最早進入的microtask
隊列的microtask
,將事件循環的microtask
設置爲已選擇的microtask
,運行microtask
,將已經執行完成的microtask
爲null
,移出microtask
中的microtask
。上述可能不太好理解,下圖是我作的一張圖片。
執行棧在執行完同步任務後,查看執行棧是否爲空,若是執行棧爲空,就會去檢查微任務(microTask
)隊列是否爲空,若是爲空的話,就執行Task
(宏任務),不然就一次性執行完全部微任務。
每次單個宏任務執行完畢後,檢查微任務(microTask
)隊列是否爲空,若是不爲空的話,會按照先入先出的規則所有執行完微任務(microTask
)後,設置微任務(microTask
)隊列爲null
,而後再執行宏任務,如此循環。
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end'); 複製代碼
首先咱們劃分幾個分類:
Tasks:run script、 setTimeout callback
Microtasks:Promise then JS stack: script Log: script start、script end。 複製代碼
執行同步代碼,將宏任務(Tasks
)和微任務(Microtasks
)劃分到各自隊列中。
Tasks:run script、 setTimeout callback
Microtasks:Promise2 then
JS stack: Promise2 callback
Log: script start、script end、promise一、promise2
複製代碼
執行宏任務後,檢測到微任務(Microtasks
)隊列中不爲空,執行Promise1
,執行完成Promise1
後,調用Promise2.then
,放入微任務(Microtasks
)隊列中,再執行Promise2.then
。
Tasks:setTimeout callback
Microtasks:
JS stack: setTimeout callback
Log: script start、script end、promise一、promise二、setTimeout
複製代碼
當微任務(Microtasks
)隊列中爲空時,執行宏任務(Tasks
),執行setTimeout callback
,打印日誌。
Tasks:setTimeout callback
Microtasks:
JS stack:
Log: script start、script end、promise一、promise二、setTimeout
複製代碼
清空Tasks隊列和JS stack
。
以上執行幀動畫能夠查看Tasks, microtasks, queues and schedules
或許這張圖也更好理解些。
console.log('script start') async function async1() { await async2() console.log('async1 end') } async function async2() { console.log('async2 end') } async1() setTimeout(function() { console.log('setTimeout') }, 0) new Promise(resolve => { console.log('Promise') resolve() }) .then(function() { console.log('promise1') }) .then(function() { console.log('promise2') }) console.log('script end') 複製代碼
這裏須要先理解async/await
。
async/await
在底層轉換成了 promise
和 then
回調函數。
也就是說,這是 promise
的語法糖。
每次咱們使用 await
, 解釋器都建立一個 promise
對象,而後把剩下的 async
函數中的操做放到 then
回調函數中。async/await
的實現,離不開 Promise
。從字面意思來理解,async
是「異步」的簡寫,而 await
是 async wait
的簡寫能夠認爲是等待異步方法執行完成。
promise1
和promise2
,再執行async1
。async1
再執行promise1
和promise2
。主要緣由是由於在谷歌(金絲雀)73版本中更改了規範,以下圖所示:
RESOLVE(thenable)
和之間的區別Promise.resolve(thenable)
。await
的值被包裹在一個 Promise
中。而後,處理程序附加到這個包裝的 Promise
,以便在 Promise
變爲 fulfilled
後恢復該函數,而且暫停執行異步函數,一旦 promise
變爲 fulfilled
,恢復異步函數的執行。await
引擎必須建立兩個額外的 Promise(即便右側已是一個 Promise
)而且它須要至少三個 microtask
隊列 ticks
(tick
爲系統的相對時間單位,也被稱爲系統的時基,來源於定時器的週期性中斷(輸出脈衝),一次中斷表示一個tick
,也被稱作一個「時鐘滴答」、時標。)。async function f() { await p console.log('ok') } 複製代碼
簡化理解爲:
function f() { return RESOLVE(p).then(() => { console.log('ok') }) } 複製代碼
RESOLVE(p)
對於 p
爲 promise
直接返回 p
的話,那麼 p
的 then
方法就會被立刻調用,其回調就當即進入 job
隊列。RESOLVE(p)
嚴格按照標準,應該是產生一個新的 promise
,儘管該 promise
肯定會 resolve
爲 p
,但這個過程自己是異步的,也就是如今進入 job
隊列的是新 promise
的 resolve
過程,因此該 promise
的 then
不會被當即調用,而要等到當前 job
隊列執行到前述 resolve
過程纔會被調用,而後其回調(也就是繼續 await
以後的語句)才加入 job
隊列,因此時序上就晚了。PromiseResolve
的調用來更改await
的語義,以減小在公共awaitPromise
狀況下的轉換次數。await
的值已是一個 Promise
,那麼這種優化避免了再次建立 Promise
包裝器,在這種狀況下,咱們從最少三個 microtick
到只有一個 microtick
。73如下版本
script start
,調用async1()
時,返回一個Promise
,因此打印出來async2 end
。await
,會新產生一個promise
,但這個過程自己是異步的,因此該await
後面不會當即調用。Promise
和script end
,將then
函數放入微任務隊列中等待執行。null
,而後按照先入先出規則,依次執行。promise1
,此時then
的回調函數返回undefinde
,此時又有then
的鏈式調用,又放入微任務隊列中,再次打印promise2
。await
的位置執行返回的 Promise
的 resolve
函數,這又會把 resolve
丟到微任務隊列中,打印async1 end
。setTimeout
。谷歌(金絲雀73版本)
await
的值已是一個 Promise
,那麼這種優化避免了再次建立 Promise
包裝器,在這種狀況下,咱們從最少三個 microtick
到只有一個 microtick
。await
創造 throwaway Promise
- 在絕大部分時間。promise
指向了同一個 Promise
,因此這個步驟什麼也不須要作。而後引擎繼續像之前同樣,建立 throwaway Promise
,安排 PromiseReactionJob
在 microtask
隊列的下一個 tick
上恢復異步函數,暫停執行該函數,而後返回給調用者。具體詳情查看(這裏)。
Node
中的Event Loop
是基於libuv
實現的,而libuv
是 Node
的新跨平臺抽象層,libuv使用異步,事件驅動的編程方式,核心是提供i/o
的事件循環和異步回調。libuv的API
包含有時間,非阻塞的網絡,異步文件操做,子進程等等。 Event Loop
就是在libuv
中實現的。
Node
的Event loop
一共分爲6個階段,每一個細節具體以下:timers
: 執行setTimeout
和setInterval
中到期的callback
。pending callback
: 上一輪循環中少數的callback
會放在這一階段執行。idle, prepare
: 僅在內部使用。poll
: 最重要的階段,執行pending callback
,在適當的狀況下回阻塞在這個階段。check
: 執行setImmediate
(setImmediate()
是將事件插入到事件隊列尾部,主線程和事件隊列的函數執行完成以後當即執行setImmediate
指定的回調函數)的callback
。close callbacks
: 執行close
事件的callback
,例如socket.on('close'[,fn])
或者http.server.on('close, fn)
。具體細節以下:
執行setTimeout
和setInterval
中到期的callback
,執行這二者回調須要設置一個毫秒數,理論上來講,應該是時間一到就當即執行callback回調,可是因爲system
的調度可能會延時,達不到預期時間。
如下是官網文檔解釋的例子:
const fs = require('fs'); function someAsyncOperation(callback) { // Assume this takes 95ms to complete fs.readFile('/path/to/file', callback); } const timeoutScheduled = Date.now(); setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms have passed since I was scheduled`); }, 100); // do someAsyncOperation which takes 95 ms to complete someAsyncOperation(() => { const startCallback = Date.now(); // do something that will take 10ms... while (Date.now() - startCallback < 10) { // do nothing } }); 複製代碼
當進入事件循環時,它有一個空隊列(fs.readFile()
還沒有完成),所以定時器將等待剩餘毫秒數,當到達95ms時,fs.readFile()
完成讀取文件而且其完成須要10毫秒的回調被添加到輪詢隊列並執行。
當回調結束時,隊列中再也不有回調,所以事件循環將看到已達到最快定時器的閾值,而後回到timers階段以執行定時器的回調。
在此示例中,您將看到正在調度的計時器與正在執行的回調之間的總延遲將爲105毫秒。
如下是我測試時間:
此階段執行某些系統操做(例如TCP錯誤類型)的回調。 例如,若是TCP socket ECONNREFUSED
在嘗試connect時receives,則某些* nix系統但願等待報告錯誤。 這將在pending callbacks
階段執行。
該poll階段有兩個主要功能:
I/O
回調。當事件循環進入poll
階段而且在timers
中沒有能夠執行定時器時,將發生如下兩種狀況之一
poll
隊列不爲空,則事件循環將遍歷其同步執行它們的callback
隊列,直到隊列爲空,或者達到system-dependent
(系統相關限制)。若是poll
隊列爲空,則會發生如下兩種狀況之一
若是有setImmediate()
回調須要執行,則會當即中止執行poll
階段並進入執行check
階段以執行回調。
若是沒有setImmediate()
回到須要執行,poll階段將等待callback
被添加到隊列中,而後當即執行。
固然設定了 timer 的話且 poll 隊列爲空,則會判斷是否有 timer 超時,若是有的話會回到 timer 階段執行回調。
此階段容許人員在poll階段完成後當即執行回調。
若是poll
階段閒置而且script
已排隊setImmediate()
,則事件循環到達check階段執行而不是繼續等待。
setImmediate()
其實是一個特殊的計時器,它在事件循環的一個單獨階段運行。它使用libuv API
來調度在poll
階段完成後執行的回調。
一般,當代碼被執行時,事件循環最終將達到poll
階段,它將等待傳入鏈接,請求等。
可是,若是已經調度了回調setImmediate()
,而且輪詢階段變爲空閒,則它將結束而且到達check
階段,而不是等待poll
事件。
console.log('start') setTimeout(() => { console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(() => { console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) Promise.resolve().then(function() { console.log('promise3') }) console.log('end') 複製代碼
若是node
版本爲v11.x
, 其結果與瀏覽器一致。
start
end
promise3
timer1
promise1
timer2
promise2
複製代碼
具體詳情能夠查看《又被node的eventloop坑了,此次是node的鍋》。
若是v10版本上述結果存在兩種狀況:
start
end
promise3
timer1
timer2
promise1
promise2
複製代碼
start
end
promise3
timer1
promise1
timer2
promise2
複製代碼
具體狀況能夠參考poll
階段的兩種狀況。
從下圖可能更好理解:
setImmediate
和setTimeout()
是類似的,但根據它們被調用的時間以不一樣的方式表現。
setImmediate()
設計用於在當前poll
階段完成後check階段執行腳本 。setTimeout()
安排在通過最小(ms)後運行的腳本,在timers
階段執行。setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); 複製代碼
執行定時器的順序將根據調用它們的上下文而有所不一樣。 若是從主模塊中調用二者,那麼時間將受到進程性能的限制。
其結果也不一致
若是在I / O
週期內移動兩個調用,則始終首先執行當即回調:
const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); }); 複製代碼
其結果能夠肯定必定是immediate => timeout
。
主要緣由是在I/O階段
讀取文件後,事件循環會先進入poll
階段,發現有setImmediate
須要執行,會當即進入check
階段執行setImmediate
的回調。
而後再進入timers
階段,執行setTimeout
,打印timeout
。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
複製代碼
process.nextTick()
雖然它是異步API的一部分,但未在圖中顯示。這是由於process.nextTick()
從技術上講,它不是事件循環的一部分。
process.nextTick()
方法將 callback
添加到next tick
隊列。 一旦當前事件輪詢隊列的任務所有完成,在next tick
隊列中的全部callbacks
會被依次調用。換種理解方式:
nextTick
隊列,就會清空隊列中的全部回調函數,而且優先於其餘 microtask
執行。let bar; setTimeout(() => { console.log('setTimeout'); }, 0) setImmediate(() => { console.log('setImmediate'); }) function someAsyncApiCall(callback) { process.nextTick(callback); } someAsyncApiCall(() => { console.log('bar', bar); // 1 }); bar = 1; 複製代碼
在NodeV10中上述代碼執行可能有兩種答案,一種爲:
bar 1
setTimeout setImmediate 複製代碼
另外一種爲:
bar 1
setImmediate setTimeout 複製代碼
不管哪一種,始終都是先執行process.nextTick(callback)
,打印bar 1
。
感謝@Dante_Hu提出這個問題await
的問題,文章已經修正。 修改了node端執行結果。V10和V11的區別。
《promise, async, await, execution order》
《Normative: Reduce the number of ticks in async/await》
《async/await 在chrome 環境和 node 環境的 執行結果不一致,求解?》
《更快的異步函數和 Promise》
《JS瀏覽器事件循環機制》
《什麼是瀏覽器的事件循環(Event Loop)?》
《一篇文章教會你Event loop——瀏覽器和Node》
《不要混淆nodejs和瀏覽器中的event loop》
《瀏覽器與Node的事件循環(Event Loop)有何區別?》
《Tasks, microtasks, queues and schedules》
《前端面試之道》
《Node.js介紹5-libuv的基本概念》
《The Node.js Event Loop, Timers, and process.nextTick()》
《node官網》