JS與Node.js中的事件循環

這兩天跟同事同事討論遇到的一個問題,js中的event loop,引出了chrome與node中運行具備setTimeoutPromise的程序時候執行結果不同的問題,從而引出了Nodejs的event loop機制,記錄一下,感受仍是蠻有收穫的javascript

console.log(1)
setTimeout(function() {
  new Promise(function(resolve, reject) {
    console.log(2)
    resolve()
  })
      .then(() => {
        console.log(3)
      })
}, 0)

setTimeout(function() {
  console.log(4)
}, 0)

// chrome中運行:1 2 3 4
// Node中運行: 1 2 4 3

chrome和Node執行的結果不同,這就頗有意思了。php

1. JS 中的任務隊列

JavaScript語言的一大特色就是單線程,也就是說,同一個時間只能作一件事。那麼,爲何JavaScript不能有多個線程呢?這樣能提升效率啊。
JavaScript的單線程,與它的用途有關。做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?
因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。
爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。html

2. 任務隊列 event loop

單線程就意味着,全部任務須要排隊,前一個任務結束,纔會執行後一個任務。若是前一個任務耗時很長,後一個任務就不得不一直等着。
因而,全部任務能夠分紅兩種,一種是同步任務(synchronous),另外一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。
具體來講,異步執行的運行機制以下。(同步執行也是如此,由於它能夠被視爲沒有異步任務的異步執行。)前端

  1. 全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。
  2. 主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
  3. 一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。
  4. 主線程不斷重複上面的第三步。

bg2014100801.jpg

只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會不斷重複。java

3. 定時器 setTimeoutsetInterval

定時器功能主要由setTimeout()setInterval()這兩個函數來完成,它們的內部運行機制徹底同樣,區別在於前者指定的代碼是一次性執行,後者則爲反覆執行。
setTimeout(fn,0)的含義是,指定某個任務在主線程最先可得的空閒時間執行,也就是說,儘量早得執行。它在"任務隊列"的尾部添加一個事件,所以要等到同步任務和"任務隊列"現有的事件都處理完,纔會獲得執行。
HTML5標準規定了setTimeout()的第二個參數的最小值(最短間隔),不得低於4毫秒,若是低於這個值,就會自動增長。在此以前,老版本的瀏覽器都將最短間隔設爲10毫秒。對於那些DOM的變更(尤爲是涉及頁面從新渲染的部分),一般不會當即執行,而是每16毫秒執行一次。這時使用requestAnimationFrame()的效果要好於setTimeout()
另外,瀏覽器內的計時器可能會由於不少緣由而減慢速度:node

  • CPU超載
  • 瀏覽器選項卡處於後臺模式
  • 筆記本電腦使用電池

全部這些均可能將最小延遲提升到300ms甚至1000ms,具體取決於瀏覽器和設置。參考 Scheduling: setTimeout and setInterval
須要注意的是,setTimeout()只是將事件插入了"任務隊列",必須等到當前代碼(執行棧)執行完,主線程纔會去執行它指定的回調函數。要是當前代碼耗時很長,有可能要等好久,因此並無辦法保證,回調函數必定會在setTimeout()指定的時間執行。chrome

4. Nodejs特色

Nodejs架構

NodeJS的顯著特色:異步機制事件驅動
事件輪詢的整個過程沒有阻塞新用戶的鏈接,也不須要維護鏈接。基於這樣的機制,理論上陸續有用戶請求鏈接,NodeJS均可以進行響應,所以NodeJS能支持比Java、php程序更高的併發量。數據庫

雖然維護事件隊列也須要成本,再因爲NodeJS是單線程,事件隊列越長,獲得響應的時間就越長,併發量上去仍是會力不從心。segmentfault

RESTful API是NodeJS最理想的應用場景,能夠處理數萬條鏈接,自己沒有太多的邏輯,只須要請求API,組織數據進行返回便可。瀏覽器

5. Node.js的Event Loop

關於Nodejs中的事件循環還有另外一篇文章詳細探討了下,能夠參考閱讀。

事件輪詢主要是針對事件隊列進行輪詢,事件生產者將事件排隊放入隊列中,隊列另一端有一個線程稱爲事件消費者會不斷查詢隊列中是否有事件,若是有事件,就當即會執行,爲了防止執行過程當中有堵塞操做影響當前線程讀取隊列,事件消費者線程會委託一個線程池專門執行這些堵塞操做。

時間輪詢

Javascript前端和Node.js的機制相似這個事件輪詢模型,有的人認爲Node.js是單線程,也就是事件消費者是單線程不斷輪詢,若是有堵塞操做怎麼辦,不是堵塞了當前單線程的執行嗎?
其實Node.js底層也有一個線程池,線程池專門用來執行各類堵塞操做,這樣不會影響單線程這個主線程進行隊列中事件輪詢和一些任務執行,線程池操做完之後,又會做爲事件生產者將操做結果放入同一個隊列中。
總之,一個事件輪詢Event Loop須要三個組件:

  1. 事件隊列Event Queue,屬於FIFO模型,一端推入事件數據,另一端拉出事件數據,兩端只經過這個隊列通信,屬於一種異步的鬆耦合。
  2. 隊列的讀取輪詢線程,事件的消費者,Event Loop的主角。
  3. 單獨線程池Thread Pool,專門用來執行長任務,重任務,幹繁重體力活的。

Node.js也是單線程的Event Loop,可是它的運行機制不一樣於瀏覽器環境。

Node.js結構

根據上圖,Node.js的運行機制以下。

  1. V8引擎解析JavaScript腳本。
  2. 解析後的代碼,調用Node API。
  3. libuv庫負責Node API的執行。它將不一樣的任務分配給不一樣的線程,造成一個Event Loop(事件循環),以異步的方式將任務的執行結果返回給V8引擎。
  4. V8引擎再將結果返回給用戶。

