摘要:的單線程,與它的用途有關(guān)。特點的顯著特點異步機制事件驅(qū)動。隊列的讀取輪詢線程,事件的消費者,的主角。它將不同的任務(wù)分配給不同的線程,形成一個事件循環(huán),以異步的方式將任務(wù)的執(zhí)行結(jié)果返回給引擎。
這兩天跟同事同事討論遇到的一個問題,js中的event loop,引出了chrome與node中運行具有setTimeout和Promise的程序時候執(zhí)行結(jié)果不一樣的問題,從而引出了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執(zhí)行的結(jié)果不一樣,這就很有意思了。
1. JS 中的任務(wù)隊列JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。那么,為什么JavaScript不能有多個線程呢?這樣能提高效率啊。
JavaScript的單線程,與它的用途有關(guān)。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復(fù)雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節(jié)點上添加內(nèi)容,另一個線程刪除了這個節(jié)點,這時瀏覽器應(yīng)該以哪個線程為準(zhǔn)?
所以,為了避免復(fù)雜性,從一誕生,JavaScript就是單線程,這已經(jīng)成了這門語言的核心特征,將來也不會改變。
為了利用多核CPU的計算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標(biāo)準(zhǔn)并沒有改變JavaScript單線程的本質(zhì)。
單線程就意味著,所有任務(wù)需要排隊,前一個任務(wù)結(jié)束,才會執(zhí)行后一個任務(wù)。如果前一個任務(wù)耗時很長,后一個任務(wù)就不得不一直等著。
于是,所有任務(wù)可以分成兩種,一種是同步任務(wù)(synchronous),另一種是異步任務(wù)(asynchronous)。同步任務(wù)指的是,在主線程上排隊執(zhí)行的任務(wù),只有前一個任務(wù)執(zhí)行完畢,才能執(zhí)行后一個任務(wù);異步任務(wù)指的是,不進入主線程、而進入"任務(wù)隊列"(task queue)的任務(wù),只有"任務(wù)隊列"通知主線程,某個異步任務(wù)可以執(zhí)行了,該任務(wù)才會進入主線程執(zhí)行。
具體來說,異步執(zhí)行的運行機制如下。(同步執(zhí)行也是如此,因為它可以被視為沒有異步任務(wù)的異步執(zhí)行。)
所有同步任務(wù)都在主線程上執(zhí)行,形成一個執(zhí)行棧(execution context stack)。
主線程之外,還存在一個"任務(wù)隊列"(task queue)。只要異步任務(wù)有了運行結(jié)果,就在"任務(wù)隊列"之中放置一個事件。
一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會讀取"任務(wù)隊列",看看里面有哪些事件。那些對應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進入執(zhí)行棧,開始執(zhí)行。
主線程不斷重復(fù)上面的第三步。
只要主線程空了,就會去讀取"任務(wù)隊列",這就是JavaScript的運行機制。這個過程會不斷重復(fù)。
3. 定時器 setTimeout與setInterval定時器功能主要由setTimeout()和setInterval()這兩個函數(shù)來完成,它們的內(nèi)部運行機制完全一樣,區(qū)別在于前者指定的代碼是一次性執(zhí)行,后者則為反復(fù)執(zhí)行。
setTimeout(fn,0)的含義是,指定某個任務(wù)在主線程最早可得的空閑時間執(zhí)行,也就是說,盡可能早得執(zhí)行。它在"任務(wù)隊列"的尾部添加一個事件,因此要等到同步任務(wù)和"任務(wù)隊列"現(xiàn)有的事件都處理完,才會得到執(zhí)行。
HTML5標(biāo)準(zhǔn)規(guī)定了setTimeout()的第二個參數(shù)的最小值(最短間隔),不得低于4毫秒,如果低于這個值,就會自動增加。在此之前,老版本的瀏覽器都將最短間隔設(shè)為10毫秒。對于那些DOM的變動(尤其是涉及頁面重新渲染的部分),通常不會立即執(zhí)行,而是每16毫秒執(zhí)行一次。這時使用requestAnimationFrame()的效果要好于setTimeout()。
另外,瀏覽器內(nèi)的計時器可能會因為很多原因而減慢速度:
CPU超載
瀏覽器選項卡處于后臺模式
筆記本電腦使用電池
所有這些都可能將最小延遲提高到300ms甚至1000ms,具體取決于瀏覽器和設(shè)置。參考 Scheduling: setTimeout and setInterval
需要注意的是,setTimeout()只是將事件插入了"任務(wù)隊列",必須等到當(dāng)前代碼(執(zhí)行棧)執(zhí)行完,主線程才會去執(zhí)行它指定的回調(diào)函數(shù)。要是當(dāng)前代碼耗時很長,有可能要等很久,所以并沒有辦法保證,回調(diào)函數(shù)一定會在setTimeout()指定的時間執(zhí)行。
NodeJS的顯著特點:異步機制、事件驅(qū)動。
事件輪詢的整個過程沒有阻塞新用戶的連接,也不需要維護連接?;谶@樣的機制,理論上陸續(xù)有用戶請求連接,NodeJS都可以進行響應(yīng),因此NodeJS能支持比Java、php程序更高的并發(fā)量。
雖然維護事件隊列也需要成本,再由于NodeJS是單線程,事件隊列越長,得到響應(yīng)的時間就越長,并發(fā)量上去還是會力不從心。
RESTful API是NodeJS最理想的應(yīng)用場景,可以處理數(shù)萬條連接,本身沒有太多的邏輯,只需要請求API,組織數(shù)據(jù)進行返回即可。
5. Node.js的Event Loop關(guān)于Nodejs中的事件循環(huán)還有另一篇文章詳細(xì)探討了下,可以參考閱讀。
事件輪詢主要是針對事件隊列進行輪詢,事件生產(chǎn)者將事件排隊放入隊列中,隊列另外一端有一個線程稱為事件消費者會不斷查詢隊列中是否有事件,如果有事件,就立即會執(zhí)行,為了防止執(zhí)行過程中有堵塞操作影響當(dāng)前線程讀取隊列,事件消費者線程會委托一個線程池專門執(zhí)行這些堵塞操作。
Javascript前端和Node.js的機制類似這個事件輪詢模型,有的人認(rèn)為Node.js是單線程,也就是事件消費者是單線程不斷輪詢,如果有堵塞操作怎么辦,不是堵塞了當(dāng)前單線程的執(zhí)行嗎?
其實Node.js底層也有一個線程池,線程池專門用來執(zhí)行各種堵塞操作,這樣不會影響單線程這個主線程進行隊列中事件輪詢和一些任務(wù)執(zhí)行,線程池操作完以后,又會作為事件生產(chǎn)者將操作結(jié)果放入同一個隊列中。
總之,一個事件輪詢Event Loop需要三個組件:
事件隊列Event Queue,屬于FIFO模型,一端推入事件數(shù)據(jù),另外一端拉出事件數(shù)據(jù),兩端只通過這個隊列通訊,屬于一種異步的松耦合。
隊列的讀取輪詢線程,事件的消費者,Event Loop的主角。
多帶帶線程池Thread Pool,專門用來執(zhí)行長任務(wù),重任務(wù),干繁重體力活的。
Node.js也是單線程的Event Loop,但是它的運行機制不同于瀏覽器環(huán)境。
根據(jù)上圖,Node.js的運行機制如下。
V8引擎解析JavaScript腳本。
解析后的代碼,調(diào)用Node API。
libuv庫負(fù)責(zé)Node API的執(zhí)行。它將不同的任務(wù)分配給不同的線程,形成一個Event Loop(事件循環(huán)),以異步的方式將任務(wù)的執(zhí)行結(jié)果返回給V8引擎。
V8引擎再將結(jié)果返回給用戶。
我們可以看到node.js的核心實際上是libuv這個庫。這個庫是c寫的,它可以使用多線程技術(shù),而我們的Javascript應(yīng)用是單線程的。
Nodejs 的異步任務(wù)執(zhí)行流程:
用戶寫的代碼是單線程的,但nodejs內(nèi)部并不是單線程!
事件機制:
Node.js不是用多個線程為每個請求執(zhí)行工作的,相反而是它把所有工作添加到一個事件隊列中,然后有一個多帶帶線程,來循環(huán)提取隊列中的事件。事件循環(huán)線程抓取事件隊列中最上面的條目,執(zhí)行它,然后抓取下一個條目。當(dāng)執(zhí)行長期運行或有阻塞I/O的代碼時,注意這里:它不會被阻塞,會繼續(xù)提取下一個事件,而對于被阻塞的事件Node.js會從線程池中取出一個線程來運行這個被阻塞的代碼,同時把當(dāng)前事件本身和它的回調(diào)事件一同添加到事件隊列(callback嵌套callback)。
在Node.js中,因為只有一個單線程不斷地輪詢隊列中是否有事件,對于數(shù)據(jù)庫文件系統(tǒng)等I/O操作,包括HTTP請求等等這些容易堵塞等待的操作,如果也是在這個單線程中實現(xiàn),肯定會堵塞影響其他工作任務(wù)的執(zhí)行,Javascript/Node.js會委托給底層的線程池執(zhí)行,并會告訴線程池一個回調(diào)函數(shù),這樣單線程繼續(xù)執(zhí)行其他事情,當(dāng)這些堵塞操作完成后,其結(jié)果與提供的回調(diào)函數(shù)一起再放入隊列中,當(dāng)單線程從隊列中不斷讀取事件,讀取到這些堵塞的操作結(jié)果后,會將這些操作結(jié)果作為回調(diào)函數(shù)的輸入?yún)?shù),然后激活運行回調(diào)函數(shù)。
請注意,Node.js的這個單線程不只是負(fù)責(zé)讀取隊列事件,還會執(zhí)行運行回調(diào)函數(shù),這是它區(qū)別于多線程模式的一個主要特點,多線程模式下,單線程只負(fù)責(zé)讀取隊列事件,不再做其他事情,會委托其他線程做其他事情,特別是多核的情況下,一個CPU核負(fù)責(zé)讀取隊列事件,一個CPU核負(fù)責(zé)執(zhí)行激活的任務(wù),這種方式最適合很耗費CPU計算的任務(wù)。反過來,Node..js的執(zhí)行激活任務(wù)也就是回調(diào)函數(shù)中的任務(wù)還是在負(fù)責(zé)輪詢的單線程中執(zhí)行,這就注定了它不能執(zhí)行CPU繁重的任務(wù),比如JSON轉(zhuǎn)換為其他數(shù)據(jù)格式等等,這些任務(wù)會影響事件輪詢的效率。
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 // 結(jié)果不穩(wěn)定 // node2 : 1,10,15,11,16,2,4,7,9,5,8,12,13,14,6 // 結(jié)果不穩(wěn)定 // node3 : 1,10,15,11,16,2,4,7,5,8,9,12,13,14,6 // 結(jié)果不穩(wěn)定 // chrome : 1,10,15,11,16,2,4,7,5,8,9,12,13,14,6
chrome的運行比較穩(wěn)定,而node環(huán)境下運行不穩(wěn)定,可能會出現(xiàn)兩種情況。
chrome運行的結(jié)果的原因是Promise、process.nextTick()的微任務(wù)Event Queue運行的權(quán)限比普通宏任務(wù)Event Queue權(quán)限高,如果取事件隊列中的事件的時候有微任務(wù),就先執(zhí)行微任務(wù)隊列里的任務(wù),除非該任務(wù)在下一輪的Event Loop中,微任務(wù)隊列清空了之后再執(zhí)行宏任務(wù)隊列里的任務(wù)。
關(guān)于Node中的事件循環(huán)和異步API的內(nèi)容,具體可以參見另一篇帖子,有具體討論。
7. 瀏覽器中的事件循環(huán)瀏覽器中和Node中的事件循環(huán)的執(zhí)行順序并不一致,在瀏覽器中,我們可以按性質(zhì)把任務(wù)分為兩類,macrotask(宏任務(wù))和 microtask(微任務(wù))。
macrotask: script (同步代碼), setTimeout, setInterval, setImmediate, MessageChannel, postMessage, I/O, UI渲染
microtask: process.nextTick, Promises(這里指瀏覽器原生實現(xiàn)的 Promise), Object.observe, MutationObserver
執(zhí)行順序:
引擎首先從macrotask queue中取出第一個任務(wù),執(zhí)行完畢后,將microtask queue中的所有任務(wù)取出,按順序全部執(zhí)行;
然后再從macrotask queue中取下一個,執(zhí)行完畢后,再次將microtask queue中的全部取出;
循環(huán)往復(fù),直到兩個queue中的任務(wù)都取完。
網(wǎng)上的帖子大多深淺不一,甚至有些前后矛盾,在下的文章都是學(xué)習(xí)過程中的總結(jié),如果發(fā)現(xiàn)錯誤,歡迎留言指出~
參考:
Node.js的事件輪詢Event Loop原理解釋
JavaScript 運行機制詳解:再談Event Loop
js與Nodejs的單線程和異步--初探
這一次,徹底弄懂 JavaScript 執(zhí)行機制
JavaScript任務(wù)隊列的順序機制(事件循環(huán))
PS:歡迎大家關(guān)注我的公眾號【前端下午茶】,一起加油吧~
另外可以加入「前端下午茶交流群」微信群,長按識別下面二維碼即可加我好友,備注加群,我拉你入群~
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/92026.html
摘要:一句話解釋在事件循環(huán)機制中,有任務(wù)兩個隊列隊列和隊列。設(shè)置任務(wù)為目前運行的任務(wù),并執(zhí)行。應(yīng)該是考慮到了這一點,至少任務(wù)中的任務(wù),是被設(shè)置了在一個事件循環(huán)中的最大調(diào)用次數(shù)的,叫。參考材料理解事件循環(huán) 在Node學(xué)習(xí)過程中,不可避免的需要對事件循環(huán)機制做深入理解,其中Macrotask(大型任務(wù))和Microtask(小型任務(wù))比較令人困惑,在一番google之后,我發(fā)現(xiàn)了幾篇資料能比較好...
摘要:事件觸發(fā)線程主要負(fù)責(zé)將準(zhǔn)備好的事件交給引擎線程執(zhí)行。它將不同的任務(wù)分配給不同的線程,形成一個事件循環(huán),以異步的方式將任務(wù)的執(zhí)行結(jié)果返回給引擎。 Fundebug經(jīng)作者浪里行舟授權(quán)首發(fā),未經(jīng)同意請勿轉(zhuǎn)載。 前言 本文我們將會介紹 JS 實現(xiàn)異步的原理,并且了解了在瀏覽器和 Node 中 Event Loop 其實是不相同的。 一、線程與進程 1. 概念 我們經(jīng)常說 JS 是單線程執(zhí)行的,...
摘要:中的定時器中的模塊包含在一段時間后執(zhí)行代碼的函數(shù),定時器不需要通過導(dǎo)入,因為所有方法都可以在全局范圍內(nèi)模擬瀏覽器,要完全了解何時執(zhí)行定時器功能,最好先閱讀事件循環(huán)。 Node.js中的定時器 Node.js中的Timers模塊包含在一段時間后執(zhí)行代碼的函數(shù),定時器不需要通過require()導(dǎo)入,因為所有方法都可以在全局范圍內(nèi)模擬瀏覽器JavaScript API,要完全了解何時執(zhí)行定...
摘要:異步在中,是在單線程中執(zhí)行的沒錯,但是內(nèi)部完成工作的另有線程池,使用一個主進程和多個線程來模擬異步。在事件循環(huán)中,觀察者會不斷的找到線程池中已經(jīng)完成的請求對象,從中取出回調(diào)函數(shù)和數(shù)據(jù)并執(zhí)行。 1. 介紹 單線程編程會因阻塞I/O導(dǎo)致硬件資源得不到更優(yōu)的使用。多線程編程也因為編程中的死鎖、狀態(tài)同步等問題讓開發(fā)人員頭痛。Node在兩者之間給出了它的解決方案:利用單線程,遠(yuǎn)離多線程死鎖、狀態(tài)...
摘要:檢索新的事件執(zhí)行與相關(guān)的回調(diào)幾乎所有,除了由定時器調(diào)度的一些和將在適當(dāng)?shù)臅r候在這里阻塞。在事件循環(huán)的每次運行之間,檢查它是否在等待任何異步或定時器,如果沒有,則徹底關(guān)閉。 Node.js事件循環(huán)、定時器和process.nextTick() 什么是事件循環(huán)? 事件循環(huán)允許Node.js執(zhí)行非阻塞I/O操作 — 盡管JavaScript是單線程的 — 通過盡可能將操作卸載到系統(tǒng)內(nèi)核。 ...
閱讀 3672·2021-09-07 09:59
閱讀 728·2019-08-29 15:12
閱讀 814·2019-08-29 11:14
閱讀 1320·2019-08-26 13:27
閱讀 2674·2019-08-26 10:38
閱讀 3143·2019-08-23 18:07
閱讀 1284·2019-08-23 14:40
閱讀 1933·2019-08-23 12:38