引言
一直對瀏覽器的進程、線程的運行一無所知,經過一次的刷刷刷相關的博客之後,對其有了初步的瞭解,是時候該總結一波了。
進程、線程之間的關係
一個進程有一個或多個線程,線程之間共同完成進程分配下來的任務。打個比方:
- 假如進程是一個工廠,工廠有它的獨立的資源
- 工廠之間相互獨立
- 線程是工廠中的工人,多個工人協作完成任務
- 工廠內有一個或多個工人
- 工人之間共享空間
再完善完善概念:
- 工廠的資源 -> 系統分配的內存(獨立的一塊內存)
- 工廠之間的相互獨立 -> 進程之間相互獨立
- 多個工人協作完成任務 -> 多個線程在進程中協作完成任務
- 工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成
- 工人之間共享空間 -> 同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)
進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位),線程是cpu調度的最小單位(線程是建立在進程的基礎上的一次程序運行單位)。
瀏覽器內的進程
知道了進程與線程之間的關係之後,下面是瀏覽器與進程的關係了。首先,瀏覽器是多進程的,之所以瀏覽器能夠運行,是因爲系統給瀏覽器分配了資源,如cpu、內存,簡單的說就是,瀏覽器每打開一個標籤頁,就相當於創建了一個獨立的瀏覽器進程。例如我們查看chrome裏面的任務管理器。
注意: 在這裏瀏覽器應該也有自己的優化機制,有時候打開多個tab頁後,可以在Chrome任務管理器中看到,有些進程被合併了(譬如打開多個空白標籤頁後,會發現多個空白標籤頁被合併成了一個進程),所以每一個Tab標籤對應一個進程並不一定是絕對的。
除了瀏覽器的標籤頁進程之外,瀏覽器還有一些其他進程來輔助支撐標籤頁的進程,如下:
① Browser進程:瀏覽器的主進程(負責協調、主控),只有一個。作用有
- 負責瀏覽器界面顯示,與用戶交互。如前進,後退等
- 負責各個頁面的管理,創建和銷燬其他進程
- 網絡資源的管理,下載等
② 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建
③ GPU進程:最多一個,用於3D繪製等
④ 瀏覽器渲染進程(瀏覽器內核),Renderer進程,內部是多線程的,也就是我們每個標籤頁所擁有的進程,互不影響,負責頁面渲染,腳本執行,事件處理等
如下圖:
瀏覽器內核
瀏覽器內核,即我們的渲染進程,有名Renderer進程,我們頁面的渲染,js的執行,事件的循環都在這一進程內進行,也就是說,該進程下面擁有着多個線程,靠着這些現成共同完成渲染任務。那麼這些線程是什麼呢,如下:
① 圖形用戶界面GUI渲染線程
- 負責渲染瀏覽器界面,包括解析HTML、CSS、構建DOM樹、Render樹、佈局與繪製等
- 當界面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該線程就會執行
② JS引擎線程
- JS內核,也稱JS引擎,負責處理執行javascript腳本
- 等待任務隊列的任務的到來,然後加以處理,瀏覽器無論什麼時候都只有一個JS引擎在運行JS程序
③ 事件觸發線程
- 聽起來像JS的執行,但是其實歸屬於瀏覽器,而不是JS引擎,用來控制時間循環(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開線程協助)
- 當JS引擎執行代碼塊如setTimeout時(也可來自瀏覽器內核的其他線程,如鼠標點擊、AJAX異步請求等),會將對應任務添加到事件線程中
- 當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理
- 注意:由於JS的單線程關係,所以這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時纔會去執行)
④ 定時觸發器線程
setInterval
與setTimeout
所在線程- 定時計時器並不是由JS引擎計時的,因爲如果JS引擎是單線程的,如果JS引擎處於堵塞狀態,那會影響到計時的準確
- 當計時完成被觸發,事件會被添加到事件隊列,等待JS引擎空閒了執行
- 注意:W3C的HTML標準中規定,setTimeout中低與
4ms
的時間間隔算爲4ms
⑤ 異步HTTP請求線程
- 在XMLHttpRequest在連接後新啓動的一個線程
- 線程如果檢測到請求的狀態變更,如果設置有回調函數,該線程會把回調函數添加到事件隊列,同理,等待JS引擎空閒了執行
瀏覽器內核,放圖加強記憶:
爲什麼JS引擎是單線程的
JavaScript作爲一門客戶端的腳本語言,主要的任務是處理用戶的交互,而用戶的交互無非就是響應DOM的增刪改,使用事件隊列的形式,一次事件循環只處理一個事件響應,使得腳本執行相對連續。如果JS引擎被設計爲多線程的,那麼DOM之間必然會存在資源競爭,那麼語言的實現會變得非常臃腫,在客戶端跑起來,資源的消耗和性能將會是不太樂觀的,故設計爲單線程的形式,並附加一些其他的線程來實現異步的形式,這樣運行成本相對於使用JS多線程來說降低了很多。
瀏覽器內核中線程之間的關係
GUI渲染線程與JS引擎線程互斥
因爲JS引擎可以修改DOM樹,那麼如果JS引擎在執行修改了DOM結構的同時,GUI線程也在渲染頁面,那麼這樣就會導致渲染線程獲取的DOM的元素信息可能與JS引擎操作DOM後的結果不一致。爲了防止這種現象,GUI線程與JS線程需要設計爲互斥關係,當JS引擎執行的時候,GUI線程需要被凍結,但是GUI的渲染會被保存在一個隊列當中,等待JS引擎空閒的時候執行渲染。
由此也可以推出,如果JS引擎正在進行CPU密集型計算,那麼JS引擎將會阻塞,長時間不空閒,導致渲染進程一直不能執行渲染,頁面就會看起來卡頓卡頓的,渲染不連貫,所以,要儘量避免JS執行時間過長。
JS引擎線程與事件觸發線程、定時觸發器線程、異步HTTP請求線程
事件觸發線程、定時觸發器線程、異步HTTP請求線程三個線程有一個共同點,那就是使用回調函數的形式,當滿足了特定的條件,這些回調函數會被執行。這些回調函數被瀏覽器內核理解成事件,在瀏覽器內核中擁有一個事件隊列,這三個線程當滿足了內部特定的條件,會將這些回調函數添加到事件隊列中,等待JS引擎空閒執行。例如異步HTTP請求線程,線程如果檢測到請求的狀態變更,如果設置有回調函數,回調函數會被添加事件隊列中,等待JS引擎空閒了執行。
但是,JS引擎對事件隊列(宏任務)與JS引擎內的任務(微任務)執行存在着先後循序,當每執行完一個事件隊列的時間,JS引擎會檢測內部是否有未執行的任務,如果有,將會優先執行(微任務)。
WebWorker
因爲JS引擎是單線程的,當JS執行時間過長會頁面阻塞,那麼JS就真的對CPU密集型計算無能爲力麼?
所以,後來HTML5中支持了 Web Worker。
來自MDN的官方解釋
Web Workers 使得一個Web應用程序可以在與主執行線程分離的後臺線程中運行一個腳本操作。這樣做的好處是可以在一個單獨的線程中執行費時的處理任務,從而允許主(通常是UI)線程運行而不被阻塞/放慢。
注意點:
- WebWorker可以想瀏覽器申請一個子線程,該子線程服務於主線程,完全受主線程控制。
- JS引擎線程與worker線程間通過特定的方式通信(postMessage API,需要通過序列化對象來與線程交互特定的數據)
所以,如果需要進行一些高耗時的計算時,可以單獨開啓一個WebWorker線程,這樣不管這個WebWorker子線程怎麼密集計算、怎麼阻塞,都不會影響JS引擎主線程,只需要等計算結束,將結果通過postMessage傳輸給主線程就可以了。
另外,還有個東西叫 SharedWorker
,與WebWorker在概念上所不同。
- WebWorker 只屬於某一個頁面,不會和其他標籤頁的Renderer進程共享,WebWorker是屬於Renderer進程創建的進程。
- SharedWorker 是由瀏覽器單獨創建的進程來運行的JS程序,它被所有的Renderer進程所共享,在瀏覽器中,最多隻能存在一個SharedWorker進程。
SharedWorker由進程管理,WebWorker是某一個Renderer進程下的線程。
瀏覽器的渲染流程
每個瀏覽器內核的渲染流程不一樣,下面我們主要以webkit
爲主。
首先是渲染的前奏:
- 瀏覽器輸入url,瀏覽器主進程接管,開了一個下載線程
- 然後進行HTTP請求(DNS查詢、IP尋址等等),等待響應,開始下載響應報文。
- 將下載完的內容轉交給Renderer進程管理
- 開始渲染...
在說渲染之前,需要理解一些概念:
- DOM Tree: 瀏覽器將HTML解析成樹形的數據結構。
- CSS Rule Tree:瀏覽器將CSS解析成樹形的數據結構。
- Render Tree:DOM樹和CSS規則樹合併後生產Render樹。
- layout:有了Render Tree,瀏覽器已經能知道網頁中有哪些節點、各個節點的CSS定義以及他們的從屬關係,從而去計算出每個節點在屏幕中的位置。
- painting: 按照算出來的規則,通過顯卡,把內容畫到屏幕上。
- reflow(迴流):當瀏覽器發現某個部分發生了點變化影響了佈局,需要倒回去重新渲染,內行稱這個回退的過程叫
reflow
。reflow 會從 <html> 這個 root frame 開始遞歸往下,依次計算所有的結點幾何尺寸和位置。reflow 幾乎是無法避免的。現在界面上流行的一些效果,比如樹狀目錄的摺疊、展開(實質上是元素的顯 示與隱藏)等,都將引起瀏覽器的 reflow。鼠標滑過、點擊……只要這些行爲引起了頁面上某些元素的佔位面積、定位方式、邊距等屬性的變化,都會引起它內部、周圍甚至整個頁面的重新渲 染。通常我們都無法預估瀏覽器到底會 reflow 哪一部分的代碼,它們都彼此相互影響着。 - repaint(重繪):改變某個元素的背景色、文字顏色、邊框顏色等等不影響它周圍或內部佈局的屬性時,屏幕的一部分要重畫,但是元素的幾何尺寸沒有變。
注意:display:none
的節點不會被加入Render Tree,而visibility: hidden
則會,所以display:none
會觸發reflow
,visibility: hidden
會觸發repaint
。
瀏覽器內核拿到響應報文之後,渲染大概分爲以下步驟
- 解析html生產DOM樹。
- 解析CSS規則。
- 根據DOM Tree和CSS Tree生成Render Tree。
- 根據Render樹進行layout,負責各個元素節點的尺寸、位置計算。
- 繪製Render樹(painting),繪製頁面像素信息。
- 瀏覽器會將各層的信息發送給GPU,GPU會將各層合成(composite),顯示在屏幕上。
詳細步驟略去,大概步驟如下,渲染完畢後JS引擎開始執行load
事件,繪製流程見下圖。
由圖中可以看出,css在加載過程中不會影響到DOM樹的生成,但是會影響到Render樹的生成,進而影響到layout,所以一般來說,style的link標籤需要儘量放在head裏面,因爲在解析DOM樹的時候是自上而下的,而css樣式又是通過異步加載的,這樣的話,解析DOM樹下的body節點和加載css樣式能儘可能的並行,加快Render樹的生成的速度,當然,如果css是通過js動態添加進來的,會引起頁面的重繪或重新佈局。
從有html標準以來到目前爲止(2017年5月),標準一直是規定style元素不應出現在body元素中。
前面提到了load
事件,那麼與DOMContentLoaded
事件有什麼分別。
- 當 DOMContentLoaded 事件觸發時,僅當DOM加載完成,不包括樣式表,圖片。 (譬如如果有async加載的腳本就不一定完成)
- 當 onLoad 事件觸發時,頁面上所有的DOM,樣式表,腳本,圖片都已經加載完成了。 (渲染完畢了)
順序是:DOMContentLoaded -> load