js Event Loop 運(yùn)行機(jī)制
一個(gè)進(jìn)程可以有多個(gè)線程,線程之間可以相互通信
概念
進(jìn)程和線程基本概念
拿出在教科書里的概念:
1、調(diào)度:線程作為調(diào)度和分配的基本單位,進(jìn)程作為擁有資源的基本單位;
2、并發(fā)性:不僅進(jìn)程之間可以并發(fā)執(zhí)行,同一個(gè)進(jìn)程的多個(gè)線程之間也可并發(fā)執(zhí)行;
3、擁有資源:進(jìn)程是擁有資源的一個(gè)獨(dú)立單位,線程不擁有系統(tǒng)資源,但可以訪問隸屬于進(jìn)程的資源;
4、系統(tǒng)開銷:在創(chuàng)建或撤消進(jìn)程時(shí),由于系統(tǒng)都要為之分配和回收資源,導(dǎo)致系統(tǒng)的開銷明顯大于創(chuàng)建或撤消線程時(shí)的開銷。
進(jìn)程和線程的關(guān)系:
一個(gè)線程只能屬于一個(gè)進(jìn)程,而一個(gè)進(jìn)程可以有多個(gè)線程,但至少有一個(gè)線程;
資源分配給進(jìn)程,同一進(jìn)程的所有線程共享該進(jìn)程的所有資源;
處理機(jī)分給線程,即真正在處理機(jī)上運(yùn)行的是線程;
線程在執(zhí)行過程中,需要協(xié)作同步。不同進(jìn)程的線程間要利用消息通信的辦法實(shí)現(xiàn)同步。線程是指進(jìn)程內(nèi)的一個(gè)執(zhí)行單元,也是進(jìn)程內(nèi)的可調(diào)度實(shí)體
瀏覽器內(nèi)核的線程
瀏覽器引擎(進(jìn)程)中包含哪些線程
UI渲染線程
負(fù)責(zé)渲染瀏覽器界面,解析HTML,CSS,構(gòu)建DOM樹和RenderObject樹,布局和繪制等。
當(dāng)界面需要重繪(Repaint)或由于某種操作引發(fā)回流(reflow)時(shí),該線程就會(huì)執(zhí)行
注意:UI渲染線程與JS引擎線程是互斥的,當(dāng)JS引擎執(zhí)行時(shí)GUI線程會(huì)被掛起(相當(dāng)于被凍結(jié)了),UI更新會(huì)被保存在一個(gè)隊(duì)列中等到JS引擎空閑時(shí)立即被執(zhí)行。
js引擎線程(JS解析線程)
也稱為JS內(nèi)核,負(fù)責(zé)處理Javascript腳本程序。(例如V8引擎)
JS引擎線程負(fù)責(zé)解析Javascript腳本,運(yùn)行代碼。
JS引擎一直等待著任務(wù)隊(duì)列中任務(wù)的到來,然后加以處理,一個(gè)Tab頁(renderer進(jìn)程)中無論什么時(shí)候都只有一個(gè)JS線程在運(yùn)行JS程序
同樣注意:UI渲染線程與JS引擎線程是互斥的,所以如果JS執(zhí)行的時(shí)間過長(zhǎng),這樣就會(huì)造成頁面的渲染不連貫,導(dǎo)致頁面渲染加載阻塞。
事件觸發(fā)線程
歸屬于瀏覽器而不是JS引擎,用來控制事件循環(huán)(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開線程協(xié)助)
當(dāng)JS引擎執(zhí)行代碼塊如setTimeOut時(shí)(也可來自瀏覽器內(nèi)核的其他線程,如鼠標(biāo)點(diǎn)擊、AJAX異步請(qǐng)求等),會(huì)將對(duì)應(yīng)任務(wù)添加到事件線程中
當(dāng)對(duì)應(yīng)的事件符合觸發(fā)條件被觸發(fā)時(shí),該線程會(huì)把事件添加到待處理隊(duì)列的隊(duì)尾,等待JS引擎的處理
注意:由于JS的單線程關(guān)系,所以這些待處理隊(duì)列中的事件都得排隊(duì)等待JS引擎處理(當(dāng)JS引擎空閑時(shí)才會(huì)去執(zhí)行)
定時(shí)觸發(fā)器線程
傳說中的setInterval與setTimeout所在線程
瀏覽器定時(shí)計(jì)數(shù)器并不是由JavaScript引擎計(jì)數(shù)的,(因?yàn)镴avaScript引擎是單線程的, 如果處于阻塞線程狀態(tài)就會(huì)影響記計(jì)時(shí)的準(zhǔn)確)
因此通過單獨(dú)線程來計(jì)時(shí)并觸發(fā)定時(shí)(計(jì)時(shí)完畢后,添加到事件隊(duì)列中,等待JS引擎空閑后執(zhí)行)
注意:W3C在HTML標(biāo)準(zhǔn)中規(guī)定,規(guī)定要求setTimeout中低于4ms的時(shí)間間隔算為4ms。
異步http請(qǐng)求線程
在XMLHttpRequest在連接后是通過瀏覽器新開一個(gè)線程請(qǐng)求
將檢測(cè)到狀態(tài)變更時(shí),如果設(shè)置有回調(diào)函數(shù),異步線程就產(chǎn)生狀態(tài)變更事件,將這個(gè)回調(diào)再放入事件隊(duì)列中。再由JavaScript引擎執(zhí)行。
js渲染引擎的Event Loop
以上線程,每個(gè)拿出來都可以詳細(xì)的說上一篇。Event Loop涉及到的JS引擎的一些運(yùn)行機(jī)制的分析。我們可以將這些線程理解為,
一個(gè)主進(jìn)程就是js引擎,其他均為輔助的線程。
主進(jìn)程存在一個(gè)執(zhí)行棧,事件觸發(fā)線程維護(hù)一個(gè)消息隊(duì)列
同步任務(wù)在執(zhí)行棧中執(zhí)行,異步任務(wù)在滿足條件后加入到消息隊(duì)列中,等待執(zhí)行。
先執(zhí)行棧中的任務(wù),執(zhí)行完畢后,檢查隊(duì)列是否為空,不為空,將隊(duì)列中的任務(wù)壓入執(zhí)行棧中執(zhí)行。直到棧和隊(duì)列均為空。
題目
1
setTimeout(function(){
console.log(0)
},500)
setTimeout(function(){
console.log(1)
},1000)
setTimeout(function(){
console.log(2)
},2000)
for(;;){
}
復(fù)制代碼上面這段代碼用于不會(huì)有輸出,同步代碼死循環(huán)阻塞了執(zhí)行棧。雖然定時(shí)后回調(diào)加入執(zhí)行隊(duì)列,但是異永遠(yuǎn)不會(huì)執(zhí)行。
2
setTimeout(function(){
console.log('setTimeout1');
Promise.resolve().then(()=>{
console.log('then1');
});
setTimeout(function(){
console.log('setTimeout3');
},0)
},0)
Promise.resolve().then(()=>{
console.log('then2');
Promise.resolve().then(()=>{
console.log('then3');
})
setTimeout(function(){
console.log('setTimeout2');
},0)
})
答案:then2 then3 setTimeout1 then1 setTimeout2
首先在題目中出現(xiàn)了es6的promise,他的出現(xiàn)讓原來我們理解的事件環(huán)產(chǎn)生了一些不同。
為什么呢?因?yàn)镻romise里有了一個(gè)一個(gè)新的概念:microtask
此時(shí)JS中分為兩種任務(wù)類型:macrotask和microtask,在ECMAScript中,microtask稱為jobs,macrotask可稱為task
微任務(wù)和宏任務(wù)
首先說明,是以瀏覽器為處理環(huán)境下的執(zhí)行邏輯
瀏覽器環(huán)境下的微任務(wù)和宏任務(wù)有哪些
宏任務(wù):setTimeout setImmediate MessageChannel
微任務(wù):Promise.then MutationObserver
記住兩點(diǎn):
微任務(wù)在宏任務(wù)之前的執(zhí)行,先執(zhí)行 執(zhí)行棧中的內(nèi)容 執(zhí)行后 清空微任務(wù)
每次取一個(gè)宏任務(wù) 就去清空微任務(wù),之后再去取宏任務(wù)
然后題目入手分析宏任務(wù)和微任務(wù)的執(zhí)行
解題思路:
setTimeout1放入宏任務(wù)執(zhí)行隊(duì)列中,微任務(wù)then2放入微任務(wù)隊(duì)列中,棧為空,優(yōu)先執(zhí)行微任務(wù),則先執(zhí)行then2。
then2之后執(zhí)行后,接下來存在微任務(wù)then3。將then3放入微任務(wù)隊(duì)列中。
接下來setTimeout2加入到宏任務(wù)隊(duì)列中。
此時(shí)執(zhí)行棧為空,執(zhí)行then3。
微任務(wù)全部執(zhí)行完畢后,執(zhí)行宏任務(wù)setTimeout1,執(zhí)行發(fā)現(xiàn)微任務(wù)then1,放置到微任務(wù)隊(duì)列中。
setTimeout1宏任務(wù)執(zhí)行完,再次清空微任務(wù)隊(duì)列,執(zhí)行then1
微任務(wù)全部執(zhí)行完畢后,執(zhí)行宏任務(wù)setTimeout2。程序結(jié)束。
那么node作為后端服務(wù),單線程有什么利弊?
優(yōu)點(diǎn):
避免頻繁創(chuàng)建、切換進(jìn)程的開銷,使執(zhí)行速度更加迅速。
資源占用小
線程安全,不用擔(dān)心同一變量同時(shí)被多個(gè)線程進(jìn)行讀寫而造成的程序崩潰。
缺點(diǎn):
不適合大量的計(jì)算和壓縮等cpu密集型的操作,會(huì)造成阻塞。
分析一下node下的消息隊(duì)列
為微任務(wù),定時(shí)器,io,setImmidiate分別分配消息隊(duì)列
先檢查定時(shí)器隊(duì)列,如果有內(nèi)容,則全部清空
從時(shí)間隊(duì)列切換到io隊(duì)列的過程中,檢查微任務(wù),如果有則情況微任務(wù)。
io隊(duì)列執(zhí)行完成,如果有check隊(duì)列的內(nèi)容,則執(zhí)行。否則繼續(xù)檢查定時(shí)器隊(duì)列。
完成閉環(huán)
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise');
});
}, 0)
setTimeout(() => {
console.log('timeout2');
}, 0)
復(fù)制代碼瀏覽器下的結(jié)果:timeout1 promise timeout2
node下的結(jié)果:timout1 timeout2 promise
簡(jiǎn)單梳理下瀏覽器渲染流程
瀏覽器的渲染流程(簡(jiǎn)單版本):
瀏覽器輸入url,瀏覽器主進(jìn)程接管,開一個(gè)下載線程,
然后進(jìn)行 http請(qǐng)求(略去DNS查詢,IP尋址等等操作),然后等待響應(yīng),獲取內(nèi)容,
隨后將內(nèi)容通過RendererHost接口轉(zhuǎn)交給Renderer進(jìn)程
瀏覽器渲染流程開始
渲染大概可以劃分成以下幾個(gè)步驟:
解析html建立dom樹
解析css構(gòu)建render樹(將CSS代碼解析成樹形的數(shù)據(jù)結(jié)構(gòu),然后結(jié)合DOM合并成render樹)
布局render樹(Layout/reflow),負(fù)責(zé)各元素尺寸、位置的計(jì)算
繪制render樹(paint),繪制頁面像素信息
瀏覽器會(huì)將各層的信息發(fā)送給GPU,GPU會(huì)將各層合成(composite),顯示在屏幕上。
所有詳細(xì)步驟都已經(jīng)略去,渲染完畢后就是load事件了,之后就是自己的JS邏輯處理了
染完畢后會(huì)觸發(fā)load事件,那么你能分清楚load事件與DOMContentLoaded事件的先后么?
很簡(jiǎn)單,知道它們的定義就可以了:
當(dāng) DOMContentLoaded 事件觸發(fā)時(shí),僅當(dāng)DOM加載完成,不包括樣式表,圖片。 (譬如如果有async加載的腳本就不一定完成)
當(dāng) onload 事件觸發(fā)時(shí),頁面上所有的DOM,樣式表,腳本,圖片都已經(jīng)加載完成了。 (渲染完畢了)
所以,順序是:DOMContentLoaded -> load
css加載是否會(huì)阻塞dom樹渲染?
這里說的是頭部引入css的情況
首先,我們都知道:css是由單獨(dú)的下載線程異步下載的。
然后再說下幾個(gè)現(xiàn)象:
css加載不會(huì)阻塞DOM樹解析(異步加載時(shí)DOM照常構(gòu)建)
但會(huì)阻塞render樹渲染(渲染時(shí)需等css加載完畢,因?yàn)閞ender樹需要css信息)
這可能也是瀏覽器的一種優(yōu)化機(jī)制。
因?yàn)槟慵虞dcss的時(shí)候,可能會(huì)修改下面DOM節(jié)點(diǎn)的樣式, 如果css加載不阻塞render樹渲染的話,那么當(dāng)css加載完之后, render樹可能又得重新重繪或者回流了,這就造成了一些沒有必要的損耗。 所以干脆就先把DOM樹的結(jié)構(gòu)先解析完,把可以做的工作做完,然后等你css加載完之后, 在根據(jù)最終的樣式來渲染render樹,這種做法性能方面確實(shí)會(huì)比較好一點(diǎn)。
JS分為同步任務(wù)和異步任務(wù)
同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧
主線程之外,事件觸發(fā)線程管理著一個(gè)任務(wù)隊(duì)列,只要異步任務(wù)有了運(yùn)行結(jié)果,就在任務(wù)隊(duì)列之中放置一個(gè)事件。
一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢(此時(shí)JS引擎空閑),系統(tǒng)就會(huì)讀取任務(wù)隊(duì)列,將可運(yùn)行的異步任務(wù)添加到可執(zhí)行棧中,開始執(zhí)行。