衆所周知,JavaScript 是單線程的,所謂單線程,就是指一次只能完成一個任務,若是有多個任務就必需要排隊,前面的一個任務完成了,再執行後面的任務,以此類推。html
須要注意的是 JavaScript 只在一個線程上運行,不表明瀏覽器內核只有一個線程,事實上瀏覽器內部有多個線程,主線程用於 JavaScript 代碼的編譯和執行,其它線程都是在後臺配合主線程。web
JavaScript 之因此選擇單線程,跟歷史有關係。JavaScript 從誕生起就是單線程,緣由是不想讓瀏覽器變得太複雜,多線程須要面臨鎖、狀態同步等問題,這對於一種網頁腳本語言來講開銷太大。若是 JavaScript 同時有兩個線程,一個線程在網頁 DOM 節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?因此,爲了不復雜性,JavaScript 一開始就是單線程,這已經成了這門語言的核心特徵。編程
上面說了 JavaScript 是單線程的,這種模式下,若是有一個很是耗時的任務進行的話,後面的任務都得排隊等着,這時候應用程序就沒法去作其餘的事情,爲此 JavaScript 語言的任務執行模式分爲兩個部分:同步(Synchronous)和異步(Asynchronous)api
那麼 JavaScript 是如何來執行異步任務的呢,就是後面要講的事件循環機制。promise
講事件循環以前,咱們先來看一下 JavaScript 中的 call stack。下圖是 JavaScript 引擎的一個簡化圖:瀏覽器
上圖中看出 JavaScript 引擎主要包含兩個部分:服務器
前面說了,JavaScript 是一種單線程編程語言,這意味着它只有一個 Call Stack
。所以,它一次僅能作一件事。Call Stack 是一個數據結構,它基本記錄了咱們在程序執行中的所處的位置,若是咱們進入一個函數,咱們把它放在堆棧的頂部。若是咱們從一個函數中返回,咱們彈出堆棧的頂部。網絡
上面圖中能夠看出,當開始執行 JS 代碼時,首先向調用棧中壓入一個 main()函數(表明了全局上下文),而後執行咱們的代碼,根據先進後出的原則,後執行的代碼會先彈出棧。數據結構
若是在調用堆棧中執行的函數調用須要花費大量時間才能進行處理,會發生什麼? 例如,假設你想在瀏覽器中使用 JavaScript 進行一些複雜的圖像轉換。這時候瀏覽器就被阻塞了,這意味着瀏覽器沒法渲染,它不能運行任何其餘代碼,它就是被卡住了。這時候就想到了咱們前面講過的異步任務的處理方式,那麼如何執行異步任務呢,就是下面要講的事件循環(event loop)機制多線程
儘管容許執行異步 JavaScript 代碼(如 setTimeout 函數),但直到 ES6 出現,實際上 JavaScript 自己歷來沒有任何明確的異步概念。 JavaScript 引擎歷來都只是執行單個程序模塊而不作更多別的事情。 那麼,誰來告訴 JS 引擎去執行你編寫的一大段程序?實際上,JS 引擎並非孤立運行,它運行在一個宿主環境中,對於大多數開發人員來講,宿主環境就是一個典型的 Web 瀏覽器或 Node.js。全部環境中的共同點是一個稱爲事件循環的內置機制,它隨着時間的推移處理程序中多個模塊的執行順序,並每次調用 JS 引擎。
因此,例如,當你的 JavaScript 程序發出一個 Ajax 請求來從服務器獲取一些數據時,你在一個回調函數中寫好了 「響應」 代碼,JS 引擎將會告訴宿主環境:
「嘿,我如今暫停執行,可是每當你完成這個網絡請求,而且你有一些數據,請調用這個函數並返回給我。
而後瀏覽器開始監聽來自網絡的響應,當響應返回給你的時候,宿主環境會將回調函數插入到事件循環中來安排回調函數的執行順序。
咱們來看下面的圖表:
咱們都使用過 setTimeout、AJAX 這些 API, 可是,這些 API 不是由 JS 引擎提供的。那這些 Web APIs 究竟是什麼? 從本質上講,它們是瀏覽器並行啓動的一部分,是你沒法訪問的線程,你僅僅只能夠調用它們。
前面說了瀏覽器內核是多線程,在內核控制下各線程相互配合以保持同步,一個瀏覽器一般由如下常駐線程組成:
上圖中看出,JavaScript 運行時,除了正在運行的主線程,還存在一個 callback queue(也叫task queue),即任務隊列,裏面是各類須要當前程序處理的異步任務(實際上,根據異步任務的類型,存在多個任務隊列)。
異步執行的運行機制以下:
下面咱們經過一個例子來看一下具體的執行過程。
console.log('Hi');
setTimeout(function cb1() {
console.log('cb1');
}, 5000);
console.log('Bye');
複製代碼
setTimeout 有個要注意的地方,如上述例子延遲 5s 執行,不是嚴格意義上的 5s,正確來講是至少 5s 之後會執行。由於 Web API 會設定一個 5s 的定時器,時間到期後將回調函數加到隊列中,此時該回調函數還不必定會立刻運行,由於隊列中可能還有以前加入的其餘回調函數,並且還必須等到 Call Stack 空了以後纔會從隊列中取一個回調執行。這也是不少人說 JavaScript 中的定時器其實不是徹底精確的緣由。
關於事件循環的詳細講解,推薦一個視頻《what the hack is event loop》
每一個線程都有本身的事件循環,因此每一個 web worker 有本身的事件循環(event loop),因此它能獨立地運行。一個事件循環有多個 task 來源,而且保證在 task 來源內的執行順序,在每次循環中瀏覽器要選擇從哪一個來源中選取 task,任務源能夠分爲 微任務(microtask) 和 宏任務(macrotask),在ES6規範中,microtask 稱爲 jobs, macrotask 稱爲 task。
macrotask 主要包括下面幾個:
microtask 主要包含:
參考 whatwg規範中關於任務隊列的定義咱們能夠了解到:
有點繞,咱們下面先看一個例子來解釋一下:
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
複製代碼
咱們來分析一下上面代碼的具體執行步驟。表格中紅色的表示當前正在執行的任務。
macrotasks | microtasks | call stack | Log |
---|---|---|---|
script |
script | script start |
macrotasks | microtasks | call stack | Log |
---|---|---|---|
script |
script | script start | |
setTimeout callback |
macrotasks | microtasks | call stack | Log |
---|---|---|---|
script |
Promise then 1 | script | script start |
setTimeout callback |
macrotasks | microtasks | call stack | Log |
---|---|---|---|
script |
Promise then 1 |
Promise callback 1 |
script start |
setTimeout callback | script end |
macrotasks | microtasks | call stack | Log |
---|---|---|---|
script |
Promise then 1 |
Promise callback 1 |
script start |
setTimeout callback | script end | ||
promise1 |
macrotasks | microtasks | call stack | Log |
---|---|---|---|
script |
Promise then 2 |
Promise callback 2 |
script start |
setTimeout callback | script end | ||
promise1 | |||
promise2 |
macrotasks | microtasks | call stack | Log |
---|---|---|---|
setTimeout callback |
setTimeout callback |
script start | |
script end | |||
promise1 | |||
promise2 | |||
setTimeout |