咱們能夠看到node.js的核心其實是libuv這個庫。這個庫是c寫的,它可使用多線程技術,而咱們的Javascript應用是單線程的。

Nodejs 的異步任務執行流程:

圖片描述

用戶寫的代碼是單線程的,但nodejs內部並非單線程!

事件機制:
Node.js不是用多個線程爲每一個請求執行工做的,相反而是它把全部工做添加到一個事件隊列中,而後有一個單獨線程,來循環提取隊列中的事件。事件循環線程抓取事件隊列中最上面的條目,執行它,而後抓取下一個條目。當執行長期運行或有阻塞I/O的代碼時,注意這裏:它不會被阻塞,會繼續提取下一個事件,而對於被阻塞的事件Node.js會從線程池中取出一個線程來運行這個被阻塞的代碼,同時把當前事件自己和它的回調事件一同添加到事件隊列(callback嵌套callback)。

在Node.js中,由於只有一個單線程不斷地輪詢隊列中是否有事件,對於數據庫文件系統等I/O操做,包括HTTP請求等等這些容易堵塞等待的操做,若是也是在這個單線程中實現,確定會堵塞影響其餘工做任務的執行,Javascript/Node.js會委託給底層的線程池執行,並會告訴線程池一個回調函數,這樣單線程繼續執行其餘事情,當這些堵塞操做完成後,其結果與提供的回調函數一塊兒再放入隊列中,當單線程從隊列中不斷讀取事件,讀取到這些堵塞的操做結果後,會將這些操做結果做爲回調函數的輸入參數,而後激活運行回調函數。

請注意,Node.js的這個單線程不僅是負責讀取隊列事件,還會執行運行回調函數,這是它區別於多線程模式的一個主要特色,多線程模式下,單線程只負責讀取隊列事件,再也不作其餘事情,會委託其餘線程作其餘事情,特別是多核的狀況下,一個CPU核負責讀取隊列事件,一個CPU核負責執行激活的任務,這種方式最適合很耗費CPU計算的任務。反過來,Node..js的執行激活任務也就是回調函數中的任務仍是在負責輪詢的單線程中執行,這就註定了它不能執行CPU繁重的任務,好比JSON轉換爲其餘數據格式等等,這些任務會影響事件輪詢的效率。

6. 實例

看一個具體實例:

console.log('1')
setTimeout(function() {
  console.log('2')
  new Promise(function(resolve) {
    console.log('4')
    resolve()
  }).then(function() {
    console.log('5')
  })
  setTimeout(() => { console.log('6') })
  new Promise(function(resolve) {
    console.log('7')
    resolve()
  }).then(function() {
    console.log('8')
  })
})
setTimeout(function() {
  console.log('9')
}, 0)
new Promise(function(resolve) {
  console.log('10')
  resolve()
}).then(function() {
  console.log('11')
})
setTimeout(function() {
  console.log('12')
  new Promise(function(resolve) {
    console.log('13')
    resolve()
  }).then(function() {
    console.log('14')
  })
})
new Promise(function(resolve) {
  console.log('15')
  resolve()
}).then(function() {
  console.log('16')
})

// node1  : 1,10,15,11,16,2,4,7,9,12,13,5,8,14,6        // 結果不穩定
// node2  : 1,10,15,11,16,2,4,7,9,5,8,12,13,14,6        // 結果不穩定
// node3  : 1,10,15,11,16,2,4,7,5,8,9,12,13,14,6        // 結果不穩定
// chrome : 1,10,15,11,16,2,4,7,5,8,9,12,13,14,6

chrome的運行比較穩定,而node環境下運行不穩定,可能會出現兩種狀況。
chrome運行的結果的緣由是Promiseprocess.nextTick()的微任務Event Queue運行的權限比普通宏任務Event Queue權限高,若是取事件隊列中的事件的時候有微任務,就先執行微任務隊列裏的任務,除非該任務在下一輪的Event Loop中,微任務隊列清空了以後再執行宏任務隊列裏的任務。

關於Node中的事件循環和異步API的內容,具體能夠參見另外一篇帖子,有具體討論。

7. 瀏覽器中的事件循環

瀏覽器中和Node中的事件循環的執行順序並不一致,在瀏覽器中,咱們能夠按性質把任務分爲兩類,macrotask(宏任務)和 microtask(微任務)。

  • macrotask: script (同步代碼), setTimeout, setInterval, setImmediate, MessageChannel, postMessage, I/O, UI渲染
  • microtask: process.nextTick, Promises(這裏指瀏覽器原生實現的 Promise), Object.observe, MutationObserver

clipboard.png

執行順序:

  • 引擎首先從macrotask queue中取出第一個任務,執行完畢後,將microtask queue中的全部任務取出,按順序所有執行;
  • 而後再從macrotask queue中取下一個,執行完畢後,再次將microtask queue中的所有取出;
  • 循環往復,直到兩個queue中的任務都取完。

網上的帖子大多深淺不一,甚至有些先後矛盾,在下的文章都是學習過程當中的總結,若是發現錯誤,歡迎留言指出~

參考:

  1. Node.js的事件輪詢Event Loop原理解釋
  2. JavaScript 運行機制詳解:再談Event Loop
  3. js與Nodejs的單線程和異步--初探
  4. 這一次,完全弄懂 JavaScript 執行機制
  5. JavaScript任務隊列的順序機制(事件循環)

PS:歡迎你們關注個人公衆號【前端下午茶】,一塊兒加油吧~

另外能夠加入「前端下午茶交流羣」微信羣,長按識別下面二維碼便可加我好友,備註加羣,我拉你入羣~

相關文章
相關標籤/搜索