您好,登錄后才能下訂單哦!
這兩天跟同事同事討論遇到的一個問題,js中的event loop
,引出了chrome與node中運行具有setTimeout
和Promise
的程序時候執行結果不一樣的問題,從而引出了Nodejs的event loop
機制,記錄一下,感覺還是蠻有收獲的
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. 定時器 setTimeout
與setInterval
定時器功能主要由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運行的結果的原因是Promise
、process.nextTick()
的微任務Event Queue運行的權限比普通宏任務Event Queue權限高,如果取事件隊列中的事件的時候有微任務,就先執行微任務隊列里的任務,除非該任務在下一輪的Event Loop中,微任務隊列清空了之后再執行宏任務隊列里的任務。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。