前端知識體系(一)瀏覽器機制以及進程線程的關系


看了一篇大神的博客,對前端學習體系突然明悟了起來。於是准備參考着大神的腳步開始體系化的學習。博客鏈接:https://segmentfault.com/a/1190000013662126。

很多時候被問到從輸入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。
本文參考:
https://www.jianshu.com/p/e06e86ef2595
https://segmentfault.com/a/1190000013662126
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
https://segmentfault.com/a/1190000012925872
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM