1. 程式人生 > >瀏覽器多程序與JS執行緒

瀏覽器多程序與JS執行緒

引言

一直對瀏覽器的程序、執行緒的執行一無所知,經過一次的刷刷刷相關的部落格之後,對其有了初步的瞭解,是時候該總結一波了。

程序、執行緒之間的關係

一個程序有一個或多個執行緒,執行緒之間共同完成程序分配下來的任務。打個比方:

  • 假如程序是一個工廠,工廠有它的獨立的資源
  • 工廠之間相互獨立
  • 執行緒是工廠中的工人,多個工人協作完成任務
  • 工廠內有一個或多個工人
  • 工人之間共享空間

再完善完善概念:

  • 工廠的資源 -> 系統分配的記憶體(獨立的一塊記憶體)
  • 工廠之間的相互獨立 -> 程序之間相互獨立
  • 多個工人協作完成任務 -> 多個執行緒在程序中協作完成任務
  • 工廠內有一個或多個工人 -> 一個程序由一個或多個執行緒組成
  • 工人之間共享空間 -> 同一程序下的各個執行緒之間共享程式的記憶體空間(包括程式碼段、資料集、堆等)

程序是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引擎空閒時才會去執行)

④ 定時觸發器執行緒

  • setIntervalsetTimeout所線上程
  • 定時計時器並不是由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為主。
首先是渲染的前奏:

  1. 瀏覽器輸入url,瀏覽器主程序接管,開了一個下載執行緒
  2. 然後進行HTTP請求(DNS查詢、IP定址等等),等待響應,開始下載響應報文。
  3. 將下載完的內容轉交給Renderer程序管理
  4. 開始渲染...

在說渲染之前,需要理解一些概念:

  • 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會觸發reflowvisibility: hidden會觸發repaint

瀏覽器核心拿到響應報文之後,渲染大概分為以下步驟

  1. 解析html生產DOM樹。
  2. 解析CSS規則。
  3. 根據DOM Tree和CSS Tree生成Render Tree。
  4. 根據Render樹進行layout,負責各個元素節點的尺寸、位置計算。
  5. 繪製Render樹(painting),繪製頁面畫素資訊。
  6. 瀏覽器會將各層的資訊傳送給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

最後

寫到這裡,總結了也有不少的內容,也對瀏覽器多執行緒、JS引擎有所瞭解,後面打算在看看JS的執行機制。前端知識也是無窮無盡,數不清的概念與無數個易忘的知識、各種框架原理,學來學去,還是發現自己知道得太少了。