01_原理-事件循環
文章目錄
- 01_原理-事件循環
- 一、瀏覽器的進程模型
- ①:何為進程?
- ②:何為線程?
- ③:瀏覽器有哪些進程和線程?
- 二、渲染主線程是如何工作的?
- 三、若干解釋
- ①:何為異步?
- ②:JS為何會阻礙渲染?
- ③:任務有優先級嗎?
- ④:js優先級案例
一、瀏覽器的進程模型
①:何為進程?
程序運行需要有它自己專屬的內存空間,可以把這塊內存空間簡單的理解為進程
②:何為線程?
有了進程后,就可以運行程序的代碼了。
運行代碼的「人」稱之為「線程」。
一個進程至少有一個線程,所以在進程開啟后會自動創建一個線程來運行代碼,該線程稱之為主線程。
如果程序需要同時執行多塊代碼,主線程就會啟動更多的線程來執行代碼,所以一個進程中可以包含多個線程。
③:瀏覽器有哪些進程和線程?
瀏覽器是一個多進程多線程的應用程序
瀏覽器內部工作極其復雜。
為了避免相互影響,為了減少連環崩潰的幾率,當啟動瀏覽器后,它會自動啟動多個進程。
1.可以在瀏覽器的任務管理器中查看當前的所有進程
其中,最主要的進程有:
1.瀏覽器進程
主要負責界面顯示、用戶交互、子進程管理等。刻覽器進程內部會啟動多個線程處理不同的任務。
2.網絡進程
負責加載網絡資源。網絡進程內部會啟動多個線程來處理不同的網絡任務。
3.渲染進程(本節課重點講解的進程)
渲染進程啟動后,會開啟一個渲染主線程,主線程負責執行HTML、CSS、JS代碼。
默認情況下,瀏覽器會為每個標簽頁開啟一個新的渲染進程,以保證不同的標簽頁之間不相互影響。
將來該默認模式可能會有所改變,有興趣的同學可參見chrome官方說明文檔
二、渲染主線程是如何工作的?
渲染主線程是瀏覽器中最繁忙的線程,需要它處理的任務包括但不限于:
- 解析HTML
- 解析cSS
- 計算樣式
- 布局
- 處理圖層
- 每秒把頁面畫60次
- 執行全局JS代碼
- 執行事件處理函數
- 執行計時器的回調函數
- 等·······
思考題:為什么渲染進程不適用多個線程來處理這些事情?
要處理這么多的任務,主線程遇到了一個前所未有的難題:如何調度任務?
比如:
-
我正在執行一個JS函數,執行到一半的時候用戶點擊了按鈕,我該立即去執行點擊事件的處理函數嗎?
-
我正在執行一個JS函數,執行到一半的時候某個計時器到達了時間,我該立即去執行它的回調嗎?
-
瀏覽器進程通知我“用戶點擊了按鈕”,與此同時,某個計時器也到達了時間,我應該處理哪一個呢?
渲染主線程想出了一個絕妙的主意來處理這個問題:排隊
- 在最開始的時候,渲染主線程會進入一個無限循環
- 每一次循環會檢查消息隊列中是否有任務存在。如果有,就取出第一個任務執行,執行完一個后進入下一次循環;如果沒有,則進入休眠狀態。
- 其他所有線程(包括其他進程的線程)可以隨時向消息隊列添加任務。新任務會加到消息隊列的未尾。在添加新任務時,如果主線程是休眠狀態,則會將其喚醒以繼續循環拿取任務
這樣一來,就可以讓每個任務有條不紊的、持續的進行下去了。
三、若干解釋
①:何為異步?
代碼在執行過程中,會遇到一些無法立即處理的任務,比如:
- 計時完成后需要執行的任務 –
setTimeout setInterval
- 網絡通信完成后需要執行的任務 –
XHR
、Fetch
- 用戶操作后需要執行的任務 –
addEventListener
如果讓渲染主線程等待這些任務的時機達到,就會導致主線程長期處于「阻塞」的狀態,從而導致瀏覽器「卡死」
渲染主線程承擔著極其重要的工作,無論如何都不能阻塞!
使用異步的方式,渲染主線程永不阻塞
面試題:如何理解JS的異步?
參考答案:
JS是一門單線程的語言,這是因為它運行在瀏覽器的渲染主線程中,而渲染主線程只有一個。
而渲染主線程承擔著諸多的工作,渲染頁面、執行JS都在其中運行
如果使用同步的方式,就極有可能導致主線程產生阻塞,從而導致消息隊列中的很多其他任務無法得到執行。
這樣一來,一方面會導致繁忙的主線程白白的消耗時間,另一方面導致頁面無法及時更新,給用戶造成卡死現
象。
所以瀏覽器采用異步的方式來避免。具體做法是當某些任務發生時,比如計時器、網絡、事件監聽,主線程將
任務交給其他線程去處理,自身立即結束任務的執行,轉而執行后續代碼。當其他線程完成時,將事先傳遞的
回調函數包裝成任務,加入到消息隊列的末尾排隊,等待主線程調度執行
在這種異步模式下,瀏覽器永不阻塞,從而最大限度的保證了單線程的流暢運行
②:JS為何會阻礙渲染?
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>
<h1>Coke</h1>
<button>按鈕</button>
<script>
var h1 = document.querySelector('h1');
var btn = document.querySelector('button');// 死循環指定的時間
function delay(duration) {var start = Date.now();while (Date.now() - start < duration) {}
}btn.onclick = function () {h1.textContent = 'Coke很帥!'delay(3000)
};
</script>
</body>
</html>
③:任務有優先級嗎?
任務沒有優先級,在消息隊列中先進先出
但消息隊列是有優先級的
根據W3C的最新解釋:
-
每個任務都有一個任務類型,同一個類型的任務必須在一個隊列,不同類型的任務可以分屬于不同的隊列。在一次事件循環中,瀏覽器可以根據實際情況從不同的隊列中取出任務執行。
-
瀏覽器必須準備好一個微隊列,微隊列中的任務優先所有其他任務執行
https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint
隨著瀏覽器的復雜度急劇提升,W3C不再使用宏隊列的說法
在目前chrome的實現中,至少包含了下面的隊列:
- 延時隊列:用于存放計時器到達后的回調任務,優先級「中」
- 交互隊列:用于存放用戶操作后產生的事件處理任務,優先級「高」
- 微隊列:用戶存放需要最快執行的任務,優先級「最高」
添加任務到微隊列的主要方式主要是使用Promise、MutationObserver
例如:
//立即把一個函數添加到微隊列
Promise.resolve().then(函數)
瀏覽器還有很多其他的隊列,由于和我們開發關系不大,不作考慮
面試題:闡述一下JS的事件循環
參考答案:
事件循環又叫做消息循環,是瀏覽器渲染主線程的工作方式。
在Chrome的源碼中,它開啟一個不會結束的for循環,每次循環從消息隊列中取出第一個任務執行,而其
他線程只需要在合適的時候將任務加入到隊列未尾即可。
過去把消息隊列簡單分為宏隊列和微隊列,這種說法目前已無法滿足復雜的瀏覽器環境,取而代之的是一種更
加靈活多變的處理方式。
根據W3C官方的解釋,每個任務有不同的類型,同類型的任務必須在同一個隊列,不同的任務可以屬于不同的
隊列。不同任務隊列有不同的優先級,在一次事件循環中,由瀏覽器自行決定取哪一個隊列的任務。但瀏覽器
必須有一個微隊列,微隊列的任務一定具有最高的優先級,必須優先調度執行。
面試題:JS中的計時器能做到精確計時嗎?為什么?
參考答案:
不行,因為:
1.計算機硬件沒有原子鐘,無法做到精確計時ǐ
2.操作系統的計時函數本身就有少量偏差,由于JS的計時器最終調用的是操作系統的函數,也就攜帶了這些偏差
3.按照W3C的標準,瀏覽器實現計時器時,如果嵌套層級超過5層,則會帶有4毫秒的最少時間,這樣在計時時間少于4毫秒時又帶來了偏差
4.受事件循環的影響,計時器的回調函數只能在主線程空閑時運行,因此又帶來了偏差
④:js優先級案例
1.案例1
setTimeout(function (){console.log(1)
}, 0)console.log(2)
2.案例2
// 死循環指定的時間
function delay(duration){var start = Date.now();while(Date.now() - start < duration){ }
}setTimeout(function (){console.log(1)
},0)delay(1000)console.log(2)
3.案例3
setTimeout(function (){console.log(1)
},0)// 將改任務放到微隊列中(微隊列:優先級最高)
Promise.resolve().then(function(){console.log(3)
})console.log(2)