首頁  >  文章  >  web前端  >  JS與Node.js中的事件循環詳解

JS與Node.js中的事件循環詳解

小云云
小云云原創
2017-12-13 09:30:471473瀏覽

js中的event loop,引出了chrome與node中運行具有setTimeoutPromise的程式時候執行結果不一樣的問題,從而引出了Nodejs的event loop機制,這篇文章透過實例給大家詳細分析了JS與Node.js中的事件的原理以及用法,希望能幫助大家。

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執行的結果不一樣,這就很有趣了。

1. JS 中的任務佇列

#JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。那麼,為什麼JavaScript不能有多個執行緒呢?這樣能提高效率啊。

JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與使用者互動,以及操作DOM。這決定了它只能是單線程,否則會帶來複雜的同步問題。例如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上加入內容,另一個線程就刪除了這個節點,這時瀏覽器應該以哪個執行緒為準?

所以,為了避免複雜性,從一誕生,JavaScript就是單線程,這已經變成了這門語言的核心特徵,將來也不會改變。

為了利用多核心CPU的運算能力,HTML5提出Web Worker標準,允許JavaScript腳本建立多個線程,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質。

2. 任務佇列event loop

#單一執行緒就意味著,所有任務都需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就得一直等著。

於是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是非同步任務(asynchronous)。同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;非同步任務指的是,不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務隊列"通知主線程,某個非同步任務可以執行了,該任務才會進入主執行緒執行。

具體來說,非同步執行的運作機制如下。 (同步執行也是如此,因為它可以被視為沒有非同步任務的非同步執行。)

所有同步任務都在主執行緒上執行,形成一個執行堆疊(execution context stack)。主執行緒之外,還存在一個"任務隊列"(task queue)。只要非同步任務有了運行結果,就在"任務隊列"之中放置一個事件。一旦"執行堆疊"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。主執行緒不斷重複上面的第三步。

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

3. 定時器 setTimeoutsetInterval

##定時器功能主要由

setTimeout()setInterval()這兩個函數來完成,它們的內部運作機製完全一樣,差別在於前者指定的程式碼是一次執行,後者則為重複執行。

setTimeout(fn,0)的意思是,指定某個任務在主執行緒最早可得的空閒時間執行,也就是說,盡可能早得執行。它在"任務佇列"的尾部新增一個事件,因此要等到同步任務和"任務佇列"現有的事件都處理完,才會執行。

HTML5標準規定了setTimeout()的第二個參數的最小值(最短間隔),不得低於4毫秒,如果低於這個值,就會自動增加。在此之前,舊版的瀏覽器都會將最短間隔設為10毫秒。另外,對於那些DOM的變動(尤其是涉及頁面重新渲染的部分),通常不會立即執行,而是每16毫秒執行一次。這時使用requestAnimationFrame()的效果比setTimeout()##。

要注意的是,setTimeout()只是將事件插入了"任務佇列",必須等到目前程式碼(執行堆疊)執行完,主執行緒才會去執行它指定的回呼函數。要是當前程式碼耗時很長,有可能要等很久,所以沒有辦法保證,回呼函數一定會在setTimeout()指定的時間執行。

4. Node.js的Event Loop#

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

Javascript前端和Node.js的機制類似這個事件輪詢模型,有的人認為Node.js是單線程,也就是事件消費者是單線程不斷輪詢,如果有堵塞操作怎麼辦,不是堵塞了當前單線程的執行嗎?

其實Node.js底層也有一個線程池,線程池專門用來執行各種堵塞操作,這樣就不會影響單線程這個主線程進行隊列中事件輪詢和一些任務執行,執行緒池操作完以後,又會作為事件生產者將操作結果放入同一個佇列中。

總之,一個事件輪詢Event Loop需要三個元件:

事件佇列Event Queue,屬於FIFO模型,一端推入事件數據,另外一端拉出事件數據,兩端只透過這個隊列通訊,屬於一種非同步的鬆散耦合。隊列的讀取輪詢線程,事件的消費者,Event Loop的主角。單獨執行緒池​​Thread Pool,專門用來執行長任務,重任務,乾繁重體力活的。

Node.js也是單執行緒的Event Loop,但是它的運作機制不同於瀏覽器環境。

根據上圖,Node.js的運作機制如下。

V8引擎解析JavaScript腳本。解析後的程式碼,呼叫Node API。 libuv函式庫負責Node API的執行。它將不同的任務分配給不同的線程,形成一個Event Loop(事件循環),以非同步的方式將任務的執行結果傳回V8引擎。 V8引擎再將結果回傳給使用者。

我們可以看到node.js的核心其實是libuv這個函式庫。這個函式庫是c寫的,它可以使用多執行緒技術,而我們的Javascript應用是單執行緒的。

Nodejs 的非同步任務執行流程:

使用者寫的程式碼是單執行緒的,但nodejs內部並不是單一執行緒!

事件機制:

Node.js不是用多個執行緒為每個請求執行工作的,相反而是它把所有工作加入到一個事件佇列中,然後有一個單獨線程,來循環提取隊列中的事件。事件循環執行緒抓取事件佇列中最上面的條目,執行它,然後抓取下一個條目。當執行長期運行或有阻塞I/O的程式碼時

在Node.js中,因為只有一個單執行緒不斷地輪詢佇列中是否有事件,對於資料庫檔案系統等I/O操作,包括HTTP請求等等這些容易堵塞等待的操作,如果也是在這個單執行緒中實現,肯定會堵塞影響其他工作任務的執行,Javascript/Node.js會委託給底層的執行緒池執行,並會告訴執行緒池一個回調函數,這樣單線程繼續執行其他事情,當這些堵塞操作完成後,其結果與提供的回調函數一起再放入隊列中,當單線程從隊列中不斷讀取事件,讀取到這些堵塞的操作結果後,會將這些操作結果作為回呼函數的輸入參數,然後啟動運行回呼函數。

請注意,Node.js的這個單線程不只是負責讀取隊列事件,還會執行運行回調函數,這是它區別於多線程模式的一個主要特點,多線程模式下,單執行緒只負責讀取佇列事件,不再做其他事情,會委託其他執行緒做其他事情,特別是多核心的情況下,一個CPU核負責讀取佇列事件,一個CPU核負責執行啟動的任務,這種方式最適合很耗費CPU運算的任務。反過來,Node..js的執行啟動任務也就是回呼函數中的任務還是在負責輪詢的單執行緒中執行,這就注定了它不能執行CPU繁重的任務,例如JSON轉換為其他資料格式等等,這些任務會影響事件輪詢的效率。

5. Nodejs特性

#NodeJS的顯著特性:非同步機制、事件驅動。

事件輪詢的整個過程沒有阻塞新使用者的連接,也不需要維護連接。基於這樣的機制,理論上陸續有使用者請求連接,NodeJS都可以回應,因此NodeJS能支援比Java、php程式更高的並發量。

雖然維護事件佇列也需要成本,再由於NodeJS是單線程,事件佇列越長,得到回應的時間就越長,並發量上去還是會力不從心。

RESTful API是NodeJS最理想的应用场景,可以处理数万条连接,本身没有太多的逻辑,只需要请求API,组织数据进行返回即可。

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('haha')
 })
 new Promise(function(resolve) {
 console.log('6')
 resolve()
 }).then(function() {
 console.log('66')
 })
})
setTimeout(function() {
 console.log('hehe')
}, 0)
new Promise(function(resolve) {
 console.log('7')
 resolve()
}).then(function() {
 console.log('8')
})
setTimeout(function() {
 console.log('9')
 new Promise(function(resolve) {
 console.log('11')
 resolve()
 }).then(function() {
 console.log('12')
 })
})
new Promise(function(resolve) {
 console.log('13')
 resolve()
}).then(function() {
 console.log('14')
})
// node1 : 1,7,13,8,14,2,4,6,hehe,9,11,5,66,12,haha // 结果不稳定
// node2 : 1,7,13,8,14,2,4,6,hehe,5,66,9,11,12,haha // 结果不稳定
// node3 : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha // 结果不稳定
// chrome : 1,7,13,8,14,2,4,6,5,66,hehe,9,11,12,haha


chrome的运行比较稳定,而node环境下运行不稳定,可能会出现两种情况。

chrome运行的结果的原因是Promiseprocess.nextTick()的微任务Event Queue运行的权限比普通宏任务Event Queue权限高,如果取事件队列中的事件的时候有微任务,就先执行微任务队列里的任务,除非该任务在下一轮的Event Loop中,微任务队列清空了之后再执行宏任务队列里的任务。

相关推荐:

Node.js事件循环教程

javascript事件循环之强制梳理

深入理解Node.js 事件循环和回调函数

以上是JS與Node.js中的事件循環詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn