從瀏覽器環境到JavaScript執行流程的一次簡單梳理

參考文章

www.ruanyifeng.com/blog/2014/1…javascript

www.zhihu.com/question/64…html

www.alloyteam.com/2016/05/jav…java

nodejs.org/zh-cn/docs/…node

juejin.im/post/59e85e…canvas

juejin.im/post/5be5a0…瀏覽器

juejin.im/post/5a6547…網絡

瀏覽器環境的多進程環境

首先明確一下兩個概念多線程

  • 進程是CUP資源分配的最小單位 (每一個進程之間相互獨立,各自擁有一塊運行資源)
  • 線程是CPU資源調度的最小單位 (一個進程能夠包含多個線程,多個線程協做完成任務,共享一個進程中的資源) 現代瀏覽器是一個及其龐大的大型軟件,在某種程度上甚至不亞於一個操做系統,它由多媒體支持、圖形顯示、GPU渲染、進程管理、內存管理、沙箱機制、存儲系統、網絡管理等大大小小數百個組件組成。若是這麼大的一個軟件是單進程的,那麼其中一個組件出現問題,整個瀏覽器就沒法運做,及其影響用戶體驗,因此瀏覽器在實現上是多進程的。

瀏覽器擁有的多個進程

  • 一個 Browser 進程
    • 瀏覽器的主進程,負責瀏覽器界面的顯示與用戶交互。
    • 負責建立和銷燬其餘進程
    • 網絡資源管理
  • 多個 Renderer 進程
    • 每一個tab頁一個進程,瀏覽器有本身的優化策略,如多個空白tab頁的時候會將其合併
    • 每一個iframe頁面單獨一個renderer進程
    • 每一個renderer進程是一個獨立的沙箱,相互之間隔離不受影響
  • 一個 GPU 進程
  • 多個 NPAPI Render 進程多個 Pepper Plugin 進程
    • 每種類型的插件對應一個進程,僅當使用該插件時才建立

瀏覽器多線程的渲染進程(Renderer進程)

代碼寫的怎麼樣,頁面性能如何的直觀感受是頁面生成的快不快,這個與瀏覽器的渲染進程息息相關。下面先梳理一下異步

Renderer進程有哪些主要的線程

  • GUI渲染線程
    • 負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。
    • 當界面須要重繪(Repaint)或因爲某種操做引起迴流(reflow)時,該線程就會執行
    • 注意,GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(至關於被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閒時當即被執行。
  • JS引擎線程
    • 也稱爲JS內核,負責處理Javascript腳本程序。(例如V8引擎)
    • JS引擎線程負責解析Javascript腳本,運行代碼。
    • 一個Renderer進程中只有一個JS引擎線程
    • GUI渲染線程與JS引擎線程是互斥的,因此若是JS執行的時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞。
  • 事件觸發線程
    • 歸屬於瀏覽器而不是JS引擎,用來控制事件循環(點擊,鼠標移動這些都是瀏覽器事件)
    • 事件觸發以後會加入到事件隊列等待執行
  • 定時觸發器線程
    • setIntervalsetTimeout所在的線程
    • 瀏覽器定時計數器並非由JavaScript引擎計數的,是交給瀏覽器計時
    • setTimeout(fn,ms) 指定某個任務在主線程最先可得的空閒時間執行,ms秒以後將fn函數加入到隊列中
    • W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算爲4ms。因此setTimeout的延時設置爲0也不可能瞬發
  • 異步http請求線程
    • 在XMLHttpRequest鏈接後是經過瀏覽器新開一個線程請求
    • 將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入事件隊列中。再由JavaScript引擎執行。

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

因爲JavaScript是可操縱DOM的,若是在修改這些元素屬性同時渲染界面(即JS線程和UI線程同時運行),那麼渲染線程先後得到的元素數據就可能不一致了。 所以爲了防止渲染出現不可預期的結果,瀏覽器設置GUI渲染線程與JS引擎爲互斥的關係,當JS引擎執行時GUI線程會被掛起,GUI更新則會被保存在一個隊列中等到JS引擎線程空閒時當即被執行。ide

網頁解析的流程

主流程

頁面的解析工做是在 Renderer 進程中進行的,Renderer 進程經過在主線程中持有的 Blink 實例邊接收邊解析 HTML 內容。每次從網絡緩衝區中讀取 8KB 之內的數據。瀏覽器自上而下逐行解析 HTML 內容,通過詞法分析、語法分析,構建 DOM 樹。當遇到外部 CSS 連接時,主線程調使用網絡請求板塊異步獲取資源,不阻塞而繼續構建 DOM 樹。當 CSS 下載完畢後,主線程在合適的時機解析 CSS 內容,通過詞法分析、語法分析,構建 CSSOM 樹。瀏覽器結合 DOM 樹和 CSSOM 樹構建 Render 樹,並計算佈局屬性,每一個 Node 的幾何屬性和在座標系中的位置,最後進行繪製展示在屏幕上。當遇到外部 JS 連接時,主線程調使用網絡請求板塊異步獲取資源,由於 JS 可能會修改 DOM 樹和 CSSOM 樹而形成迴流和重繪,此時 DOM 樹的構建是處於阻塞狀態的。但主線程並不會掛起,瀏覽器會用一個輕量級的掃描器去發現後續須要下載的外部資源,提早發起網絡請求,而腳本內部的資源不會識別,比方 document.write。當 JS 下載完畢後,瀏覽器調使用 V8 引擎在 Script Streamer 線程中解析、編譯 JS 內容,並在主線程中執行。

image.png | left | 747x292

渲染流程

當 DOM 樹構建完畢後,還需通過好幾迴轉換,它們有多種中間表示。首先計算佈局、繪圖樣式,轉換爲 RenderObject 樹(也叫 Render 樹)。再轉換爲 RenderLayer 樹,當 RenderObject 擁有同一個座標系(比方 canvas、absolute)時,它們會合併爲一個 RenderLayer,這一步由 CPU 負責合成。接着轉換爲 GraphicsLayer 樹,當 RenderLayer 知足合成層條件(比方 transform,熟知的硬件加速)時,會有本身的 GraphicsLayer,不然與父節點合併,這一步一樣由 CPU 負責合成。最後,每一個 GraphicsLayer 都有一個 GraphicsContext 對象,負責將層繪製成位圖做爲紋理上傳給 GPU,由 GPU 負責合成多個紋理,最終顯示在屏幕上。 另外,爲了提高渲染性能效率,瀏覽器會有專使用的 Compositor 線程來負責層合成,同時負責解決部分交互事件(比方滾動、觸摸),直接響應 UI 升級而不阻塞主線程。主線程把 RenderLayer 樹同步給 Compositor 線程,由它開啓多個 Rasterizer 線程,進行光柵化解決,在可視區域以瓦片爲單位把頂點數據轉換爲片元,最後交付給 GPU 進行最終合成渲染。

JavaScript的事件循環

首先要將JavaScript分紅同步任務和異步任務,其次是引入宏任務(macro-task)和微任務(micro-task)

同步任務和異步任務

下面用一副導圖來描述一下同步任務與異步任務的執行

  • 首先會判斷一個任務是同步任務仍是異步任務,同步任務進入主線程,異步的進入Event Table並註冊函數
    • 同步任務:頁面骨架和頁面渲染等這些主要的且資源耗時小的任務
    • 異步任務:圖片、音樂等加載資源耗時長的或須要等待條件觸發的任務
  • 當資源下載完畢或者指定的事件完成以後Event Table將對應的函數移入Event Queue中等待執行
  • 主線程內的任務執行完畢爲空,會去Event Queue讀取對應的函數,進入主線程執行。
  • 上述過程會不斷重複,也就是常說的Event Loop(事件循環)。

圖片1.png | center | 738x596

宏任務和微任務

  • 宏任務(macrotask):
    • 能夠理解爲執行的整個代碼塊就是一個宏任務(每次執行棧中的代碼)
    • 每個宏任務都是連貫執行,中間不會中斷去執行其餘任務
    • 瀏覽器爲了可以使得JS內部task與DOM任務可以有序的執行,會在一個task執行結束後,在下一個 task 執行開始前,對頁面進行從新渲染 (task->渲染->task->...
    • 主代碼塊,setTimeout,setInterval等(能夠看到,事件隊列中的每個事件都是一個macrotask)
  • 微任務(microtask):
    • 一個宏任務執行完畢下一個宏任務執行以前執行這一個宏任務中產生的微任務(某一個macrotask執行完後,就會將在它執行期間產生的全部microtask都執行完畢(在渲染前))
    • 因此它的響應速度相比setTimeout(setTimeout是task)會更快,由於無需等渲染
    • Promise,process.nextTick, .then等

圖片2.png | center | 747x164

一道讀程序題

console.log(1); 
setTimeout(function(){
    console.log(2);
},1000);

console.log(3); 

var p = new Promise(function(resolve,reject){ 
    setTimeout(function(){
        console.log(4);
    },500);
    resolve(); 
}).then(function(){
    console.log(5);
});

console.log(6);

p.then(function(){
    console.log(7);
});
console.log(8)

// 1 3 6 8 5 7 4 2
複製代碼

第一次宏任務循環: 執行的整段代碼看作一個宏任務

  • 遇到 console.log(1) 打印1
  • setTimeout()交給定時器處理線程處理,1秒延時後將任務加入宏任務隊列 記爲 s1
  • 遇到console.log(3) 打印3
  • 遇到Promise,執行裏面的回調函數
  • setTimeout()交給定時器處理線程處理,0.5秒延時後將任務加入宏任務隊列 記爲s2
  • then中的回調函數加入此次的微任務隊列 then1
  • 跳出Promise後,執行console.log(6),打印6
  • 執行p.then,把回調函數加入微任務隊列 then2
  • 執行console.log(8)打印 8 此時打印的是 1 3 6 8 宏任務隊列:s1,s2 微任務隊列:then1,then2 第一次宏任務執行結束以後執行此次產生的微任務,依次執行then1,then2,依次打印 58

第二次宏任務開始 這裏須要注意的是s1s2觸發計時事件,是在Event Table中註冊,等待計時完畢以後把回調函數加入Event Queue中,因此 s20.5秒比s11秒要先完成,這樣在Event Queue中先放入s2,再放入s1 第二次宏任務執行s2,打印4,沒有微任務 第三次宏任務執行s1,打印2,沒有微任務

一些定時事件、點擊事件、網絡加載都是交給瀏覽器的API處理的,這些地方會建立宏任務,並且並非直接加入到隊列中,而是有一個EventTable來記錄這些事件,等到條件知足以後再將回調的函數註冊到Event Queue中,每次宏任務從 Event Queue中獲取下一個宏任務

相關文章
相關標籤/搜索