前言
感覺(jué)知識(shí)就像網(wǎng)貸,是個(gè)無(wú)底洞啊,本來(lái)只是在犀牛書上看到定時(shí)器的內(nèi)容,只有一頁(yè)而已,然而我卻花了幾周的時(shí)間來(lái)整理它,不過(guò)真的是學(xué)無(wú)止境,還有很多細(xì)節(jié)無(wú)法深入,大家一起學(xué)習(xí)進(jìn)步吖~
簡(jiǎn)單的栗子
例1:
setTimeout(() => {
console.log('hello world')
}, 0)
function printStr(str) {
console.log(str)
}
printStr('hello Melody')
例2:
let startTime = Date.now();
setTimeout(() => {
console.log(Date.now()-startTime)
}, 100)
for (let i = 0; i < 1000000000; i++) {}
這兩個(gè)問(wèn)題絕大部分人都能答的上來(lái),不過(guò)答案的擴(kuò)展性極強(qiáng),我想面試官問(wèn)你這個(gè)問(wèn)題也只是以此為引子想看看你的基礎(chǔ)是否夠扎實(shí)。
最最基本也要知道以上兩個(gè)問(wèn)題的答案,由此延伸其內(nèi)部原理,包括但不限于:
- js引擎為什么是單線程的?
- 什么叫阻塞?異步是如何解決阻塞問(wèn)題的?
- 線程和進(jìn)程的區(qū)別
- 瀏覽器進(jìn)程
- 什么是任務(wù)隊(duì)列?什么是執(zhí)行棧?
- 事件循壞(Event Loop)
- Microtasks 和 Macrotasks
- HTML5的Web Worker?
......
js引擎為什么是單線程的?
其實(shí)關(guān)于 js引擎為什么是單線程的 這個(gè)問(wèn)題我覺(jué)得就像 地球?yàn)槭裁词菆A的花兒為什么這樣紅 一樣無(wú)聊,但是我還是問(wèn)了這個(gè)無(wú)聊的問(wèn)題,(我就是這么無(wú)聊(* ̄︶ ̄))js誕生之初本就沒(méi)指望他干什么大事,它不走C和Java的“高端”路線,一開(kāi)始就找準(zhǔn)定位--腳本語(yǔ)言,作為腳本語(yǔ)言,它不需要很快的速度,很強(qiáng)的性能,所以本著簡(jiǎn)單易學(xué)的原則,js被設(shè)計(jì)成單線程語(yǔ)言。
還有一個(gè)說(shuō)法是,js會(huì)操作dom,而dom的修改會(huì)觸發(fā)瀏覽器的渲染進(jìn)程去渲染界面,如果js是多線程的,當(dāng)它同時(shí)對(duì)同一個(gè)節(jié)點(diǎn)做了增加和刪除操作,渲染進(jìn)程就不知道該怎么渲染這個(gè)節(jié)點(diǎn)了。關(guān)于這個(gè)原因其實(shí)我覺(jué)得比較牽強(qiáng),Python不就是多線程的,你們咋不說(shuō)它也會(huì)造成這個(gè)問(wèn)題哪?
所以單線程就單線程,它也不見(jiàn)得就比多線程差多少啊。
什么叫阻塞?異步是如何解決阻塞問(wèn)題的?
js里面的阻塞就是成群結(jié)隊(duì)的函數(shù)啊網(wǎng)絡(luò)請(qǐng)求啊排著隊(duì)等js執(zhí)行,執(zhí)行就得花時(shí)間,某個(gè)任務(wù)花的時(shí)間長(zhǎng)了,占用js的時(shí)間多了,就導(dǎo)致后面的任務(wù)等待執(zhí)行的時(shí)間很長(zhǎng),比較壞的情況是,js引擎線程和GUI渲染線程是互斥的,如果js久久執(zhí)行不完,就會(huì)導(dǎo)致窗口一直白屏,甚至瀏覽器認(rèn)為該窗口失去響應(yīng),會(huì)詢問(wèn)用戶是否要關(guān)閉該窗口。
阻塞不是單線程的專利,事實(shí)上無(wú)論是單線程or多線程,當(dāng)并發(fā)量過(guò)高時(shí)都會(huì)造成阻塞,只是單線程更容易阻塞,不需要高并發(fā)量,只需要來(lái)一個(gè)耗時(shí)的I/O讀取操作就可以讓線程無(wú)法繼續(xù)往下處理,只能干等著。所以對(duì)于這類耗時(shí)的操作,沒(méi)必要等待他們執(zhí)行完成,只需要告訴他們:我還有事要先去忙沒(méi)空等你,你結(jié)果返回了再來(lái)通知我,我自會(huì)回去處理,這就是異步和回調(diào)。
因?yàn)楫惒降挠锰帉?shí)在太多,一不小心就是異步接異步,回調(diào)套回調(diào),然后就陷入了“回調(diào)地獄”,不過(guò)近幾年js的回調(diào)已經(jīng)可以處理得非常優(yōu)雅了,包括有:
- Promise對(duì)象
- Generator函數(shù)
- async函數(shù)(Generator函數(shù)的語(yǔ)法糖,用起來(lái)確實(shí)非常順手)
線程和進(jìn)程
我們說(shuō):xxx語(yǔ)言是單(多)線程的,瀏覽器是多進(jìn)程的
我們說(shuō):電腦好卡啊,打開(kāi)任務(wù)管理器看看哪些進(jìn)程占用CPU過(guò)多
所以:進(jìn)程和線程到底是個(gè)啥?
官方解釋進(jìn)程是cpu資源分配的最小單位, 線程是cpu調(diào)度的最小單位
不懂,有沒(méi)有通俗一點(diǎn)的解釋?
CPU是計(jì)算機(jī)的“大腦”,“身體”的各個(gè)機(jī)能運(yùn)轉(zhuǎn)都依靠于“大腦”處理,我們的大腦永遠(yuǎn)在工作,除非我們掛掉了。CPU也會(huì)一直運(yùn)作,除非切斷了電源。但是不管CPU有多忙,它一次只能運(yùn)行一個(gè)任務(wù),或者說(shuō):它在一個(gè)時(shí)間段內(nèi)只會(huì)運(yùn)行一個(gè)進(jìn)程。
比如說(shuō)這一時(shí)刻我開(kāi)啟了一個(gè)瀏覽器窗口,并且正在用鍵盤輸入文字,那么CPU要管理的進(jìn)程就有瀏覽器和鍵盤,當(dāng)然除此之外顯卡啊RAM啊等各種資源的進(jìn)程也都是一直在運(yùn)行的??傊还苡卸嗌賯€(gè)進(jìn)程(任務(wù))要執(zhí)行,都只能排著隊(duì)等待CPU的“臨幸”,還有一點(diǎn)是,CPU“臨幸”某個(gè)進(jìn)程的時(shí)間并不是根據(jù)這個(gè)進(jìn)程執(zhí)行完需要多久來(lái)確定的,而是由CPU自己分配,如果CPU分配的時(shí)間用完了,那么CPU就會(huì)存儲(chǔ)有關(guān)這個(gè)進(jìn)程的執(zhí)行環(huán)境,我們稱之為“執(zhí)行上下文”,等CPU下一次回來(lái)繼續(xù)執(zhí)行該進(jìn)程時(shí),實(shí)際要做的:
- 加載執(zhí)行上下文
- 執(zhí)行進(jìn)程
- 如果CPU分配時(shí)間用完,跳到4,如果進(jìn)程結(jié)束,跳到5
- 保存進(jìn)程的執(zhí)行上下文,等待下一次CPU處理
- 回收該進(jìn)程占用資源,包括其執(zhí)行上下文。
好了,現(xiàn)在再來(lái)理解進(jìn)程是cpu資源分配的最小單位是不是容易多了?計(jì)算機(jī)運(yùn)轉(zhuǎn)的過(guò)程實(shí)際就是CPU在操控各個(gè)進(jìn)程運(yùn)轉(zhuǎn)的過(guò)程,CPU資源有限,能容納的進(jìn)程數(shù)量有限,CPU能力有限,它一次始終只能運(yùn)行一個(gè)進(jìn)程。
而進(jìn)程還很“大”,為了將進(jìn)程細(xì)分,就引入了線程的概念,這就像一段程序代碼由函數(shù)和全局變量組成,進(jìn)程也是由很多個(gè)線程組成的,這些線程共享進(jìn)程的資源,也就是cpu為該進(jìn)程分配的上下文環(huán)境。線程是cpu調(diào)度的最小單位這句話的通俗解釋是cpu將自己的資源給進(jìn)程,但真正使用這些資源的是線程。
瀏覽器進(jìn)程
瀏覽器是多進(jìn)程的,它包括:
- 瀏覽器進(jìn)程(主進(jìn)程):管理瀏覽器的前進(jìn)后退、與用戶交互,同時(shí)負(fù)責(zé)處理所有和磁盤、網(wǎng)絡(luò)的通信,不分析和渲染任何網(wǎng)頁(yè)內(nèi)容。
- 渲染進(jìn)程(瀏覽器內(nèi)核):渲染進(jìn)程是多線程的,它負(fù)責(zé)解析HTML,CSS構(gòu)建DOM樹(shù),負(fù)責(zé)解析js和事件觸發(fā)等等。要注意的是它對(duì)磁盤和網(wǎng)絡(luò)等都沒(méi)有訪問(wèn)權(quán)限,這些都是通過(guò)瀏覽器進(jìn)程去訪問(wèn)的。
- 插件進(jìn)程:專為Flash, Adobe reader這類插件創(chuàng)建的進(jìn)程。
- GPU進(jìn)程:目前絕大部分瀏覽器都有GPU進(jìn)程(GPU就是我們通常說(shuō)的顯卡的芯片)GPU進(jìn)程主要負(fù)責(zé)硬件加速,即提高瀏覽器對(duì)視頻和圖像的渲染體驗(yàn)。
以此我們可以看到瀏覽器做的事情其實(shí)非常多,它的各個(gè)進(jìn)程相互配合,可以實(shí)現(xiàn)瀏覽網(wǎng)頁(yè)、播放視頻、查看pdf, excel等各類文檔(只要安裝了對(duì)應(yīng)插件)、發(fā)起網(wǎng)絡(luò)請(qǐng)求、讀取計(jì)算機(jī)磁盤等等功能。不過(guò)由于瀏覽器內(nèi)核是由不同廠商開(kāi)發(fā),所以瀏覽器之間也有差別,雖然它們都被要求遵守W3C(萬(wàn)維網(wǎng)聯(lián)盟)的規(guī)范,但也僅僅是遵守了部分規(guī)范而已。
渲染進(jìn)程(瀏覽器內(nèi)核)
渲染進(jìn)程是我們最關(guān)注的進(jìn)程,它有多個(gè)線程,主要包括:
- GUI渲染線程:負(fù)責(zé)渲染界面,即解析HTML和CSS構(gòu)建DOM樹(shù)
- JS引擎線程: 負(fù)責(zé)解析JavaScript腳本,處理自己內(nèi)部的任務(wù)(執(zhí)行棧),如果沒(méi)有任務(wù)就去執(zhí)行隊(duì)列的棧頂取任務(wù)執(zhí)行。
- 計(jì)時(shí)器線程:等待延時(shí)時(shí)間到達(dá)就將計(jì)時(shí)器里的事件放到執(zhí)行隊(duì)列中。
- http請(qǐng)求線程:等待網(wǎng)絡(luò)請(qǐng)求返回結(jié)果就將其回調(diào)函數(shù)放到執(zhí)行隊(duì)列中。
-
事件觸發(fā)線程:等待用戶做點(diǎn)擊、按下鼠標(biāo)等操作時(shí)將該事件放到執(zhí)行隊(duì)列中。另外,該線程還負(fù)責(zé)控制事件循環(huán)。
......
現(xiàn)在讓我們從這段簡(jiǎn)單的話里理出幾條重要的信息。
GUI渲染線程和JS引擎線程是互斥的
因?yàn)閖s腳本可以操作DOM樹(shù),所以為了避免GUI渲染和js執(zhí)行在操作DOM時(shí)發(fā)生沖突,它們并不會(huì)一起發(fā)生,當(dāng)GUI渲染過(guò)程遇到<script>標(biāo)簽時(shí)就會(huì)停下來(lái)等待這段js代碼執(zhí)行完再繼續(xù)渲染。js引擎是由js當(dāng)前所在環(huán)境提供的
js可以在瀏覽器里面運(yùn)行是因?yàn)闉g覽器提供了解析js語(yǔ)法的引擎,Node讓js可以運(yùn)行在服務(wù)端是也是因?yàn)镹ode給js提供了引擎,并且,各瀏覽器之間、Node和瀏覽器之間實(shí)現(xiàn)的js引擎都是有差異的。很多事情是瀏覽器做的而不是js引擎做的
我以前一直沒(méi)深究過(guò)單線程的js是如何調(diào)配事件,如何監(jiān)聽(tīng)異步事件的回調(diào)函數(shù)的,現(xiàn)在才知道這些都是瀏覽器在處理,或者說(shuō)是瀏覽器的渲染進(jìn)程在處理,而js引擎要做的就是依次處理執(zhí)行棧中的任務(wù),當(dāng)執(zhí)行棧為空就去執(zhí)行隊(duì)列取出第一個(gè)任務(wù)接著處理。瀏覽器會(huì)給異步任務(wù)開(kāi)辟另外的線程
這里另外的線程就是指計(jì)時(shí)器線程、http請(qǐng)求線程和事件觸發(fā)線程等。而js會(huì)在執(zhí)行棧(同步任務(wù))為空時(shí)才去處理任務(wù)隊(duì)列(異步任務(wù))的任務(wù)。
執(zhí)行棧和任務(wù)隊(duì)列
執(zhí)行棧又叫主線程,任務(wù)隊(duì)列又叫消息隊(duì)列
上面我們已經(jīng)提到了這兩個(gè)概念了,現(xiàn)在讓我們結(jié)合例子和圖來(lái)加深理解。
例3:
setTimeout(() => {
console.log('setTimeout延時(shí)到了')
}, 500)
function excute(a, b) {
let addRes = add(a, b);
console.log(`add result: ${addRes}`)
}
function add(a, b) {
return a+b
}
excute(2,3)
代碼開(kāi)始遇到了setTimeout,由計(jì)時(shí)器線程處理,計(jì)時(shí)器線程會(huì)負(fù)責(zé)計(jì)時(shí),當(dāng)?shù)竭_(dá)setTimeout的延時(shí)時(shí)間即這里的500ms時(shí)會(huì)將其加入到任務(wù)隊(duì)列中等待js執(zhí)行。
接著執(zhí)行excute()函數(shù),excute()是同步函數(shù),所以進(jìn)入執(zhí)行棧(主線程)中:
在執(zhí)行excute()過(guò)程中發(fā)現(xiàn)它內(nèi)部調(diào)用了add()函數(shù),于是將add()函數(shù)也加入執(zhí)行棧中:
add()函數(shù)執(zhí)行完以后將結(jié)果返回并從執(zhí)行棧中彈出:
excute()函數(shù)打印結(jié)果,并從執(zhí)行棧中彈出,此時(shí)執(zhí)行棧中為空,js開(kāi)始去處理任務(wù)隊(duì)列中的任務(wù),假如現(xiàn)在前面的定時(shí)器任務(wù)已經(jīng)加入到任務(wù)隊(duì)列中了:
js會(huì)去任務(wù)隊(duì)列詢問(wèn)是否有待處理事件,如果有就取第一條執(zhí)行,此時(shí)打印出
console.log('setTimeout延時(shí)到了')
。
本篇文章開(kāi)頭的第一個(gè)例子setTimeout的延時(shí)是0,不是延時(shí)0ms執(zhí)行,而是延時(shí)0ms加入到任務(wù)隊(duì)列中,所以它會(huì)在所有的同步任務(wù)執(zhí)行完以后再執(zhí)行。而在第二個(gè)例子中,for (let i = 0; i < 1000000000; i++) {}
是一個(gè)比較耗時(shí)的同步任務(wù),所以setTimeout打印出的時(shí)間差是大于100ms的。(當(dāng)然,即使是一個(gè)耗時(shí)很短的同步任務(wù)也會(huì)導(dǎo)致setTimeout打印出的值大于100,這里只是為了放大同步任務(wù)對(duì)其的影響而已)
事件循環(huán)(Event Loop)
經(jīng)過(guò)前面的分析可以得出以下結(jié)論:
js先順序執(zhí)行執(zhí)行棧中的任務(wù),當(dāng)執(zhí)行棧為空時(shí)再去詢問(wèn)任務(wù)隊(duì)列是否有任務(wù),而任務(wù)隊(duì)列是一個(gè)先進(jìn)先出的機(jī)構(gòu),js引擎始終從任務(wù)隊(duì)列的頂部取任務(wù)執(zhí)行,js引擎從任務(wù)隊(duì)列取事件的過(guò)程是循環(huán)不斷的,所以這個(gè)過(guò)程又被稱為“事件循環(huán)(Event Loop)”
整個(gè)過(guò)程大致是這樣:
但是!但是!不僅僅是這么簡(jiǎn)單,如果僅僅是同步任務(wù)和異步任務(wù)這種區(qū)分方式,那么看下面這個(gè)例子:
例4:
setTimeout(() => {
console.log('定時(shí)器1開(kāi)始了~')
}, 0)
Promise.resolve().then(() => {
console.log('promise1 開(kāi)始了~')
})
setTimeout(() => {
console.log('定時(shí)器2開(kāi)始了~')
Promise.resolve().then(() => {
console.log('promise2 開(kāi)始了~')
})
}, 0)
console.log("---end---");
這段代碼的輸出結(jié)果是什么?js引擎是如何處理不同類型的異步任務(wù)的?
答案是Microtasks 和 Macrotasks。
Microtasks 和 Macrotasks
Macrotasks也稱Tasks,后文我就直接寫Tasks,也方便大家區(qū)分。
我在學(xué)習(xí)這里的時(shí)候就被誤導(dǎo)過(guò),當(dāng)時(shí)以為Tasks和Microtasks是針對(duì)異步任務(wù)而言的,而其實(shí)不是,應(yīng)該說(shuō)這才是區(qū)分任務(wù)最準(zhǔn)確的方式。
- Tasks:所有的同步任務(wù)(執(zhí)行棧)、setTimeout、setInterval等
- Microtasks:Promise、process.nextTick等
一個(gè)簡(jiǎn)單的結(jié)論是:先執(zhí)行Tasks,Tasks執(zhí)行完以后再執(zhí)行Microtasks
當(dāng)我看到這個(gè)結(jié)論時(shí),心中已經(jīng)有了答案,我覺(jué)得例4代碼的打印結(jié)果是:
---end---(因?yàn)檫@是在執(zhí)行棧中的任務(wù),會(huì)最先執(zhí)行)
定時(shí)器1開(kāi)始了~ (setTimeout屬于Tasks,先于Promise執(zhí)行)
定時(shí)器2開(kāi)始了~
promise1 開(kāi)始了~ (Promise屬于Microtasks,會(huì)在Tasks執(zhí)行完以后才開(kāi)始執(zhí)行)
promise2 開(kāi)始了~
然而正確的結(jié)果是:
---end---
promise1 開(kāi)始了~
定時(shí)器1開(kāi)始了~
定時(shí)器2開(kāi)始了~
promise2 開(kāi)始了~
咦?說(shuō)好的Tasks先于Microtasks執(zhí)行呢?怎么反倒是Promise先執(zhí)行了?然后我又仔細(xì)讀了這段話:
js開(kāi)始執(zhí)行Tasks,執(zhí)行過(guò)程中如果遇到Microtasks就將其加入任務(wù)隊(duì)列中,當(dāng)Tasks執(zhí)行完畢以后就去執(zhí)行Microtasks。然后觸發(fā)GUI渲染線程重新渲染界面,當(dāng)GUI渲染完成以后再繼續(xù)下一輪Tasks,如果下一輪又遇到了Microtasks則等這一輪Tasks執(zhí)行完畢以后又繼續(xù)執(zhí)行Microtasks......
所以,準(zhǔn)確的事件循環(huán)應(yīng)該是:Tasks -> Microtasks -> GUI渲染 -> Tasks....
前面的結(jié)論其實(shí)也沒(méi)有問(wèn)題,確實(shí)是先執(zhí)行Tasks,Tasks執(zhí)行完以后再執(zhí)行Microtasks,只是這句話有歧義,先執(zhí)行Tasks 的意思是先執(zhí)行當(dāng)前這一個(gè)Tasks,所以??!并不是說(shuō)Tasks會(huì)先于所有的Microtasks執(zhí)行,而是在每一次的事件循環(huán)過(guò)程中,當(dāng)前的Tasks一定會(huì)先于當(dāng)前的Microtasks執(zhí)行
如果還不明白,再看例4的代碼(一部分):
setTimeout(() => {
console.log('定時(shí)器1開(kāi)始了~')
}, 0)
Promise.resolve().then(() => {
console.log('promise1 開(kāi)始了~')
})
console.log("---end---");
setTimeout
和console.log("---end---")
是兩個(gè)Tasks,Promise
是Microtasks,而setTimeout
和Promise
是異步任務(wù)會(huì)加入到任務(wù)隊(duì)列中等待執(zhí)行,console.log("---end---")
會(huì)直接進(jìn)入主線程(執(zhí)行棧)執(zhí)行,現(xiàn)在重新畫一個(gè)流程圖就應(yīng)該是這樣的:
(畫圖畫到吐血啊~)
執(zhí)行過(guò)程已經(jīng)非常清楚了,每一輪事件循環(huán)只會(huì)執(zhí)行一個(gè)Tasks和多個(gè)Microtasks,而所有的同步任務(wù)一開(kāi)始就在執(zhí)行棧中了,它們的執(zhí)行優(yōu)先級(jí)最高,所以setTimeout
或者setInterval
這類Tasks會(huì)在第二輪以后才被執(zhí)行。
現(xiàn)在再來(lái)看例4的全部代碼:
//代碼塊1
setTimeout(() => {
console.log('定時(shí)器1開(kāi)始了~')
}, 0)
//代碼塊2
Promise.resolve().then(() => {
console.log('promise1 開(kāi)始了~')
})
//代碼塊3
setTimeout(() => {
console.log('定時(shí)器2開(kāi)始了~')
//代碼塊3-1
Promise.resolve().then(() => {
console.log('promise2 開(kāi)始了~')
})
}, 0)
//代碼塊4
console.log("---end---");
根據(jù)前面的分析,第一輪事件循環(huán)包括:
- 主線程里的代碼,屬于Tasks的代碼塊4
- 任務(wù)隊(duì)列里的代碼,屬于Microtasks的代碼塊2
第二輪事件循環(huán)包括:
- 任務(wù)隊(duì)列里面的代碼,屬于Tasks的代碼塊1
第三輪事件循環(huán)包括:
- 任務(wù)隊(duì)列里面的代碼,屬于Tasks的代碼塊3
- 任務(wù)隊(duì)列里的代碼,屬于Microtasks的代碼塊3-1
注意:不同的瀏覽器結(jié)果不一樣,但根據(jù)規(guī)范,這確實(shí)才是正確的結(jié)果。
HTML5的Web Worker
Web Worker是讓js可以模擬多線程工作的技術(shù),即Web Worker里面的任務(wù)不會(huì)阻塞主線程執(zhí)行和GUI渲染,但是,由于我們前面提到的原因,Web Worker是不能處理與DOM相關(guān)的任務(wù)的,具體來(lái)說(shuō),在Web Worker里可以操作的對(duì)象有:
- navigator對(duì)象
- location對(duì)象(只讀)
- XMLHttpRequest對(duì)象
- setTimeout和setInterval方法
- 應(yīng)用緩存
不可操作的對(duì)象有:
- DOM對(duì)象
- Window對(duì)象
- document對(duì)象
因?yàn)槲乙策€沒(méi)用到過(guò)這個(gè)技術(shù),所以就不再展開(kāi)它的詳細(xì)用法了,建議大家閱讀MDN上的使用 Web Workers,講的非常詳細(xì)。
總之呢,Web Worker并沒(méi)有讓js由單線程變成多線程,它只是讓js有了多線程的能力,一般來(lái)說(shuō),會(huì)放在Web Worker里的任務(wù)都是耗時(shí)或計(jì)算量很大的,而大部分時(shí)候我們都不需要js來(lái)做計(jì)算量很大的工作,所以目前用到它的地方還不多,不過(guò)這也只是我的看法~
寫在最后
感覺(jué)寫在最后的話被我寫在前言里面了,所以好像也沒(méi)啥好總結(jié)的了,只是感覺(jué)自己很拖沓,這篇文章前前后后拖了大半個(gè)月,真的是很懶很拖延了~