徹底搞清楚JS瀏覽器事件循環機制

一、概述

在分析瀏覽器的渲染過程之前,我們先了解一下什麼是進程和線程:

(1)什麼是進程?

進程是CPU進行資源分配的基本單位

(2)什麼是線程?

線程是CPU調度的最小單位,是建立在進程的基礎上運行的單位,共享進程的內存空間。

那麼我們可以得出結論:

1、進程是會佔用系統資源;2、一個進程內可以存在一個或者多個線程,這就是單線程和多線程;3、無論是單線程還是多線程都是在一個進程內。

二、多進程

1、瀏覽器是多進程

2、不同類型的標籤頁都會開啓一個新的進程

3、相同類型的標籤頁是會合併到一個進程

上圖中瀏覽器的各個進程的主要作用:

1、瀏覽器進程

(1)負責管理各個標籤頁的創建和銷燬

(2)負責瀏覽器的頁面顯示和功能(前進,後退,收藏等)

(3)負責資源的管理與下載

2、第三方插件進程

(1)負責每個第三方插件的使用,每個第三方插件使用時候都會創建一個對應的進程

3、GPU進程

(1)負責3D繪製和硬件加速

4、瀏覽器渲染進程(咱們這回主要分析的)

1、瀏覽器內核,主要負責HTML,CSS,JS等文件的解析和執行

三、瀏覽器內核

瀏覽器內核就是瀏覽器渲染進程,從接收下載文件後再到呈現整個頁面的過程,由瀏覽器渲染進程負責,主要流程如下:

1、解析HTML文件和CSS文件,加載圖片等資源文件,渲染成用戶看到的頁面

2、執行解析js文件腳本代碼

這裏主要講瀏覽器頁面渲染過程,在該過程中瀏覽器渲染進程會開啓多個線程協作完成,主要的線程以及作用如下:

  1. GUI渲染線程
    (1)當瀏覽器收到響應的html後,該線程開始解析HTML文檔構建DOM樹,解析CSS文件構建CSSOM,合併構成渲染樹,並計算佈局樣式,繪製在頁面上(該處可深挖的坑,HTML解析規則,CSS解析規則,渲染流程細節)
    (2)當界面樣式被修改的時候可能會觸發reflow和repaint,該線程就會重新計算,重新繪製,是前端開發需要着重優化的點

  2. JS引擎線程
    (1)JS內核,也稱JS引擎(例如V8引擎),負責處理執行javascript腳本程序,
    (2)由於js是單線程(一個Tab頁內中無論什麼時候都只有一個JS線程在運行JS程序),依靠任務隊列來進行js代碼的執行,所以js引擎會一直等待着任務隊列中任務的到來,然後加以處理。

  3. 事件觸發線程
    (1)歸屬於渲染(瀏覽器內核)進程,不受JS引擎線程控制。主要用於控制事件(例如鼠標,鍵盤等事件),當該事件被觸發時候,事件觸發線程就會把該事件的處理函數添加進任務隊列中,等待JS引擎線程空閒後執行

  4. 定時器出發線程
    (1)傳說中的setInterval與setTimeout所在線程
    (2)瀏覽器的定時器並不是由JavaScript引擎計數的,因爲JavaScript引擎是單線程的, 如果處於阻塞線程狀態就會影響計時的準確,因此通過單獨的線程來計時並觸發定時器,計時完畢後,滿足定時器的觸發條件,則將定時器的處理函數添加進任務隊列中,等待JS引擎線程空閒後執行。
    (3)W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算爲4ms

  5. 異步HTTP請求線程
    (1)當XMLHttpRequest連接後,瀏覽器會新開的一個線程,當監控到readyState狀態變更時,如果設置了該狀態的回調函數,則將該狀態的處理函數推進任務隊列中,等待JS引擎線程空閒後執行
    (2)注意:瀏覽器對通一域名請求的併發連接數是有限制的,Chrome和Firefox限制數爲6個,ie8則爲10個。

總結:2-5 四個線程參與了JS的執行,但是永遠只有JS引擎線程在執行JS腳本程序,其他三個線程只負責將滿足觸發條件的處理函數推進任務隊列,等待JS引擎線程執行。

在這裏插入圖片描述

GUI渲染線程與JS引擎線程互斥

由於JavaScript是可操縱DOM的,如果在修改這些元素屬性同時渲染界面(即JS線程和UI線程同時運行),那麼渲染線程前後獲得的元素數據就可能不一致了。

因此爲了防止渲染出現不可預期的結果,瀏覽器設置GUI渲染線程與JS引擎爲互斥的關係。

當JS引擎執行時GUI線程會被掛起,GUI更新則會被保存在一個隊列中等到JS引擎線程空閒時立即被執行。

JavaScript 引擎是單線程

JavaScript 引擎是單線程,也就是說每次只能執行一項任務,其他任務都得按照順序排隊等待被執行,只有當前的任務執行完成之後纔會往下執行下一個任務。

基本概念

1、js的執行過程是單線程的模式,也就是同步進行,只有前面的代碼執行完了纔會往下面執行

2、但是執行js代碼也只是瀏覽器的線程之一所負責的事情,這個線程被稱爲js引擎,瀏覽器還具有其他線程:界面渲染線程(UI)、瀏覽器事件觸發線程(控制交互,響應用戶)、http請求線程(處理請求,而ajax發送請求則會委託瀏覽器新開一個http線程)、EventLoop輪詢線程(負責輪詢消息隊列)

3、瀏覽器中js代碼的作用:執行JavaScript代碼 、對用戶的輸入(包含鼠標點擊、鍵盤輸入等等)做出反應 、處理異步的網絡請求

js單線程

1、單線程的含義是js只能在一個線程上運行,也就說,js同時只能執行一個js任務,其它的任務則會排隊等待執行。

2、js是單線程的,並不代表js引擎線程只有一個。js引擎有多個線程,一個主線程,其它的後臺配合主線程。

3、多線程之間會共享運行資源,瀏覽器端的js會操作dom,多個線程必然會帶來同步的問題,所有js核心選擇了單線程來避免處理這個麻煩。js可以操作dom,影響渲染,所以js引擎線程和UI線程是互斥的。這也就解釋了js執行時會阻塞頁面的渲染

js消息隊列

1、JavaScript運行時,除了一個運行線程,引擎還提供一個消息隊列,裏面是各種需要當前程序處理的消息。新的消息進入隊列的時候,會自動排在隊列的尾端

2、單線程意味着js任務需要排隊,如果前一個任務出現大量的耗時操作,後面的任務得不到執行,任務的積累會導致頁面的“假死”。這也是js編程一直在強調需要回避的“坑”

js執行任務方式

1、首先js任務分兩種:同步任務、異步任務

2、同步任務:在主線程排隊支持的任務,前一個任務執行完畢後,執行後一個任務,形成一個執行棧,線程執行時在內存形成的空間爲棧,進程形成堆結構,這是內存的結構。執行棧可以實現函數的層層調用。注意不要理解成同步代碼進入棧中,按棧的出棧順序來執行。

3、異步任務:會被主線程掛起,不會進入主線程,而是進入消息隊列,而且必須指定回調函數,只有消息隊列通知主線程,並且執行棧爲空時,該消息對應的任務纔會進入執行棧獲得執行的機會。

4、主線程說明:

(1)所有同步任務都在主線程上執行,形成一個執行棧。
(2)主線程之外,還存在一個”任務隊列”(消息隊列)。只要異步任務有了運行結果,就在”任務隊列”之中放置一個事件。
(3)一旦”執行棧”中的所有同步任務執行完畢,系統就會讀取”任務隊列”(消息隊列),看看裏面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
(4)主線程不斷重複上面的第三步。

5、消息隊列說明:

(1)消息隊列隊列(或者叫任務隊列)是一個事件的隊列,IO響應時(鼠標點擊等輸入輸出設備的操作),會往隊列中添加一個消息,此時說明相關的異步代碼到了執行的時機,可以進入主線程的執行棧了。

(2)主線程讀取消息隊列,可以讀取到對應的事件。

(3)消息隊列可以響應IO事件,還有用戶產生的事件(比如點擊鼠標,頁面滾動),只要指定了回調函數,就會進入消息隊列,等待EventLoop輪詢線程處理,是否可以進入主線程的執行棧。

(4)消息和回調函數相互聯繫的含義:主線程讀到消息,就會執行相應的回調函數;進入消息隊列的消息,必須對應相應的回調函數,否則這個消息會被丟棄不會進入消息隊列。

(5)消息隊列是一個先進先出的隊列結構,這就決定了它的執行順序,先產生的消息會被主線程先讀取,會不會執行則會先檢查一下執行時間,因爲存在setTimeout等定時函數,這類事件產生的消息進入到消息隊列,被執行的時機取決與它在隊列中的位置和執行時間有關。【使用setTimeout能夠避免阻塞UI線程就是這個原因】。

5、需要注意的是:執行棧中的代碼(同步任務),總是在讀取”任務隊列”(異步任務)之前執行。

EventLoop

1、主線程從”任務隊列”中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱爲Event Loop(事件循環)。

2、簡單說,瀏覽器的兩個線程:一個負責程序本身的運行,稱爲”主線程”;另一個負責主線程與其他進程(主要是各種I/O操作)的通信,被稱爲”Event Loop線程”(可以譯爲”消息線程”)。

3、由於js是運行在單線程上的,所有瀏覽器單獨開啓一個線程來處理事件消息的輪詢,避免阻塞js的執行。

異步代碼執行邏輯

1、每當遇到I/O的時候,主線程就讓EventLoop線程去通知相應的I/O程序,然後接着往後運行,所以不存在等待時間。等到I/O程序完成操作,EventLoop線程把消息添加到消息隊列,主線程就調用事先設定的回調函數,完成整個任務。

2、js的ajax是new XMLHttpRequest()對象實現的,瀏覽器會新開一個線程來處理http請求,這就是ajax能夠實現局部刷新的同時,還能響應用戶交互的原因。

定時器

1、前面也提到了定時器,定時器是會在進入消息隊列,這也就和異步代碼的執行邏輯一樣了。它在”消息隊列”的尾部添加一個消息,因此要等到同步任務和”消息隊列”現有的任務都處理完,纔會得到執行的機會,還要看定時器設置的時間是否到了纔會執行。

<script>
    for(var i = 0 ; i < 10; i++){
       setTimeout(function(){
           console.log(i);//打印10次10
       },0);
    }
</script>

所以,只有等到主線程的任務執行完之後,setTimeout中的事件纔會被執行,雖然時間間隔是0秒,但是必須等主線程任務完成,所以最後打印的都是10

2、可以用閉包來解決問題,每次執行for循環的時候,把函數作爲返回值給setTimeout作爲參數,函數裏面存着從for循環裏面拿到的每一個值,下面代碼會返回多個函數,每個函數的j值都不一樣

<script>
    for(var i = 0; i< 3; i++){
        function foo(j){
        // var j;
        // j = 實參
        //j = i
            return function(){
                console.log(j);
            };
        }
        //0
        var f = foo(i);
        setTimeout(f, 0);
    }
</script>

總結

Javascript 有一個 main thread 主線程和 call-stack 調用棧(執行棧),所有的任務都會被放到調用棧等待主線程執行。

JS 調用棧

JS 調用棧是一種後進先出的數據結構。當函數被調用時,會被添加到棧中的頂部,執行完成之後就從棧頂部移出該函數,直到棧內被清空。

同步任務、異步任務

JavaScript 單線程中的任務分爲同步任務和異步任務。同步任務會在調用棧中按照順序排隊等待主線程執行,異步任務則會在異步有了結果後將註冊的回調函數添加到任務隊列(消息隊列)中等待主線程空閒的時候,也就是棧內被清空的時候,被讀取到棧中等待主線程執行。任務隊列是先進先出的數據結構。

Event Loop

調用棧中的同步任務都執行完畢,棧內被清空了,就代表主線程空閒了,這個時候就會去任務隊列中按照順序讀取一個任務放入到棧中執行。每次棧內被清空,都會去讀取任務隊列有沒有任務,有就讀取執行,一直循環讀取-執行的操作,就形成了事件循環。
在這裏插入圖片描述
**加粗樣式**
宏任務(macro-task)、微任務(micro-task)

除了廣義的同步任務和異步任務,JavaScript 單線程中的任務可以細分爲宏任務和微任務。

macro-task包括:script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。

micro-task包括:process.nextTick, Promises, Object.observe, MutationObserver。

有了宏任務和微任務的概念後,那 JS 的執行順序是怎樣的?是宏任務先還是微任務先?

第一次事件循環中,JavaScript 引擎會把整個 script 代碼當成一個宏任務執行,執行完成之後,再檢測本次循環中是否尋在微任務,存在的話就依次從微任務的任務隊列中讀取執行完所有的微任務,再讀取宏任務的任務隊列中的任務執行,再執行所有的微任務,如此循環。JS 的執行順序就是每次事件循環中的宏任務-微任務。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章