瀏覽器的進(jìn)程模型
何為進(jìn)程?
程序運(yùn)行需要有它自己專屬的內(nèi)存空間
,可以把這塊內(nèi)存空間簡單的理解為進(jìn)程
每個(gè)應(yīng)用至少有一個(gè)進(jìn)程,進(jìn)程之間相互獨(dú)立
,即使要通信,也需要雙方同意。
何為線程?
有了進(jìn)程后,就可以運(yùn)行程序的代碼了。
運(yùn)行代碼的「人」稱之為「線程」。
一個(gè)進(jìn)程至少有一個(gè)線程,所以在進(jìn)程開啟后會自動(dòng)創(chuàng)建一個(gè)線程來運(yùn)行代碼,該線程稱之為主線程。
如果程序需要同時(shí)執(zhí)行多塊代碼,主線程就會啟動(dòng)更多的線程來執(zhí)行代碼,所以一個(gè)進(jìn)程中可以包含多個(gè)線程。
瀏覽器有哪些進(jìn)程和線程?
瀏覽器是一個(gè)多進(jìn)程多線程的應(yīng)用程序
瀏覽器內(nèi)部工作極其復(fù)雜。
為了避免相互影響,為了減少連環(huán)崩潰的幾率,當(dāng)啟動(dòng)瀏覽器后,它會自動(dòng)啟動(dòng)多個(gè)進(jìn)程。
可以在瀏覽器的任務(wù)管理器中查看當(dāng)前的所有進(jìn)程
其中,最主要的進(jìn)程有:
瀏覽器進(jìn)程
主要負(fù)責(zé)界面顯示、用戶交互、子進(jìn)程管理等。瀏覽器進(jìn)程內(nèi)部會啟動(dòng)多個(gè)線程處理不同的任務(wù)。
網(wǎng)絡(luò)進(jìn)程
負(fù)責(zé)加載網(wǎng)絡(luò)資源。網(wǎng)絡(luò)進(jìn)程內(nèi)部會啟動(dòng)多個(gè)線程來處理不同的網(wǎng)絡(luò)任務(wù)。
渲染進(jìn)程(本節(jié)課重點(diǎn)講解的進(jìn)程)
渲染進(jìn)程啟動(dòng)后,會開啟一個(gè)渲染主線程,主線程負(fù)責(zé)執(zhí)行 HTML、CSS、JS 代碼。
默認(rèn)情況下,瀏覽器會為每個(gè)標(biāo)簽頁開啟一個(gè)新的渲染進(jìn)程,以保證不同的標(biāo)簽頁之間不相互影響。
將來該默認(rèn)模式可能會有所改變,有興趣的同學(xué)可參見chrome官方說明文檔
渲染主線程是如何工作的?
渲染主線程是瀏覽器中最繁忙的線程,需要它處理的任務(wù)包括但不限于:
思考題:為什么渲染進(jìn)程不適用多個(gè)線程來處理這些事情?
要處理這么多的任務(wù),主線程遇到了一個(gè)前所未有的難題:如何調(diào)度任務(wù)?
比如:
我正在執(zhí)行一個(gè) JS 函數(shù),執(zhí)行到一半的時(shí)候用戶點(diǎn)擊了按鈕,我該立即去執(zhí)行點(diǎn)擊事件的處理函數(shù)嗎?
我正在執(zhí)行一個(gè) JS 函數(shù),執(zhí)行到一半的時(shí)候某個(gè)計(jì)時(shí)器到達(dá)了時(shí)間,我該立即去執(zhí)行它的回調(diào)嗎?
瀏覽器進(jìn)程通知我“用戶點(diǎn)擊了按鈕”,與此同時(shí),某個(gè)計(jì)時(shí)器也到達(dá)了時(shí)間,我應(yīng)該處理哪一個(gè)呢?
......
渲染主線程想出了一個(gè)絕妙的主意來處理這個(gè)問題:排隊(duì)
在最開始的時(shí)候,渲染主線程會進(jìn)入一個(gè)無限循環(huán)
每一次循環(huán)會檢查消息隊(duì)列
中是否有任務(wù)存在。如果有,就取出第一個(gè)任務(wù)執(zhí)行,執(zhí)行完一個(gè)后進(jìn)入下一次循環(huán);如果沒有,則進(jìn)入休眠狀態(tài)。
其他所有線程(包括其他進(jìn)程的線程)可以隨時(shí)向消息隊(duì)列添加任務(wù)。新任務(wù)會加到消息隊(duì)列的末尾。在添加新任務(wù)時(shí),如果主線程是休眠狀態(tài),則會將其喚醒以繼續(xù)循環(huán)拿取任務(wù)
這樣一來,就可以讓每個(gè)任務(wù)有條不紊的、持續(xù)的進(jìn)行下去了。
整個(gè)過程,被稱之為事件循環(huán)(消息循環(huán))
若干解釋
何為異步?
代碼在執(zhí)行過程中,會遇到一些無法立即處理的任務(wù),比如:
計(jì)時(shí)完成后需要執(zhí)行的任務(wù) —— setTimeout
、setInterval
網(wǎng)絡(luò)通信完成后需要執(zhí)行的任務(wù) -- XHR
、Fetch
用戶操作后需要執(zhí)行的任務(wù) -- addEventListener
如果讓渲染主線程等待這些任務(wù)的時(shí)機(jī)達(dá)到,就會導(dǎo)致主線程長期處于「阻塞」
的狀態(tài),從而導(dǎo)致瀏覽器「卡死」
渲染主線程承擔(dān)著極其重要的工作,無論如何都不能阻塞!
因此,瀏覽器選擇**異步
**來解決這個(gè)問題
使用異步的方式,渲染主線程永不阻塞
面試題:如何理解 JS 的異步?
參考答案:
JS是一門單線程的語言,這是因?yàn)樗\(yùn)行在瀏覽器的渲染主線程中,而渲染主線程只有一個(gè)。
而渲染主線程承擔(dān)著諸多的工作,渲染頁面、執(zhí)行 JS 都在其中運(yùn)行。
如果使用同步的方式,就極有可能導(dǎo)致主線程產(chǎn)生阻塞,從而導(dǎo)致消息隊(duì)列中的很多其他任務(wù)無法得到執(zhí)行。這樣一來,一方面會導(dǎo)致繁忙的主線程白白的消耗時(shí)間,另一方面導(dǎo)致頁面無法及時(shí)更新,給用戶造成卡死現(xiàn)象。
所以瀏覽器采用異步的方式來避免。具體做法是當(dāng)某些任務(wù)發(fā)生時(shí),比如計(jì)時(shí)器、網(wǎng)絡(luò)、事件監(jiān)聽,主線程將任務(wù)交給其他線程去處理,自身立即結(jié)束任務(wù)的執(zhí)行,轉(zhuǎn)而執(zhí)行后續(xù)代碼。當(dāng)其他線程完成時(shí),將事先傳遞的回調(diào)函數(shù)包裝成任務(wù),加入到消息隊(duì)列的末尾排隊(duì),等待主線程調(diào)度執(zhí)行。
在這種異步模式下,瀏覽器永不阻塞,從而最大限度的保證了單線程的流暢運(yùn)行。
JS為何會阻礙渲染?
先看代碼
<h1>Mr.Yuan is awesome!</h1>
<button>change</button>
<script>
var h1 = document.querySelector('h1');
var btn = document.querySelector('button');
// 死循環(huán)指定的時(shí)間
function delay(duration) {
var start = Date.now();
while (Date.now() - start < duration) {}
}
btn.onclick = function () {
h1.textContent = '袁老師很帥!';
delay(3000);
};
</script>
點(diǎn)擊按鈕后,會發(fā)生什么呢?
三秒鐘內(nèi)之后才h1內(nèi)容變更為袁老師很帥!
;為啥不是立即變更呢?因?yàn)?code>h1.textContent的內(nèi)容確實(shí)變更了,但是渲染到頁面需要繪制
,繪制任務(wù)也會被放到隊(duì)列中,但是在繪制之前還有個(gè)delay(3000)
,然后才是繪制。
任務(wù)有優(yōu)先級嗎?
任務(wù)沒有優(yōu)先級,在消息隊(duì)列中先進(jìn)先出
但消息隊(duì)列是有優(yōu)先級的
根據(jù) W3C 的最新解釋:
每個(gè)任務(wù)都有一個(gè)任務(wù)類型
,同一個(gè)類型的任務(wù)必須在一個(gè)隊(duì)列,不同類型的任務(wù)可以分屬于不同的隊(duì)列。 在一次事件循環(huán)中,瀏覽器可以根據(jù)實(shí)際情況從不同的隊(duì)列中取出任務(wù)執(zhí)行。
瀏覽器必須準(zhǔn)備好一個(gè)微隊(duì)列,微隊(duì)列中的任務(wù)優(yōu)先所有其他任務(wù)執(zhí)行 html.spec.whatwg.org/multipage/w…
隨著瀏覽器的復(fù)雜度急劇提升,W3C 不再使用宏隊(duì)列的說法
在目前 chrome 的實(shí)現(xiàn)中,至少包含了下面的隊(duì)列:
延時(shí)隊(duì)列:用于存放計(jì)時(shí)器到達(dá)后的回調(diào)任務(wù),優(yōu)先級「中」
交互隊(duì)列:用于存放用戶操作后產(chǎn)生的事件處理任務(wù),優(yōu)先級「高」
微隊(duì)列:用戶存放需要最快執(zhí)行的任務(wù),優(yōu)先級「最高」
添加任務(wù)到微隊(duì)列的主要方式主要是使用 Promise
、MutationObserver
例如:
// 立即把一個(gè)函數(shù)添加到微隊(duì)列
Promise.resolve().then(函數(shù))
瀏覽器還有很多其他的隊(duì)列,由于和我們開發(fā)關(guān)系不大,不作考慮
面試題:闡述一下 JS 的事件循環(huán)
參考答案:
事件循環(huán)又叫做消息循環(huán),是瀏覽器渲染主線程的工作方式。
在 Chrome 的源碼中,它開啟一個(gè)不會結(jié)束的 for 循環(huán),每次循環(huán)從消息隊(duì)列中取出第一個(gè)任務(wù)執(zhí)行,而其他線程只需要在合適的時(shí)候?qū)⑷蝿?wù)加入到隊(duì)列末尾即可。
過去把消息隊(duì)列簡單分為宏隊(duì)列和微隊(duì)列,這種說法目前已無法滿足復(fù)雜的瀏覽器環(huán)境,取而代之的是一種更加靈活多變的處理方式。
根據(jù) W3C 官方的解釋,每個(gè)任務(wù)有不同的類型,同類型的任務(wù)必須在同一個(gè)隊(duì)列,不同的任務(wù)可以屬于不同的隊(duì)列。不同任務(wù)隊(duì)列有不同的優(yōu)先級,在一次事件循環(huán)中,由瀏覽器自行決定取哪一個(gè)隊(duì)列的任務(wù)。但瀏覽器必須有一個(gè)微隊(duì)列,微隊(duì)列的任務(wù)一定具有最高的優(yōu)先級,必須優(yōu)先調(diào)度執(zhí)行。
面試題:JS 中的計(jì)時(shí)器能做到精確計(jì)時(shí)嗎?為什么?
參考答案:
不行,因?yàn)椋?/p>
計(jì)算機(jī)硬件沒有原子鐘,無法做到精確計(jì)時(shí)
操作系統(tǒng)的計(jì)時(shí)函數(shù)本身就有少量偏差,由于 JS 的計(jì)時(shí)器最終調(diào)用的是操作系統(tǒng)的函數(shù),也就攜帶了這些偏差
按照 W3C 的標(biāo)準(zhǔn),瀏覽器實(shí)現(xiàn)計(jì)時(shí)器時(shí),如果嵌套層級超過 5 層,則會帶有 4 毫秒的最少時(shí)間,這樣在計(jì)時(shí)時(shí)間少于 4 毫秒時(shí)又帶來了偏差
受事件循環(huán)的影響,計(jì)時(shí)器的回調(diào)函數(shù)只能在主線程空閑時(shí)運(yùn)行,因此又帶來了偏差
作者:Geek喜多川海夢
鏈接:https://juejin.cn/post/7350103757402964003
來源:稀土掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。
該文章在 2024/3/25 15:02:54 編輯過