JavaScript語言最大特色就是單線程,可是這裏的單線程指的是主線程是單線程的。那爲何js要單線程呢? 由於,JS主要用於操做DOM,若是是有兩個線程,一個在DOM上添加內容,一個在DOM上刪除內容,此時瀏覽器該以哪一個爲準呢? 因此爲了不復雜性,JavaScript從誕生起就是單線程的。html
同步和異步關注的是消息通知機制前端
JavaScript
是單線程的。就意味着全部任務都須要排隊,前一個任務結束,後一個任務才能執行。前一個任務耗時很長,後一個任務也得一直等着。可是IO設備(好比ajax
網絡請求)很慢,CPU一直初一顯得狀態,這樣就很不合理了。
同步任務
是指在主線程上執行的任務,只有前一個任務執行完畢,下一個任務才能執行。異步任務
是指不進入主線程,而是進入任務隊列(task queue)
的任務,只有主線程任務執行完畢,任務隊列的任務纔會進入主線程執行。node
從上圖看到:git
Node.js也是單線程的Event Loop,可是它的運行機制不一樣於瀏覽器環境。github
根據上圖,Node.js的運行機制以下:ajax
除了
setTimeout
和setInterval
這兩個方法,Node.js還提供了另外兩個與"任務隊列"有關的方法:process.nextTick和setImmediate。api
process.nextTick
方法能夠在當前"執行棧"的尾部----下一次Event Loop(主線程讀取"任務隊列")以前----觸發回調函數。也就是說,它指定的任務老是發生在全部異步任務以前。setImmediate
方法則是在當前"任務隊列"的尾部添加事件,也就是說,它指定的任務老是在下一次Event Loop時執行,這與setTimeout(fn, 0)
很像。promise
英文原文: When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.
瀏覽器
當Node.js啓動時會初始化event loop, 每個event loop都會包含按以下順序六個循環階段bash
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
每個階段都有一個裝有callbacks的fifo queue(隊列),當event loop運行到一個指定階段時,node將執行該階段的fifo queue(隊列),當隊列callback執行完或者執行callbacks數量超過該階段的上限時,event loop會轉入下一下階段. **注意上面六個階段都不包括 process.nextTick()。**process.nextTick()不在event loop的任何階段執行,而是在各個階段切換的中間執行,即從一個階段切換到下個階段前執行。
任務可分爲宏任務和微任務
常見的宏任務和微任務:
macro-task(宏任務)
: setTimeout
, setInterval
, setImmediate
, I/O
micro-task(微任務)
:process.nextTick
, 原生Promise
(有些實現的promise
將then
方法放到了宏任務中),Object.observe
(已廢棄), MutationObserver
看下面的例子:
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
Task
是嚴格按照時間順序壓棧和執行的,因此瀏覽器可以使得 JavaScript
內部任務與 DOM
任務可以有序的執行。當一個 task 執行結束後,在下一個 task
執行開始前,瀏覽器能夠對頁面進行從新渲染。每個 task
都是須要分配的,例如從用戶的點擊操做到一個點擊事件,渲染HTML文檔,同時還有上面例子中的 setTimeout
。
基於前面描述的event loop
, setTimeout
它會在延遲時間結束後分配一個新的 task
至 event loop
中,而不是當即執行,因此 setTimeout
的回調函數會等待前面的 task 都執行結束後再運行。這就是爲何 setTimeout
會輸出在 script end
以後,由於 script end
是第一個 task 的其中一部分,而 setTimeout
則是一個新的 task
。
微任務
一般來講就是須要在當前 task 執行結束後當即執行的任務,例如須要對一系列的任務作出迴應,或者是須要異步的執行任務而又不須要分配一個新的 task,這樣即可以減少一點性能的開銷。
微任務任務隊列是一個與 task
任務隊列相互獨立的隊列,微任務將會在每個 task
任務執行結束以後執行。每個 task
中產生的 微任務 都將會添加到 微任務 隊列中,微任務 中產生的 微任務 將會添加至當前隊列的尾部,而且 微任務 會按序的處理完隊列中的全部任務。
每當一個 Promise
被決議(或是被拒絕),便會將其回調函數添加至 微任務隊列中做爲一個新的 微任務。這也保證了 Promise
能夠異步的執行。因此當咱們調用 .then(resolve, reject)
的時候,會當即生成一個新的 微任務添加至隊列中,這就是爲何上面的 promise1
和 promise2
會輸出在 script end
以後,由於 微任務隊列中的任務必須等待當前 task
執行結束後再執行,而 promise1
和 promise2
輸出在 setTimeout
以前,這是由於 setTimeout
是一個新的 task
,而 微任務執行在當前 task
結束以後,下一個 task
開始以前。
參考: