本篇主要介紹JavaScript的運行機制:單線程事件循環(Event Loop). html
結論先: 在JavaScript中, 利用運行至完成和非阻塞IO 完成單線程下異步任務的處理. 就是先處理主模塊(主線程)上的同步任務, 再處理異步任務. 異步任務使用事件循環機制完成調度.node
涉及的內容有: 單線程, 事件循環, 同步執行, 異步執行, 定時器, nodeJS的事件循環ajax
開始以前, 先看下面的代碼, 給出結果:chrome
// 當前時間 console.log('A: ' + new Date()); // 1秒(1000毫秒)後執行的定時器 // 異步執行的代碼 setTimeout(function() { console.log('B: ' + new Date()); }, 1000); // 循環3秒(3000毫秒) var end = Date.now() + 3000; while(Date.now() < end) { } // 當前時間 console.log('C: ' + new Date());
在瀏覽器中的結果爲(chrome-50.0.2661.102):瀏覽器
A: Thu May 25 2017 13:48:26 GMT+0800 (中國標準時間) C: Thu May 25 2017 13:48:29 GMT+0800 (中國標準時間) B: Thu May 25 2017 13:48:29 GMT+0800 (中國標準時間)
在NodeJS(v7.7.2 win-x64)中的結果爲:服務器
>node scripts\async.js A: Thu May 25 2017 13:50:55 GMT+0800 (中國標準時間) C: Thu May 25 2017 13:50:58 GMT+0800 (中國標準時間) B: Thu May 25 2017 13:50:58 GMT+0800 (中國標準時間)
tip: 瀏覽器下和NodeJS結果一致.微信
分析上面的代碼與結果, 注意的要點:網絡
雖然設置的定時器爲1秒後執行, 但實際的執行時間在3秒之後, 看結果中B:的輸出, 在A:的3秒後.多線程
B:的輸出在C:的輸出以後. 可見, 雖然在while循環後, 時間已經到了定時器代碼須要執行的時間, 但並無當即執行, 而是等到了console.log('C: ')執行完, 再執行的定時器的代碼.併發
本篇就是說明爲何會出現以上的現象. 下面請一步步的看.
單線程, 指的是JavaScript在一個時間僅處理一個任務. 就是JavaScript在執行時, 存在一個執行隊列, 依次執行隊列中的任務, 不能同時執行多個任務.
單線程的優點, 也是JavaScript選擇單線程的緣由是:
1, 下降處理複雜性, 簡化開發. 例如不用考慮死鎖, 競爭機制等.
2, 做爲用於處理與用戶互動的腳本語言, 能夠更加容易地處理狀態同步的問題(想一想考慮用戶操做的不肯定性).
3, JavaScript核心維護人員自身的設計與理解.
4, 越簡單越容易推廣, 快速上手.
除了優點, 單線程有明顯的劣勢, 就是併發處理能力, 由於單線程處理下全部的任務就要排隊處理. 可是若是排在前面的任務處理很耗時, 那就致使後面的任務一直處於等待狀態. 若是前面的任務出處於滿載運行狀態還能夠, 可是若是前面的任務處於IO等待狀態呢? 就會致使CPU處理資源的浪費.
思考, 前面的是AJAX任務, 後邊是其餘任務. AJAX任務須要等待網絡請求響應結束, 才能處理, 此時前面的AJAX任務就處於IO等待狀態. 從而致使後面的任務也執行不了, 形成了單線程下的資源浪費. (CPU沒有辦法高速運轉, 處於空閒狀態).
在此狀況下, 徹底能夠掛起前面的AJAX任務(掛起等待AJAX的響應結果), 先執行後面的任務. 等後面的任務處理完畢後, 再看前面的AJAX任務是否獲得了IO結果, 若是有結果了, 在翻回來處理便可. 這種處理方式, 就是異步方式.
單線程的JavaScript爲了更好利用CPU的性能, 將執行的任務設計爲: 同步任務和異步任務, 兩類.
同步任務(synchronous task), 就是須要一個個順序執行的任務, 不能跳過, 執行完前一個才能執行後一個. 咱們稱之爲在主模塊(主線程)執行的任務.
異步任務(asynchronous task), 指的是被掛起執行的任務, 在系統內部處於等待IO處理結果狀態, 一旦處理完畢, 記錄下來, 等待後續處理. 須要事件循環處理的任務. 上面示例中的AJAX任務就是異步任務.
你應該會想, JavaScript不是單線程麼, 怎麼還能異步處理呢?
是這樣的, JavaScript的單線程, 指的是在JavaScript語言(語法)層面是單線程的. 而內部的執行, 仍是能夠利用處處理器多線程和操做系統的任務調度的, 在後臺處理咱們的異步任務. 當操做在後臺被處理完成後(例如ajax接收完畢了服務器的響應), 操做系統將結果告知給JavaScript, 並最終被JavaScript執行.
JavaScript是如何調度這些同步任務和異步任務的呢?
就涉及到了, 本文的重點: 任務隊列 和 事件循環, 執行棧.
如圖(邏輯概述圖)所示:
執行以下:
step1, 同步任務直接放入到主模塊(主線程)任務隊列執行. 異步任務掛起後臺執行, 等待IO事件完成或行爲事件被觸發.
step2, 系統後臺執行異步任務, 若是某個異步任務事件發生(或者是行爲事件被觸發), 則將該任務push到任務隊列中, 每一個任務會對應一個回調函數進行處理. 這個步驟在後臺一直執行, 由於就不斷有事件被觸發, IO不斷完成, 任務被不斷的加入到任務隊列中.
step3, 執行任務隊列中的任務. 任務的具體執行是在執行棧中完成的. 當運行棧中一個任務的基本運行單元(稱之爲Frame, 楨)所有執行完畢後, 去讀取任務隊列中的下一個任務, 繼續執行. 是一個循環的過程. 處理一個任務隊列中的任務, 稱之爲一個tick.
注意, step3, 是一個循環的過程, 這就是事件循環. 循環執行任務隊列中已經發生的事件對應的任務.
再參考開始的代碼, 咱們能夠知道:
// A:當前時間 // 同步代碼, 直接進入任務隊列 console.log('A: ' + new Date()); // B:1秒(1000毫秒)後執行的定時器 // 異步代碼, 等待到時事件發生, 纔會進入任務隊列 setTimeout(function() { console.log('B: ' + new Date()); }, 1000); // 循環3秒(3000毫秒), // 同步代碼, 直接進入任務隊列 var end = Date.now() + 3000; // 同步代碼, 直接進入任務隊列 while(Date.now() < end) { } // C:當前時間 // 同步代碼, 直接進入任務隊列 console.log('C: ' + new Date());
也就意味着, 此時, log(A), while, log(C) 三個任務, 已經進入到了任務隊列中. 而setTimeout是異步任務(與AJAX一致)在等待事件發生(到時事件). 於此同時, JavaScript開始處理任務隊列. 隊列是先進先出, 須要依次處理. 因此, 即時當前已經到1s了, 事件發生, 也僅僅是將該任務push入任務隊列而已(並無當即執行回調函數). 當將setTimeout入隊列時, log(C)已經在隊列中了, 所以, setTimeout的log(B), 會在log(B)後執行. 這就是輸出了: A, C, B的緣由. 以下圖(邏輯概述圖)所示:
由任務隊列可知, 輸出爲: A, C, B順序.
JavaScript提供了能夠操做定時器的函數, setTimeout()和setInterval. 在NodeJS中還有setImmediate().
setTimeout(callback, timer), 多久(毫秒計)後執行, 常規用法已經演示.
須要提醒你們的是, setTimetout()是延時觸發, 而不是即時觸發. 指的是, 在有機會處理計時器事件時, 優先處理最早到時的計時器程序. 而不是時間到當即處理. 由於是單線程, 須要先處理當前的任務, 例如主模塊中的任務(同步任務).
實操中還有一個setTimeout(callback, 0)的用法, 表示當即加入到任務隊列. 可是注意, 並非在執行setTimeout的時候, 就加入隊列了, 而是當所有的同步任務入隊列後, 當即加入到任務隊列, 也就意味着同步任務以後第一個執行. 但聽說這個值內部執行時有一個最小值, 4ms.
上面的代碼, 將時間改成0, 測試結果仍是A, C, B. 不會由於先執行的setTimeout()而就將任務先執行.
// 異步代碼, 等待到時事件發生, 纔會進入任務隊列 setTimeout(function() { console.log('B: ' + new Date()); }, 0);
未發生的定時器, 可使用clearTimeout()方法清除.
setInterval(callback, timer) 與setTimeout()類似, 不過是在callback執行完畢後, 再次設置了計時器. 再也不贅述.
NodeJS的事件循環模型比瀏覽器更爲複雜些.
以下圖所示(引用自NodeJS官方文檔), 事件循環, 按照下圖的順序調用事件.
因爲出現了不一樣的事件循環段, 例如 timer, check, 出現了額外的控制定時器方法.
邏輯含義上講, 與setTimeout(callback, 0)一致. 都是當即執行. 在NodeJS中setImmediate()存在的主要場景就是, 在異步IO調用中, 若是同時使用setImmediate()和settimeout(), 能夠保證, setImmediate()先於全部的setTimeout()執行.
以下代碼: (引用自NodeJS官方文檔)
var fs = require('fs'); // 異步文件IO fs.readFile(__filename, () => { setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); }); });
以上代碼的執行結果, 必定是:
>node scripts\async-node.js immediate timeout 這是由於, 根據NodeJS的事件循環處理順序, 處理完IO後, 須要處理check, 而setImmediate()就是check中的事件. 所以先處理.
但上面的代碼若是沒有在異步IO中調用, 在主模塊(主線程)中調用, 則順序不必定, 由操做系統調度決定!
tick, 就是一個事件循環週期. 在prcess.nextTick()中設置的異步callback會在當前事件循環週期結束, 下一個事件循環週期開始前執行.
像是一個插入的tick. 生成了一個新的週期. 說白了, 是一個插隊行爲.
所以, 在時間上看, 必定先於settimeout(callback, 0)和setImmediate()執行. 一般用來處理在下一個事件週期(異步任務)前, 必需要處理好的任務. 常見的有, 處理錯誤, 回收資源, 和 從新執行存在錯誤的操做等.
測試一下執行時機:
setTimeout(function timeout () { console.log('timeout'); },0); setImmediate(function immediate () { console.log('immediate'); }); process.nextTick(function immediate () { console.log('nickTick'); });
結果爲:
>node scripts\async-node.js nextTick timeout immediate
可見, nextTick先發生.
注意, 在NodeJS中, nexttick並非一個特殊的定時器.
注意, 因爲nextTick()會插隊執行, 所以, NodeJS限制了nextTick()遞歸調用的深度. 防止IO處理飢餓.一直在處理nextTick(). 因爲該緣由, 遞歸時, NodeJS建議使用setImmediate()完成.
process.nextTick, 永遠先執行.
setImmediate和setTimeout, 那個先到時那個先執行. 若是同時, 則由系統調度負責.
在JavaScript中, 利用運行至完成和非阻塞IO 完成單線程下異步任務的處理. 就是先處理主模塊(主線程)上的同步任務, 再處理異步任務. 異步任務使用事件循環機制完成調度.
參考:
NodeJS文檔, https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ JavaScript 運行機制詳解:再談Event Loop, http://www.ruanyifeng.com/blog/2014/10/event-loop.html 樸靈 深刻淺出Node.js http://www.infoq.com/cn/master-nodejs
以上就是本人對事件循環的理解. 一家之言, 歡迎討論拍磚!
更多內容, 能夠關注, 微信公衆號, 小韓說理.