EventLoop那些事兒

Event Loop

在實(shí)踐的過(guò)程中酣难,你是否遇到過(guò)以下場(chǎng)景谍夭,為什么 setTimeout 會(huì)比 Promise 后執(zhí)行,明明代碼寫在 Promise 之前憨募。這其實(shí)涉及到了 Event Loop 相關(guān)的知識(shí)紧索,我們來(lái)詳細(xì)地了解 Event Loop 相關(guān)知識(shí),知道 JS 異步運(yùn)行代碼的原理菜谣。

進(jìn)程與線程

涉及面試題:進(jìn)程與線程區(qū)別珠漂?JS 單線程帶來(lái)的好處?

相信大家經(jīng)常會(huì)聽到 JS 是單線程執(zhí)行的尾膊,但是你是否疑惑過(guò)什么是線程媳危?

講到線程,那么肯定也得說(shuō)一下進(jìn)程冈敛。本質(zhì)上來(lái)說(shuō)待笑,兩個(gè)名詞都是 CPU 工作時(shí)間片的一個(gè)描述。

進(jìn)程描述了 CPU 在運(yùn)行指令及加載和保存上下文所需的時(shí)間抓谴,放在應(yīng)用上來(lái)說(shuō)就代表了一個(gè)程序暮蹂。線程是進(jìn)程中的更小單位,描述了執(zhí)行一段指令所需的時(shí)間癌压。

把這些概念拿到瀏覽器中來(lái)說(shuō)仰泻,當(dāng)你打開一個(gè) Tab 頁(yè)時(shí),其實(shí)就是創(chuàng)建了一個(gè)進(jìn)程滩届,一個(gè)進(jìn)程中可以有多個(gè)線程集侯,比如渲染線程、JS 引擎線程、HTTP 請(qǐng)求線程等等浅悉。當(dāng)你發(fā)起一個(gè)請(qǐng)求時(shí)趟据,其實(shí)就是創(chuàng)建了一個(gè)線程,當(dāng)請(qǐng)求結(jié)束后术健,該線程可能就會(huì)被銷毀汹碱。

上文說(shuō)到了 JS 引擎線程和渲染線程,大家應(yīng)該都知道荞估,在 JS 運(yùn)行的時(shí)候可能會(huì)阻止 UI 渲染咳促,這說(shuō)明了兩個(gè)線程是互斥的。這其中的原因是因?yàn)?JS 可以修改 DOM勘伺,如果在 JS 執(zhí)行的時(shí)候 UI 線程還在工作跪腹,就可能導(dǎo)致不能安全的渲染 UI。這其實(shí)也是一個(gè)單線程的好處飞醉,得益于 JS 是單線程運(yùn)行的冲茸,可以達(dá)到節(jié)省內(nèi)存,節(jié)約上下文切換時(shí)間缅帘,沒有鎖的問(wèn)題的好處轴术。當(dāng)然前面兩點(diǎn)在服務(wù)端中更容易體現(xiàn),對(duì)于鎖的問(wèn)題钦无,形象的來(lái)說(shuō)就是當(dāng)我讀取一個(gè)數(shù)字 15 的時(shí)候逗栽,同時(shí)有兩個(gè)操作對(duì)數(shù)字進(jìn)行了加減,這時(shí)候結(jié)果就出現(xiàn)了錯(cuò)誤失暂。解決這個(gè)問(wèn)題也不難彼宠,只需要在讀取的時(shí)候加鎖,直到讀取完畢之前都不能進(jìn)行寫入操作弟塞。

執(zhí)行棧

涉及面試題:什么是執(zhí)行棧凭峡?

可以把執(zhí)行棧認(rèn)為是一個(gè)存儲(chǔ)函數(shù)調(diào)用的棧結(jié)構(gòu),遵循先進(jìn)后出的原則决记。

aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC8xMS8xMy8xNjcwZDJkMjBlYWQzMmVj.gif

執(zhí)行椣牒保可視化

當(dāng)開始執(zhí)行 JS 代碼時(shí),首先會(huì)執(zhí)行一個(gè) main 函數(shù)霉涨,然后執(zhí)行我們的代碼按价。根據(jù)先進(jìn)后出的原則,后執(zhí)行的函數(shù)會(huì)先彈出棧笙瑟,在圖中我們也可以發(fā)現(xiàn)楼镐,foo 函數(shù)后執(zhí)行,當(dāng)執(zhí)行完畢后就從棧中彈出了往枷。

平時(shí)在開發(fā)中框产,大家也可以在報(bào)錯(cuò)中找到執(zhí)行棧的痕跡

function foo() {
  throw new Error('error')
}
function bar() {
  foo()
}
bar()

aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC8xMS8xMy8xNjcwYzBlMjE1NDAwOTBj.png

函數(shù)執(zhí)行順序

大家可以在上圖清晰的看到報(bào)錯(cuò)在 foo 函數(shù)凄杯,foo 函數(shù)又是在 bar 函數(shù)中調(diào)用的。

當(dāng)我們使用遞歸的時(shí)候秉宿,因?yàn)闂戒突?纱娣诺暮瘮?shù)是有限制的,一旦存放了過(guò)多的函數(shù)且沒有得到釋放的話描睦,就會(huì)出現(xiàn)爆棧的問(wèn)題

function bar() {
  bar()
}
bar()

aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC8xMS8xMy8xNjcwYzEyOGFjY2U5NzVm.png

爆棧

瀏覽器中的 Event Loop

涉及面試題:異步代碼執(zhí)行順序膊存?解釋一下什么是 Event Loop ?

上一小節(jié)我們講到了什么是執(zhí)行棧忱叭,大家也知道了當(dāng)我們執(zhí)行 JS 代碼的時(shí)候其實(shí)就是往執(zhí)行棧中放入函數(shù)隔崎,那么遇到異步代碼的時(shí)候該怎么辦?其實(shí)當(dāng)遇到異步的代碼時(shí)韵丑,會(huì)被掛起并在需要執(zhí)行的時(shí)候加入到 Task(有多種 Task) 隊(duì)列中爵卒。一旦執(zhí)行棧為空,Event Loop 就會(huì)從 Task 隊(duì)列中拿出需要執(zhí)行的代碼并放入執(zhí)行棧中執(zhí)行撵彻,所以本質(zhì)上來(lái)說(shuō) JS 中的異步還是同步行為钓株。

aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC8xMS8yMy8xNjc0MGZhNGNkOWM2OTM3.png

事件循環(huán)

不同的任務(wù)源會(huì)被分配到不同的 Task 隊(duì)列中,任務(wù)源可以分為 微任務(wù)(microtask) 和 宏任務(wù)(macrotask)陌僵。在 ES6 規(guī)范中享幽,microtask 稱為 jobs,macrotask 稱為 task拾弃。下面來(lái)看以下代碼的執(zhí)行順序:

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end')
}
async1()

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

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })

console.log('script end')
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

注意:新的瀏覽器中不是如上打印的,因?yàn)?await 變快了摆霉,具體內(nèi)容可以往下看

首先先來(lái)解釋下上述代碼的 asyncawait 的執(zhí)行順序豪椿。當(dāng)我們調(diào)用 async1 函數(shù)時(shí),會(huì)馬上輸出 async2 end携栋,并且函數(shù)返回一個(gè) Promise搭盾,接下來(lái)在遇到 await的時(shí)候會(huì)就讓出線程開始執(zhí)行 async1 外的代碼,所以我們完全可以把 await 看成是讓出線程的標(biāo)志婉支。

