衆所周知,JavaScript 是一門單線程語言,雖然在 html5 中提出了 Web-Worker ,但這並未改變 JavaScript 是單線程這一核心。可看HTML規範中的這段話:html
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.html5
爲了協調事件、用戶交互、腳本、UI 渲染和網絡處理等行爲,用戶引擎必須使用 event loops。Event Loop 包含兩類:一類是基於 Browsing Context ,一種是基於 Worker ,兩者是獨立運行的。 下面本文用一個例子,着重講解下基於 Browsing Context 的事件循環機制。web
來看下面這段 JavaScript 代碼:ajax
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');
先猜想一下這段代碼的輸出順序是什麼,再去瀏覽器控制檯輸入一下,看看實際輸出的順序和你猜想出的順序是否一致,若是一致,那就說明,你對 JavaScript 的事件循環機制仍是有必定了解的,繼續往下看能夠鞏固下你的知識;而若是實際輸出的順序和你的猜想不一致,那麼本文下面的部分會爲你答疑解惑。api
全部的任務能夠分爲同步任務和異步任務,同步任務,顧名思義,就是當即執行的任務,同步任務通常會直接進入到主線程中執行;而異步任務,就是異步執行的任務,好比ajax網絡請求,setTimeout 定時函數等都屬於異步任務,異步任務會經過任務隊列( Event Queue )的機制來進行協調。具體的能夠用下面的圖來大體說明一下:promise
同步和異步任務分別進入不一樣的執行環境,同步的進入主線程,即主執行棧,異步的進入 Event Queue 。主線程內的任務執行完畢爲空,會去 Event Queue 讀取對應的任務,推入主線程執行。 上述過程的不斷重複就是咱們說的 Event Loop (事件循環)。瀏覽器
在事件循環中,每進行一次循環操做稱爲tick,經過閱讀規範可知,每一次 tick 的任務處理模型是比較複雜的,其關鍵的步驟能夠總結以下:網絡
能夠用一張圖來講明下流程:
app
這裏相信有人會想問,什麼是 microtasks ?規範中規定,task分爲兩大類, 分別是 Macro Task (宏任務)和 Micro Task(微任務), 而且每一個宏任務結束後, 都要清空全部的微任務,這裏的 Macro Task也是咱們常說的 task ,有些文章並無對其作區分,後面文章中所說起的task皆看作宏任務( macro task)。webapp
(macro)task 主要包含:script( 總體代碼)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 環境)
microtask主要包含:Promise、MutaionObserver、process.nextTick(Node.js 環境)
setTimeout/Promise 等API即是任務源,而進入任務隊列的是由他們指定的具體執行任務。來自不一樣任務源的任務會進入到不一樣的任務隊列。其中 setTimeout 與 setInterval 是同源的。
千言萬語,不如就着例子講來的清楚。下面咱們能夠按照規範,一步步執行解析下上面的例子,先貼一下例子代碼(省得你往上翻)。
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');
至此,Event Queue 中存在三個任務,以下表:
宏任務 | 微任務 |
---|---|
setTimeout | then1 |
- | then2 |
so,你猜對了嗎?
看看你掌握了沒
再來一個題目,來作個練習:
console.log('script start'); setTimeout(function() { console.log('timeout1'); }, 10); new Promise(resolve => { console.log('promise1'); resolve(); setTimeout(() => console.log('timeout2'), 10); }).then(function() { console.log('then1') }) console.log('script end');
這個題目就稍微有點複雜了,咱們再分析下:
首先,事件循環從宏任務 (macrotask) 隊列開始,最初始,宏任務隊列中,只有一個 scrip t(總體代碼)任務;當遇到任務源 (task source) 時,則會先分發任務到對應的任務隊列中去。因此,就和上面例子相似,首先遇到了console.log,輸出 script start; 接着往下走,遇到 setTimeout 任務源,將其分發到任務隊列中去,記爲 timeout1; 接着遇到 promise,new promise 中的代碼當即執行,輸出 promise1, 而後執行 resolve ,遇到 setTimeout ,將其分發到任務隊列中去,記爲 timemout2, 將其 then 分發到微任務隊列中去,記爲 then1; 接着遇到 console.log 代碼,直接輸出 script end 接着檢查微任務隊列,發現有個 then1 微任務,執行,輸出then1 再檢查微任務隊列,發現已經清空,則開始檢查宏任務隊列,執行 timeout1,輸出 timeout1; 接着執行 timeout2,輸出 timeout2 至此,全部的都隊列都已清空,執行完畢。其輸出的順序依次是:script start, promise1, script end, then1, timeout1, timeout2
用流程圖看更清晰:
有個小 tip:從規範來看,microtask 優先於 task 執行,因此若是有須要優先執行的邏輯,放入microtask 隊列會比 task 更早的被執行。
最後的最後,記住,JavaScript 是一門單線程語言,異步操做都是放到事件循環隊列裏面,等待主執行棧來執行的,並無專門的異步執行線程。。
這一次,完全弄懂 JavaScript 執行機制 Tasks, microtasks, queues and schedules 從一道題淺說 JavaScript 的事件循環