目錄
事件循環:引入
一、瀏覽器的進程模型
1.1、什么是進程(Process)
1.2、什么是線程(Thread)
1.3、進程與線程之間的關系聯系與區別
二、瀏覽器有哪些進程和線程
2.1、瀏覽器的主要進程
①瀏覽器進程
②網絡進程
③渲染進程
2.2、渲染主線程的工作原理
渲染主線程的消息隊列
三、事件循環
3.1、什么是事件循環(Event Loop)
3.2、渲染主線程的事件循環如何確定任務的優先級??
3.3、事件循環的執行示例
示例一
示例二
測試題:如下代碼塊執行后輸出順序是什么?
四、相關問題
4.1、為什么要使用事件循環
4.2、如何理解JS的異步
4.3、JS中計時器能精確計時嗎,為什么?
五、總結與相關資源
事件循環(消息循環):引入
????????事件循環是瀏覽器的核心內容。
????????與計時器、Promise、ajax、node等技術有關。
????????要想說清楚事件循環,必須先聊進程與線程。
一、瀏覽器的進程模型
1.1、什么是進程(Process)
? ? ? ? 我們先看看定義:?
- 進程是程序的執行實例。它是操作系統進行資源分配和調度的一個獨立單位。
- 進程擁有獨立的內存空間,可以擁有或分配不同的資源如CPU時間、文件、消息隊列等。
- 進程可以創建子進程,形成進程樹結構。
? ? ? ? 對于coder來說,說到實例肯定不陌生,一個程序的運行就至少需要產生一個實例,實例負責給程勛運行提供運行所需的資源。
????????簡單的說,程序運行需要它專屬的內存空間(RAM和虛擬內存),這部分內存空間可以簡單的理解為該程序對應的進程。
????????每個應用至少有一個進程,且相互獨立,即使要通信,也要雙方同意。
1.2、什么是線程(Thread)
? ? ? ? 先看定義:
- 線程是程序執行的邏輯單元,是程序中一個單一的順序控制流程。
- 在一個進程中可以包含多個線程,它們共享進程的資源,如內存空間,但每個線程有自己的線程棧和程序計數器。
? ? ? ? 簡單的說,線程是進程的執行者。一個進程可以有多個線程,線程之間資源共享, 通信簡單,獨立執行,開銷較小。
????????一個進程至少有一個線程,所以進程開啟后就會自動創建一個線程來運行代碼,該線程稱之為主線程。
????????如果程序需要同時執行多塊代碼,主線程就會啟動更多的線程來執行代碼,所以一個進程中可以包含多個線程。重要的事情要多次重復,這些線程資源共享, 通信簡單,獨立執行,開銷較小(線程相比于進程)!
1.3、進程與線程之間的關系聯系與區別
? ? ? ? 綜上所述,二者之間的聯系與區別就很明確了:
- 進程是程序某一部分或整體的運行實例,每個程序運行都至少需要一個進程。(但不一定只有一個,為了保證程序的穩定性,往往會有多個進程,一個進程崩潰不會導致整個程序崩潰)
- 線程是進程的執行者,每個進程都至少包含一個線程(即主線程)。
- 線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中。因此同一進程內的線程可以共享進程的資源,如全局變量、文件句柄等。
- 與創建新進程相比,創建線程的開銷較小,因為線程可以復用進程的資源。
- ?由于線程共享同一地址空間,線程間的通信更簡單,不需要復雜的進程間通信機制。
????????關系示意圖:
二、瀏覽器有哪些進程和線程
????????首先,瀏覽器是一個多進程多線程的應用程序。
2.1、瀏覽器的主要進程
????????瀏覽器內部工作極其復雜,為了避免相互影響,為了減少連環崩潰的幾率,當啟動瀏覽器后,它會自動啟動多個進程。一般主要有瀏覽器進程、網絡進程、渲染進程。
????????可以在瀏覽器的任務管理器中查看當前的所有進程。
①瀏覽器進程
????????主要負責界?顯示、?戶交互、子進程管理等。瀏覽器進程內部會啟動多個線程處理不同的任務。
②網絡進程
????????負責加載網絡資源。網絡進程內部會啟動多個線程來處理不同的?絡任務。
③渲染進程
????????渲染進程啟動后,會開啟?個渲染主線程,主線程負責執行 HTML、CSS、JS 代碼。
????????默認情況下,瀏覽器會為每個標簽頁開啟?個新的渲染進程,以保證不同的標簽頁之間不相互影響。
2.2、渲染主線程的工作原理
????????渲染主線程是瀏覽器中最繁忙的線程,也是前端開發中提高運行效率需要著重關注的線程,需要它處理的任務包括不限于:解析HTML、解析CSS、計算樣式、布局、處理圖層、每秒60次渲染,執行全局JS代碼、執行事件處理函數、執行計時器回調函數等。
? ? ? ? 那渲染主線程如何執行和調度這些任務呢?總要有個章法去有序執行這些步驟,同時兼顧這些步驟的因果順序和中途插入的步驟。
????????比如任務之間存在因果順序:不解析HTML、CSS,就沒辦法執行布局任務。
? ? ? ? 又比如任務之間會有插入情況:執行JS函數的過程中,用戶點擊了某個按鈕或者計時器到了時間需要執行回調函數。
? ? ? ? 這里就引入了一個概念:
? ? ? ? 渲染主線程的消息隊列
- 在最開始的時候,渲染主線程會進入一個無限循環
- 每一次循環會檢查消息隊列中是否有任務存在。如果有,就取出第一個任務執行,執行完一個后進入下一次循環;如果沒有,則進入休眠狀態。
- 其他所有線程(包括其他進程的線程)可以隨時向消息隊列添加任務。新任務會加到消息隊列的末尾。在添加新任務時,如果主線程是休眠狀態,則會將其喚醒以繼續循環拿取任務
? ? ? ? 簡而言之,渲染主線程的消息隊列就是渲染主線程的任務管家,負責給渲染主線程要執行的任務進行排序、管理、調度。渲染主線程只需要一直檢查消息隊列里面有沒有任務,按序執行即可,但消息隊列要考慮的可就多了(bushi)
? ? ? ? 這樣一來,就可以讓每個任務有條不紊的、持續的進行下去了。現在就能引出本文的核心內容:事件循環。
三、事件循環
3.1、什么是事件循環(Event Loop)
????????又稱消息循環(Message Loop),在有些情景也叫 Run Loop。
????????一言以蔽之:事件循環就是渲染主線程不斷循環不斷從消息隊列中讀取事件并執行的過程。
? ? ? ? 也可以說:事件循環又叫做消息循環,是瀏覽器渲染主線程的工作方式。(但并非只有瀏覽器的渲染主線程會進行事件循環,有時候網絡線程也會)
? ? ? ? 不是所有的線程都有事件循環,但是渲染主線程一般都有。
3.2、渲染主線程的事件循環如何確定任務的優先級??
? ? ? ? 首先,任務本身沒有優先級,消息隊列遵守先進先出的規則。
? ? ? ? 但是消息隊列有優先級。消息隊列一般至少由三個隊列:微隊列、交互隊列、延時隊列構成,其分類和優先級規則如下:
? ? ? ? 微隊列 > 交互隊列 > 延時隊列
- 微隊列:用戶存放需要最快執行的任務(一般由Promise和MutationObserver生成),優先級「最高」
- 交互隊列:用于存放用戶操作后產生的事件處理任務,優先級「高」
- 延時隊列:用于存放計時器到達后的回調任務,優先級「中」
????????這里的優先級是指事件循環過程中,高優先級的隊列會“插隊”放入隊列。比如現在隊列中微隊列和延時隊列各有一個事件,先讀取微隊列中的任務,執行后又產生了一個微隊列任務和一個交互隊列任務,那么下一個執行的是新產生的微隊列任務,然后是新產生的交互隊列任務,最后才是一開始的延時隊列任務。
????????如下圖所示,消息隊列大概是個這樣的模型,只有微隊列完全空掉才會執行交互隊列中的任務,在同一類型的隊列中才嚴格遵守“先進先出”的隊列規則:
3.3、事件循環的執行示例
????????請問如下幾個例子的輸出順序是什么?
示例一
setTimeout(function () {console.log(1);
}, 0);function delay(duration) {var start = Date.now();while (Date.now() - start < duration) {}
}
delay(3000);
console.log(2);// 輸出順序為 2 、 1
(點擊代碼詳情查看答案)
? ? ? ? 解析:整體先作為一個任務①順序執行。setTimeout生成一個新任務②,放到延時隊列中(雖然計時為0,但是任務①還沒執行完畢,所以哪怕計時到了也只能在隊列等候執行)。delay函數將渲染主線程阻塞3秒,然后輸出2,任務①執行完畢,通過事件循環執行任務②,輸出1。
示例二
function a() {console.log(1);Promise.resolve().then(function () {console.log(2);});
}
setTimeout(function () {console.log(3);
}, 0);Promise.resolve().then(a);console.log(5);// 輸出順序為: 5 、 1 、 2 、 3
(點擊代碼詳情查看答案)
????????解析:整體作為任務①執行。setTimeout生成一個新任務②,放到延時隊列中。Promise生成一個新任務③(執行a函數),放到微隊列中。然后輸出5,任務①執行完畢。
? ? ? ? 此時消息隊列中微隊列有任務③,優先執行,先輸出1,然后Promise生成一個新任務④,放到微隊列中,任務③執行完畢。
? ? ? ? 此時微隊列又有任務④,優先執行,輸出2。任務④執行完畢。
? ? ? ? 此時消息隊列中微隊列和交互隊列為空,執行延時隊列中的任務②,輸出3,任務②執行完畢。
? ? ? ? 即輸出結果為:5 1 2 3。
測試題:如下代碼塊執行后輸出順序是什么?
function a() {console.log(1);Promise.resolve().then(function () {console.log(2);});
}setTimeout(function () {console.log(3);Promise.resolve().then(a)
}, 0);Promise.resolve().then(function(){console.log(4);
});console.log(5);// 輸出順序為: 5 、 4 、 3 、 1 、 2
四、相關問題
4.1、為什么要使用事件循環
????????在本文2.2中提到“讓每個任務有條不紊的、持續的進行下去”。那么為什么不使用事件循環就會出現問題?為什么“執行JS函數的過程中,用戶點擊了某個按鈕或者計時器到了時間需要執行回調函數”就會有矛盾?這兩個任務又沒有因果關系,直接一起執行不行嗎?
? ? ? ? 事實上,JS是一門單線程語言,這是因為它運行在瀏覽器的渲染主線程中,而渲染主線程只有一個。也就是說,JS函數不能多個一起進行,哪怕兩個任務相互獨立,也要有個規定來調度任務,有序執行。所以必須要有一個像事件循環一樣的邏輯來管理、調度任務。
4.2、如何理解JS的異步
????????代碼在執行過程中,會遇到一些無法立即處理的任務,比如:
- 計時完成后需要執行的任務 —— setTimeout、setInterval
- 網絡通信完成后需要執行的任務 —— XHR、Fetch
- 用戶操作后需要執行的任務 —— addEventListener
????????如果讓渲染主線程等待這些任務的時機達到,就會導致渲染主線程長期處于“阻塞”的狀態,從而讓用戶感覺瀏覽器“卡死”,讓用戶的體驗變差。
????????因此,瀏覽器使用異步來解決這個問題。
????????具體做法是當某些任務發生時,比如計時器、網絡、事件監聽,主線程將任務交給其他線程去處理,自身立即結束任務的執行,轉而執行后續代碼。當其他線程完成時,將事先傳遞的回調函數包裝成任務,加入到消息隊列的末尾排隊,等待主線程調度執行。
????????從而最大程度的保證單線程的流暢運行。
4.3、JS中計時器能精確計時嗎,為什么?
? ? ? ? 不可以。原因如下:
? ? ? ? 從硬件角度來說:JS計時器是調用了操作系統中的計時函數,該函數本身就有少量偏差,硬件精度有限。
? ? ? ? 從語法標準上說:W3C標準中建議瀏覽器的計時器嵌套層級超過5層,則存在至少4ms的最少事件,這樣也會帶來偏差。
// 例如嵌套的層數小于等于5層,那么就會按照設置的時間執行。
setTimeout(function () {setTimeout(function () {setTimeout(function () {setTimeout(function () {setTimeout(function () {}, 0);}, 0);}, 0);}, 0);
}, 0);// 假如嵌套的層數大于5層,即使設置了0毫秒的間隔,瀏覽器也會確保至少有4毫秒的延遲,以避免潛在的性能問題,即:setTimeout(function () {setTimeout(function () {setTimeout(function () {setTimeout(function () {setTimeout(function () {setTimeout(function () {}, 0);}, 0);}, 0);}, 0);}, 0);}, 0);// 實際執行效果:setTimeout(function () {setTimeout(function () {setTimeout(function () {setTimeout(function () {setTimeout(function () {setTimeout(function () {}, 4);}, 4);}, 4);}, 4);}, 4);}, 4);
? ? ? ? 從事件循環的邏輯上講,計時器的回調函數只能在主線程空閑時進行,并不一定能在計時完成后立馬開始執行邏輯。
? ? ? ? 綜上所述,JS中計時器做不到精確計時。
五、總結與相關資源
? ? ? ? 度一教育的袁進老師談到他的理解:單線程是異步產生的原因,事件循環是異步的實現方式。
? ? ? ? 本質是因為渲染進程因為計算機圖形學的限制,只能是單線程。所以需要“異步”這個技術思想來解決頁面阻塞的問題,而“事件循環”是實現“異步”這個技術思想的最主要的技術手段。
????????但事件循環并不是全部的技術手段,比如Promise,雖然受事件循環管理,但是如果沒有事件循環,單一Promise依然能實現異步不是嗎?
????????博客不應該只有代碼和解決方案,重點應該在于給出解決方案的同時分享思維模式,只有思維才能可持續地解決問題,只有思維才是真正值得學習和分享的核心要素。如果這篇博客能給您帶來一點幫助,麻煩您點個贊支持一下,還可以收藏起來以備不時之需,有疑問和錯誤歡迎在評論區指出~
????????更多優質內容,請關注:
JS底層邏輯:
???? ? ?路由通配符,小小的字符有大大的作用,你真的熟悉嗎??
????????管理數據必備!偵聽器watch用法詳解
??? ? ??什么是深拷貝?深拷貝和淺拷貝有什么區別
JS語法篇:
????????你真的會使用Vue3的onMounted鉤子函數嗎?Vue3中onMounted的用法詳解
????????對象數據的讀取,看這一篇就夠了!
????????通過array.every()實現數據驗證、權限檢查和一致性檢查,array.some與array.every的區別
????????通過array.some()實現權限檢查、表單驗證、庫存管理、內容審查和數據處理
????????通過array.map()實現數據轉換、創建派生數組、異步數據流處理、搜索和過濾等需求
????????通過array.reduce()實現數據匯總、條件篩選和映射、對象屬性的扁平化、轉換數據格式等
????????通過array.filter()實現數組的數據篩選、數據清洗和鏈式調用
巧妙算法與竅門:
????????多維數組操作,不要再用遍歷循環foreach了,來試試數組展平的小妙招!
????????別再用雙層遍歷循環來做新舊數組對比,尋找新增元素了!
????????shpfile轉GeoJSON且控制轉化精度;如何獲取GeoJSON?GeoJson結構詳解
????????Mapbox添加行政區矢量圖層、分級設色圖層、自定義鼠標懸浮框、添加天地圖底圖等
Element plus拓展:
????????通過el-tree自定義渲染網頁版工作目錄,實現鼠標懸浮顯示完整名稱等
????????el-table實現動態數據的實時排序,一篇文章講清楚elementui的表格排序功能
?????? ?el-table中如何添加漸變色帶、多色色帶