然后當(dāng)同步代碼全部執(zhí)行完畢以后鸯隅,就會(huì)去執(zhí)行所有的異步代碼,那么又會(huì)回到 await 的位置執(zhí)行返回的 Promiseresolve 函數(shù)向挖,這又會(huì)把 resolve 丟到微任務(wù)隊(duì)列中蝌以,接下來(lái)去執(zhí)行 then 中的回調(diào),當(dāng)兩個(gè) then 中的回調(diào)全部執(zhí)行完畢以后何之,又會(huì)回到 await 的位置處理返回值跟畅,這時(shí)候你可以看成是 Promise.resolve(返回值).then(),然后 await 后的代碼全部被包裹進(jìn)了 then 的回調(diào)中溶推,所以 console.log('async1 end') 會(huì)優(yōu)先執(zhí)行于 setTimeout徊件。

如果你覺得上面這段解釋還是有點(diǎn)繞奸攻,那么我把 async 的這兩個(gè)函數(shù)改造成你一定能理解的代碼

new Promise((resolve, reject) => {
  console.log('async2 end')
  // Promise.resolve() 將代碼插入微任務(wù)隊(duì)列尾部
  // resolve 再次插入微任務(wù)隊(duì)列尾部
  resolve(Promise.resolve())
}).then(() => {
  console.log('async1 end')
})

也就是說(shuō),如果 await 后面跟著 Promise 的話虱痕,async1 end 需要等待三個(gè) tick 才能執(zhí)行到睹耐。那么其實(shí)這個(gè)性能相對(duì)來(lái)說(shuō)還是略慢的,所以 V8 團(tuán)隊(duì)借鑒了 Node 8 中的一個(gè) Bug部翘,在引擎底層將三次 tick 減少到了二次 tick硝训。但是這種做法其實(shí)是違法了規(guī)范的,當(dāng)然規(guī)范也是可以更改的略就,這是 V8 團(tuán)隊(duì)的一個(gè) PR捎迫,目前已被同意這種做法。

所以 Event Loop 執(zhí)行順序如下所示:

  • 首先執(zhí)行同步代碼表牢,這屬于宏任務(wù)
  • 當(dāng)執(zhí)行完所有同步代碼后窄绒,執(zhí)行棧為空,查詢是否有異步代碼需要執(zhí)行
  • 執(zhí)行所有微任務(wù)
  • 當(dāng)執(zhí)行完所有微任務(wù)后崔兴,如有必要會(huì)渲染頁(yè)面
  • 然后開始下一輪 Event Loop彰导,執(zhí)行宏任務(wù)中的異步代碼,也就是 setTimeout 中的回調(diào)函數(shù)

所以以上代碼雖然 setTimeout 寫在 Promise 之前敲茄,但是因?yàn)?Promise 屬于微任務(wù)而 setTimeout 屬于宏任務(wù)位谋,所以會(huì)有以上的打印。

微任務(wù)包括 process.nextTick 堰燎,promise 掏父,MutationObserver,其中 process.nextTick 為 Node 獨(dú)有秆剪。

宏任務(wù)包括 script 赊淑, setTimeoutsetInterval 仅讽,setImmediate 陶缺,I/OUI rendering洁灵。

這里很多人會(huì)有個(gè)誤區(qū)饱岸,認(rèn)為微任務(wù)快于宏任務(wù),其實(shí)是錯(cuò)誤的徽千。因?yàn)楹耆蝿?wù)中包括了 script 苫费,瀏覽器會(huì)先執(zhí)行一個(gè)宏任務(wù),接下來(lái)有異步代碼的話才會(huì)先執(zhí)行微任務(wù)双抽。

Node 中的 Event Loop

涉及面試題:Node 中的 Event Loop 和瀏覽器中的有什么區(qū)別黍衙?process.nexttick 執(zhí)行順序?

Node 中的 Event Loop 和瀏覽器中的是完全不相同的東西荠诬。

Node 的 Event Loop 分為 6 個(gè)階段琅翻,它們會(huì)按照順序反復(fù)運(yùn)行位仁。每當(dāng)進(jìn)入某一個(gè)階段的時(shí)候,都會(huì)從對(duì)應(yīng)的回調(diào)隊(duì)列中取出函數(shù)去執(zhí)行方椎。當(dāng)隊(duì)列為空或者執(zhí)行的回調(diào)函數(shù)數(shù)量到達(dá)系統(tǒng)設(shè)定的閾值聂抢,就會(huì)進(jìn)入下一階段。

aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC8xMS8xMy8xNjcwYzNmZTNmOWE1ZTJi.png

timer

timers 階段會(huì)執(zhí)行 setTimeoutsetInterval 回調(diào)棠众,并且是由 poll 階段控制的琳疏。

同樣,在 Node 中定時(shí)器指定的時(shí)間也不是準(zhǔn)確時(shí)間闸拿,只能是盡快執(zhí)行空盼。

I/O

I/O 階段會(huì)處理一些上一輪循環(huán)中的少數(shù)未執(zhí)行的 I/O 回調(diào)

idle, prepare

idle, prepare 階段內(nèi)部實(shí)現(xiàn),這里就忽略不講了新荤。

poll

poll 是一個(gè)至關(guān)重要的階段揽趾,這一階段中,系統(tǒng)會(huì)做兩件事情

  1. 回到 timer 階段執(zhí)行回調(diào)
  2. 執(zhí)行 I/O 回調(diào)

并且在進(jìn)入該階段時(shí)如果沒有設(shè)定了 timer 的話苛骨,會(huì)發(fā)生以下兩件事情

  • 如果 poll 隊(duì)列不為空篱瞎,會(huì)遍歷回調(diào)隊(duì)列并同步執(zhí)行,直到隊(duì)列為空或者達(dá)到系統(tǒng)限制
  • 如果 poll 隊(duì)列為空時(shí)痒芝,會(huì)有兩件事發(fā)生
    • 如果有 setImmediate 回調(diào)需要執(zhí)行俐筋,poll 階段會(huì)停止并且進(jìn)入到 check 階段執(zhí)行回調(diào)
    • 如果沒有 setImmediate 回調(diào)需要執(zhí)行,會(huì)等待回調(diào)被加入到隊(duì)列中并立即執(zhí)行回調(diào)严衬,這里同樣會(huì)有個(gè)超時(shí)時(shí)間設(shè)置防止一直等待下去

當(dāng)然設(shè)定了 timer 的話且 poll 隊(duì)列為空澄者,則會(huì)判斷是否有 timer 超時(shí),如果有的話會(huì)回到 timer 階段執(zhí)行回調(diào)请琳。

check

check 階段執(zhí)行 setImmediate

close callbacks

close callbacks 階段執(zhí)行 close 事件

在以上的內(nèi)容中粱挡,我們了解了 Node 中的 Event Loop 的執(zhí)行順序,接下來(lái)我們將會(huì)通過(guò)代碼的方式來(lái)深入理解這塊內(nèi)容单起。

首先在有些情況下,定時(shí)器的執(zhí)行順序其實(shí)是隨機(jī)

setTimeout(() => {
    console.log('setTimeout')
}, 0)
setImmediate(() => {
    console.log('setImmediate')
})

對(duì)于以上代碼來(lái)說(shuō)劣坊,setTimeout 可能執(zhí)行在前嘀倒,也可能執(zhí)行在后

  • 首先 setTimeout(fn, 0) === setTimeout(fn, 1),這是由源碼決定的
  • 進(jìn)入事件循環(huán)也是需要成本的局冰,如果在準(zhǔn)備時(shí)候花費(fèi)了大于 1ms 的時(shí)間测蘑,那么在 timer 階段就會(huì)直接執(zhí)行 setTimeout 回調(diào)
  • 那么如果準(zhǔn)備時(shí)間花費(fèi)小于 1ms,那么就是 setImmediate 回調(diào)先執(zhí)行了

當(dāng)然在某些情況下康二,他們的執(zhí)行順序一定是固定的碳胳,比如以下代碼:

const fs = require('fs')

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})

在上述代碼中,setImmediate 永遠(yuǎn)先執(zhí)行沫勿。因?yàn)閮蓚€(gè)代碼寫在 IO 回調(diào)中挨约,IO 回調(diào)是在 poll 階段執(zhí)行味混,當(dāng)回調(diào)執(zhí)行完畢后隊(duì)列為空,發(fā)現(xiàn)存在 setImmediate 回調(diào)诫惭,所以就直接跳轉(zhuǎn)到 check 階段去執(zhí)行回調(diào)了翁锡。

上面介紹的都是 macrotask 的執(zhí)行情況,對(duì)于 microtask 來(lái)說(shuō)夕土,它會(huì)在以上每個(gè)階段完成前清空 microtask 隊(duì)列馆衔,下圖中的 Tick 就代表了 microtask

aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAxOC8xMS8xNC8xNjcxMGZiODBkZDQyZDI3.png
setTimeout(() => {
  console.log('timer21')
}, 0)

Promise.resolve().then(function() {
  console.log('promise1')
})

對(duì)于以上代碼來(lái)說(shuō),其實(shí)和瀏覽器中的輸出是一樣的怨绣,microtask 永遠(yuǎn)執(zhí)行在 macrotask 前面角溃。

最后我們來(lái)講講 Node 中的 process.nextTick,這個(gè)函數(shù)其實(shí)是獨(dú)立于 Event Loop 之外的篮撑,它有一個(gè)自己的隊(duì)列减细,當(dāng)每個(gè)階段完成后,如果存在 nextTick 隊(duì)列咽扇,就會(huì)清空隊(duì)列中的所有回調(diào)函數(shù)邪财,并且優(yōu)先于其他 microtask 執(zhí)行。

setTimeout(() => {
 console.log('timer1')

 Promise.resolve().then(function() {
   console.log('promise1')
 })
}, 0)

process.nextTick(() => {
 console.log('nextTick')
 process.nextTick(() => {
   console.log('nextTick')
   process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
     })
   })
 })
})

對(duì)于以上代碼质欲,大家可以發(fā)現(xiàn)無(wú)論如何树埠,永遠(yuǎn)都是先把 nextTick 全部打印出來(lái)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末嘶伟,一起剝皮案震驚了整個(gè)濱河市怎憋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌九昧,老刑警劉巖绊袋,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異铸鹰,居然都是意外死亡癌别,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門蹋笼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)展姐,“玉大人,你說(shuō)我怎么就攤上這事剖毯』浚” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵逊谋,是天一觀的道長(zhǎng)擂达。 經(jīng)常有香客問(wèn)我,道長(zhǎng)胶滋,這世上最難降的妖魔是什么板鬓? 我笑而不...
    開封第一講書人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任悲敷,我火速辦了婚禮,結(jié)果婚禮上穗熬,老公的妹妹穿的比我還像新娘镀迂。我一直安慰自己,他們只是感情好唤蔗,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開白布探遵。 她就那樣靜靜地躺著,像睡著了一般妓柜。 火紅的嫁衣襯著肌膚如雪箱季。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評(píng)論 1 290
  • 那天棍掐,我揣著相機(jī)與錄音藏雏,去河邊找鬼。 笑死作煌,一個(gè)胖子當(dāng)著我的面吹牛掘殴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播粟誓,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼奏寨,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了鹰服?” 一聲冷哼從身側(cè)響起病瞳,我...
    開封第一講書人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎悲酷,沒想到半個(gè)月后套菜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡设易,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年逗柴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片顿肺。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡戏溺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出挟冠,到底是詐尸還是另有隱情于购,我是刑警寧澤袍睡,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布知染,位于F島的核電站,受9級(jí)特大地震影響斑胜,放射性物質(zhì)發(fā)生泄漏控淡。R本人自食惡果不足惜嫌吠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望掺炭。 院中可真熱鬧辫诅,春花似錦、人聲如沸涧狮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)者冤。三九已至肤视,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間涉枫,已是汗流浹背邢滑。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留愿汰,地道東北人困后。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像衬廷,于是被迫代替她去往敵國(guó)和親摇予。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349