1. 程式人生 > 實用技巧 >瀏覽器機制以及程序執行緒的關係

瀏覽器機制以及程序執行緒的關係

原文連結:https://www.cnblogs.com/tutuj/p/11025042.html

很多時候被問到從輸入url地址之後,會發生什麼?很多時候回答都很籠統,沒有自己的核心,所以學習一下大神的思路,以下總結的只是骨幹,只有將每一個部分都學習到,這樣才是一個知識體系,才能很好的理解上下結構與關係。

1. 從瀏覽器接收url到開啟網路請求執行緒(這一部分可以展開瀏覽器的機制以及程序與執行緒之間的關係)

2. 開啟網路執行緒到發出一個完整的http請求(這一部分涉及到dns查詢,tcp/ip請求,五層因特網協議棧等知識)

3. 從伺服器接收到請求到對應後臺接收到請求(這一部分可能涉及到負載均衡,安全攔截以及後臺內部的處理等等)

4. 後臺和前臺的http互動(這一部分包括http頭部、響應碼、報文結構、cookie等知識,可以提下靜態資源的cookie優化,以及編碼解碼,如gzip壓縮等)

5. 單獨拎出來的快取問題,http的快取(這部分包括http快取頭部,etag,catch-control等)

6. 瀏覽器接收到http資料包後的解析流程(解析html-詞法分析然後解析成dom樹、解析css生成css規則樹、合併成render樹,然後layout、painting渲染、複合圖層的合成、GPU繪製、外鏈資源的處理、loaded和domcontentloaded等)

7. CSS的視覺化格式模型(元素的渲染規則,如包含塊,控制框,BFC,IFC等概念)

8. JS引擎解析過程(JS的解釋階段,預處理階段,執行階段生成執行上下文,VO,作用域鏈、回收機制等等)

9. 其它(可以拓展不同的知識模組,如跨域,web安全,hybrid模式等等內容

第一部分:瀏覽器程序以及程序執行緒關係

一、瀏覽器程序

首先我們瞭解一下官方的程序和執行緒的概念。

  • 程序是cpu資源分配的最小單位(是能擁有資源和獨立執行的最小單位)
  • 執行緒是cpu排程的最小單位(執行緒是建立在程序的基礎上的一次程式執行單位,一個程序中可以有多個執行緒)

然後我們的瀏覽器是多程序的,每開啟一個Tab頁,就相當於建立了一個獨立的瀏覽器程序。瀏覽器包括以下幾個主要的程序:

  1. Browser程序:瀏覽器的主控程序,只有一個。作用有

    • 負責瀏覽器介面顯示,與使用者互動。如前進,後退等
    • 負責各個頁面的管理,建立和銷燬其他程序
    • 將Renderer程序得到的記憶體中的Bitmap,繪製到使用者介面上
    • 網路資源的管理,下載等
  2. 第三方外掛程序:每種型別的外掛對應一個程序,僅當使用該外掛時才建立
  3. GPU程序:最多一個,用於3D繪製等
  4. 瀏覽器渲染程序(瀏覽器核心)(Renderer程序,內部是多執行緒的):預設每個Tab頁面一個程序,互不影響。主要作用為

    • 頁面渲染,指令碼執行,事件處理等

而在這麼多程序之中瀏覽器渲染程序是最重要的,因為它包括很多執行緒,而這些執行緒起了頁面渲染執行顯示的主要作用。以下列舉一些主要的執行緒:

  1. GUI渲染執行緒

    • 負責渲染瀏覽器介面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。
    • 當介面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該執行緒就會執行
    • 注意,GUI渲染執行緒與JS引擎執行緒是互斥的,當JS引擎執行時GUI執行緒會被掛起,GUI更新會被儲存在一個佇列中等到JS引擎空閒時立即被執行。
  2. JS引擎執行緒

    • 也稱為JS核心,負責處理Javascript指令碼程式。
    • JS引擎執行緒負責解析Javascript指令碼,執行程式碼。
    • JS引擎一直等待著任務佇列中任務的到來,然後加以處理,一個Tab頁(renderer程序)中無論什麼時候都只有一個JS執行緒在執行JS程式
    • 同樣注意,GUI渲染執行緒與JS引擎執行緒是互斥的,所以如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞。
  3. 事件觸發執行緒

    • 歸屬於瀏覽器而不是JS引擎,用來控制事件迴圈(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開執行緒協助)
    • 當JS引擎執行程式碼塊如setTimeOut時(也可來自瀏覽器核心的其他執行緒,如滑鼠點選、AJAX非同步請求等),會將對應任務新增到事件執行緒中
    • 當對應的事件符合觸發條件被觸發時,該執行緒會把事件新增到待處理佇列的隊尾,等待JS引擎的處理
    • 注意,由於JS的單執行緒關係,所以這些待處理佇列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時才會去執行)

  4. 定時觸發器執行緒

    • 傳說中的setIntervalsetTimeout所線上程
    • 瀏覽器定時計數器並不是由JavaScript引擎計數的,(因為JavaScript引擎是單執行緒的, 如果處於阻塞執行緒狀態就會影響記計時的準確)
    • 因此通過單獨執行緒來計時並觸發定時(計時完畢後,新增到事件佇列中,等待JS引擎空閒後執行)
    • 注意,W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算為4ms。
  5. 非同步http請求執行緒

    • 在XMLHttpRequest在連線後是通過瀏覽器新開一個執行緒請求
    • 將檢測到狀態變更時,如果設定有回撥函式,非同步執行緒就產生狀態變更事件,將這個回撥再放入事件佇列中。再由JavaScript引擎執行。

那麼這麼多執行緒,他們之間有什麼關係呢?

首先GUI渲染執行緒與JS引擎執行緒互斥

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

因此為了防止渲染出現不可預期的結果,瀏覽器設定GUI渲染執行緒與JS引擎為互斥的關係,當JS引擎執行時GUI執行緒會被掛起,
GUI更新則會被儲存在一個佇列中等到JS引擎執行緒空閒時立即被執行。

JS阻塞頁面載入

從上述的互斥關係,可以推匯出,JS如果執行時間過長就會阻塞頁面。

譬如,假設JS引擎正在進行巨量的計算,此時就算GUI有更新,也會被儲存到佇列中,等待JS引擎空閒後執行。
然後,由於巨量計算,所以JS引擎很可能很久很久後才能空閒,自然會感覺到巨卡無比。

所以,要儘量避免JS執行時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞的感覺。

那麼接下來是主控程序Browser程序對渲染程序的控制過程。

  • Browser程序收到使用者請求,首先需要獲取頁面內容(譬如通過網路下載資源),隨後將該任務通過RendererHost介面傳遞給Render程序
  • Renderer程序的Renderer介面收到訊息,簡單解釋後,交給渲染執行緒,然後開始渲染

    • 渲染執行緒接收請求,載入網頁並渲染網頁,這其中可能需要Browser程序獲取資源和需要GPU程序來幫助渲染
    • 當然可能會有JS執行緒操作DOM(這樣可能會造成迴流並重繪)
    • 最後Render程序將結果傳遞給Browser程序
  • Browser程序接收到結果並將結果繪製出來。

js執行機制

最後由於js是單執行緒,所以對於任務的執行自然會有一個順序,稱之為任務佇列,所有任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等著。所以任務又分為兩種,一種是同步任務:指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務。另一種是非同步任務:指的是不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。

總體來說,他的執行機制是這樣的:

(1)所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。
(2)主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
(4)主執行緒不斷重複上面的第三步。

只要主執行緒空了,就會去讀取"任務佇列",這就是JavaScript的執行機制。這個過程會不斷重複。

主執行緒執行的時候,產生堆(heap)和棧(stack),棧中的程式碼呼叫各種外部API,它們在"任務佇列"中加入各種事件(click,load,done)。只要棧中的程式碼執行完畢,主執行緒就會去讀取"任務佇列",依次執行那些事件所對應的回撥函式。

接下來看一個例項:

<script>
        console.log('start')  //同步任務在主執行緒上執行,進入執行棧。
        setTimeout(function () {        //非同步任務進入 task table,等到0秒之後進入task queue。
            console.log('setTimeout1');
        }, 0);
        console.log('end');   //同步任務在主執行緒上執行,進入執行棧。
</script>

所以這段程式的執行結果是:

然而我們這樣籠統的分為同步任務和非同步任務並不能非常精確到每一種事件,所以在此基礎上我們又分了巨集任務和微任務。

  • macro-task(巨集任務):包括整體程式碼script,setTimeout,setInterval
  • micro-task(微任務):Promise,process.nextTick

那麼現在的執行機制變成了:

  • 執行一個巨集任務,過程中如果遇到微任務,就將其放到微任務的【事件佇列】裡
  • 當前巨集任務執行完成後,會檢視微任務的【事件佇列】,並將裡面全部的微任務依次執行完

將同步任務非同步任務和巨集任務微任務相結合,便是更為準確的js執行機制。接下來請看網路盜圖:

  • 整體的script(作為第一個巨集任務)開始執行的時候,會把所有程式碼分為兩部分:“同步任務”、“非同步任務”;
  • 同步任務會直接進入主執行緒依次執行;
  • 非同步任務會再分為巨集任務和微任務;
  • 巨集任務進入到Event Table中,並在裡面註冊回撥函式,每當指定的事件完成時,Event Table會將這個函式移到Event Queue中;
  • 微任務也會進入到另一個Event Table中,並在裡面註冊回撥函式,每當指定的事件完成時,Event Table會將這個函式移到Event Queue中;
  • 當主執行緒內的任務執行完畢,主執行緒為空時,會檢查微任務的Event Queue,如果有任務,就全部執行,如果沒有就執行下一個巨集任務;
  • 上述過程會不斷重複,這就是Event Loop事件迴圈;

接下來請看例項:

<script>
        setTimeout(function () {     //setTimeout是非同步,且是巨集函式,放到巨集函式佇列中
            console.log(1)
        });
        new Promise(function (resolve) {    //new Promise是同步任務,直接執行,列印2,並執行for迴圈
            console.log(2);
            for (var i = 0; i < 10000; i++) {
                i == 9999 && resolve();
            }
        }).then(function () {      //promise.then是微任務,放到微任務佇列中
            console.log(3)
        });
        console.log(4);     //console.log(4)同步任務,直接執行,列印4
    </script>

第一次迴圈執行了兩個同步任務,列印了2、4,接下來檢查微任務佇列,發現.then函式,於是執行函式打印出3。接下來執行非同步任務setTimeout,於是打印出來1。

所以最後的結果是2、4、3、1。

接下來難度升級,請看例項2:

 <script>
        function add(x, y) {
            console.log(1)
            setTimeout(function () { // timer1
                console.log(2)
            }, 1000)
        }
        
        add();

        setTimeout(function () { // timer2
            console.log(3)
        })

        new Promise(function (resolve) {
            console.log(4)
            setTimeout(function () { // timer3
                console.log(5)
            }, 100)
            for (var i = 0; i < 100; i++) {
                i == 99 && resolve()
            }
        }).then(function () {
            setTimeout(function () { // timer4
                console.log(6)
            }, 0)
            console.log(7)
        })

        console.log(8)
    </script>

他的執行過程是:

1.add()是同步任務,直接執行,列印1;
2.add()裡面的setTimeout是非同步任務且巨集函式,記做timer1放到巨集函式佇列;
3.add()下面的setTimeout是非同步任務且巨集函式,記做timer2放到巨集函式佇列;
4.new Promise是同步任務,直接執行,列印4;
5.Promise裡面的setTimeout是非同步任務且巨集函式,記做timer3放到巨集函式佇列;
6.Promise裡面的for迴圈,同步任務,執行程式碼;
7.Promise.then是微任務,放到微任務佇列;
8.console.log(8)是同步任務,直接執行,列印8;
9.此時主執行緒任務執行完畢,檢查微任務佇列中,有Promise.then,執行微任務,發現有setTimeout是非同步任務且巨集函式,記做timer4放到巨集函式佇列;
10.微任務佇列中的console.log(7)是同步任務,直接執行,列印7;
11.微任務執行完畢,第一次迴圈結束;
12.檢查巨集任務Event Table,裡面有timer1、timer2、timer3、timer4,四個定時器巨集任務,按照定時器延遲時間得到可以執行的順序,即Event Queue:timer2、timer4、timer3、timer1,取出排在第一個的timer2;
13.取出timer2執行,console.log(3)同步任務,直接執行,列印3;
14.沒有微任務,第二次Event Loop結束;
15.取出timer4執行,console.log(6)同步任務,直接執行,列印6;
16.沒有微任務,第三次Event Loop結束;
17.取出timer3執行,console.log(5)同步任務,直接執行,列印5;
18.沒有微任務,第四次Event Loop結束;
19.取出timer1執行,console.log(2)同步任務,直接執行,列印2;
20.沒有微任務,也沒有巨集任務,第五次Event Loop結束;
21.結果:1,4,8,7,3,6,5,2。