1. 程式人生 > >多進程瀏覽器、多線程頁面渲染與js的單線程

多進程瀏覽器、多線程頁面渲染與js的單線程

發展 webkit 應用程序 rep 內核 sync 輸入 splay interval

線程與進程

說到單線程,就得從操作系統進程開始說起。在早期的操作系統中並沒有線程的概念,進程是能擁有資源和獨立運行的最小單位,也是程序執行的最小單位。任務調度采用的是時間片輪轉的搶占式調度方式,而進程是任務調度的最小單位,每個進程有各自獨立的一塊內存,使得各個進程之間內存地址相互隔離。後來,隨著計算機的發展,對CPU的要求越來越高,進程之間的切換開銷較大,已經無法滿足越來越復雜的程序的要求了。於是就發明了線程,線程是程序執行中一個單一的順序控制流程,是程序執行流的最小單元。這裏把線程比喻一個車間的工人,即一個車間可以允許由多個工人協同完成一個任務。進程是應用程序的執行實例,每一個進程都是由私有的虛擬地址空間、代碼、數據和其它系統資源所組成;進程在運行過程中能夠申請創建和使用系統資源(如獨立的內存區域等),這些資源也會隨著進程的終止而被銷毀。而線程則是進程內的一個獨立執行單元,在不同的線程之間是可以共享進程資源的,所以在多線程的情況下,需要特別註意對臨界資源的訪問控制。在系統創建進程之後就開始啟動執行進程的主線程,而進程的生命周期和這個主線程的生命周期一致,主線程的退出也就意味著進程的終止和銷毀。主線程是由系統進程所創建的,同時用戶也可以自主創建其它線程,這一系列的線程都會並發地運行於同一個進程中。

CPU

CPU是計算機的核心,其負責承擔計算機的計算任務。這裏我們比喻為一個工廠,這裏將進程比喻為工廠的車間,它代表CPU所能處理的單個任務。任一時刻,CPU總是運行一個進程,其他進程處於非運行狀態。

多進程和多線程

  • 多進程:多進程指的是在同一個時間裏,同一個計算機系統中如果允許兩個或兩個以上的進程處於運行狀態。多進程帶來的好處是明顯的,比如你可以聽歌的同時,打開編輯器敲代碼,編輯器和聽歌軟件的進程之間絲毫不會相互幹擾。
  • 多線程是指程序中包含多個執行流,即在一個程序中可以同時運行多個不同的線程來執行不同的任務,也就是說允許單個程序創建多個並行執行的線程來完成各自的任務。

瀏覽器多進程架構

跟現在的很多多線程瀏覽器不一樣,Chrome瀏覽器使用多個進程來隔離不同的網頁。因此在Chrome中打開一個網頁相當於起了一個進程。

在瀏覽器剛被設計出來的時候,那時的網頁非常的簡單,每個網頁的資源占有率是非常低的,因此一個進程處理多個網頁時可行的。然後在今天,大量網頁變得日益復雜。把所有網頁都放進一個進程的瀏覽器面臨在健壯性,響應速度,安全性方面的挑戰。因為如果瀏覽器中的一個tab網頁崩潰的話,將會導致其他被打開的網頁應用。另外相對於線程,進程之間是不共享資源和地址空間的,所以不會存在太多的安全問題,而由於多個線程共享著相同的地址空間和資源,所以會存在線程之間有可能會惡意修改或者獲取非授權數據等復雜的安全問題。

註意: 在這裏瀏覽器應該也有自己的優化機制,有時候打開多個tab頁後,可以在Chrome任務管理器中看到,有些進程被合並了(譬如打開多個空白標簽頁後,會發現多個空白標簽頁被合並成了一個進程),所以每一個Tab標簽對應一個進程並不一定是絕對的。

除了瀏覽器的標簽頁進程之外,瀏覽器還有一些其他進程來輔助支撐標簽頁的進程,如下:
① Browser進程:瀏覽器的主進程(負責協調、主控),只有一個。作用有

  • 負責瀏覽器界面顯示,與用戶交互。如前進,後退等
  • 負責各個頁面的管理,創建和銷毀其他進程
  • 網絡資源的管理,下載等

② 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建
③ GPU進程:最多一個,用於3D繪制等
④ 瀏覽器渲染進程(瀏覽器內核),Renderer進程,內部是多線程的,也就是我們每個標簽頁所擁有的進程,互不影響,負責頁面渲染,腳本執行,事件處理等

瀏覽器內核,渲染進程

瀏覽器內核,即我們的渲染進程,有名Renderer進程,簡單來說瀏覽器內核是通過取得頁面內容、整理信息(應用CSS)、計算和組合最終輸出可視化的圖像結果,通常也被稱為渲染引擎。從上面我們可以知道,Chrome瀏覽器為每個tab頁面單獨啟用進程,因此每個tab網頁都有由其獨立的渲染引擎實例。該進程下面擁有著多個線程,靠著這些現成共同完成渲染任務。那麽這些線程是什麽呢,如下:

1.js引擎(JS內核)線程(js引擎有多個線程,一個主線程,其它的後臺配合主線程)
作用:

  • 主要負責處理Javascript腳本程序,例如V8引擎。Javascript引擎線程理所當然是負責解析Javascript腳本,運行代碼。
  • 等待任務隊列的任務的到來,然後加以處理,瀏覽器無論什麽時候都只有一個JS引擎在運行JS程序

2.GUI渲染線程
作用:

  • 負責渲染瀏覽器界面,包括解析HTML、CSS、構建DOM樹、Render樹、布局與繪制等
  • 當界面需要重繪(Repaint)或由於某種操作引發回流(reflow)時,該線程就會執行

在Javascript引擎運行腳本期間,GUI渲染線程都是處於掛起狀態的,也就是說被”凍結”了,GUI更新會被保存在一個隊列中等到引擎線程空閑時立即被執行。如果JS引擎正在進行CPU密集型計算,那麽JS引擎將會阻塞,長時間不空閑,導致渲染進程一直不能執行渲染,頁面就會看起來卡頓卡頓的,渲染不連貫,所以,要盡量避免JS執行時間過長。如果需要進行一些高耗時的計算時,可以單獨開啟一個WebWorker線程,這樣不管這個WebWorker子線程怎麽密集計算、怎麽阻塞,都不會影響JS引擎主線程,只需要等計算結束,將結果通過postMessage傳輸給主線程就可以了。

另外,還有個東西叫 SharedWorker,與WebWorker在概念上所不同。

  • WebWorker 只屬於某一個頁面,不會和其他標簽頁的Renderer進程共享,WebWorker是屬於Renderer進程創建的進程。
  • SharedWorker 是由瀏覽器單獨創建的進程來運行的JS程序,它被所有的Renderer進程所共享,在瀏覽器中,最多只能存在一個SharedWorker進程。

SharedWorker由進程管理,WebWorker是某一個Renderer進程下的線程。

3.事件觸發線程
作用:聽起來像JS的執行,但是其實歸屬於瀏覽器,而不是JS引擎。控制交互,響應用戶,當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理。這些事件可以是當前執行的代碼塊如定時任務、也可來自瀏覽器內核的其他線程如鼠標點擊、AJAX異步請求等,但由於JS的單線程關系所有這些事件都得排隊等待JS引擎處理。

4.http請求線程
作用:ajax請求等,在XMLHttpRequest在連接後是通過瀏覽器新開一個線程請求, 將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件放到 JavaScript引擎的處理隊列中等待處理。

5.定時觸發器線程
作用:setTimeout和setInteval,瀏覽器定時計數器並不是由JavaScript引擎計數的, 因為JavaScript引擎是單線程的, 如果處於阻塞線程狀態就會影響記計時的準確, 因此通過單獨線程來計時並觸發定時是更為合理的方案。

ps:W3C的HTML標準中規定,setTimeout中低與4ms的時間間隔算為4ms

onclick 由瀏覽器內核的 DOM Binding 模塊來處理,當事件觸發的時候,回調函數會立即添加到任務隊列中。

setTimeout 會由瀏覽器內核的 timer 模塊來進行延時處理,當時間到達的時候,才會將回調函數添加到任務隊列中。

ajax 則會由瀏覽器內核的 network 模塊來處理,在網絡請求完成返回之後,才將回調添加到任務隊列中。

  

鍵盤、鼠標I/O輸入輸出事件、窗口大小的resize事件、定時器(setTimeout、setInterval)事件、Ajax請求網絡I/O回調等。當這些異步任務發生的時候,它們將會被放入瀏覽器的事件任務隊列中去,所以異步是瀏覽器的兩個或者兩個以上線程共同完成的。比如ajax異步請求和setTimeout

js單線程

js運作在瀏覽器中,是單線程的,js代碼始終在一個線程上執行,此線程被稱為js引擎線程。

Javascript是單線程的, 那麽為什麽Javascript要是單線程的?

這是因為Javascript這門腳本語言誕生的使命所致:JavaScript為處理頁面中用戶的交互,以及操作DOM樹、CSS樣式樹來給用戶呈現一份動態而豐富的交互體驗和服務器邏輯的交互處理。如果JavaScript是多線程的方式來操作這些UI DOM,則可能出現UI操作的沖突; 如果Javascript是多線程的話,在多線程的交互下,處於UI中的DOM節點就可能成為一個臨界資源,假設存在兩個線程同時操作一個DOM,一個負責修改一個負責刪除,那麽這個時候就需要瀏覽器來裁決如何生效哪個線程的執行結果。當然我們可以通過鎖來解決上面的問題。但為了避免因為引入了鎖而帶來更大的復雜性,Javascript在最初就選擇了單線程執行。

ps:web worker也只是允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。

但是如果單線程,任務都需要排隊。排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閑著的,因為IO設備(輸入輸出設備)很慢(比如Ajax操作從網絡讀取數據),不得不等著結果出來,再往下執行。

JavaScript語言的設計者意識到,這時主線程完全可以不管IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回了結果,再回過頭,把掛起的任務繼續執行下去。

於是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。

同步任務和異步任務

同步任務:在主線程排隊支持的任務,前一個任務執行完畢後,執行後一個任務,形成一個執行棧,線程執行時在內存形成的空間為棧,進程形成堆結構,這是內存的結構。執行棧可以實現函數的層層調用。

異步任務會被主線程掛起,不會進入主線程,而是進入消息隊列,而且必須指定回調函數,只有消息隊列通知主線程,並且執行棧為空時,該消息對應的任務才會進入執行棧獲得執行的機會。

主線程

主線程執行: 【js的運行機制】

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

事件循環

主線程從”任務隊列”中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱為Event Loop(事件循環)。

主線程運行的時候,產生堆(heap)和棧(stack),棧中的代碼調用各種外部API,它們在”任務隊列”中加入各種事件(click,load,done)。只要棧中的代碼執行完畢,主線程就會去讀取”任務隊列”,依次執行那些事件所對應的回調函數。

JS引擎對事件隊列(宏任務)與JS引擎內的任務(微任務)執行存在著先後循序,當每執行完一個事件隊列的時間,JS引擎會檢測內部是否有未執行的任務,如果有,將會優先執行(微任務)。

執行棧中的代碼(同步任務),總是在讀取”任務隊列”(異步任務)之前執行。

Promise

在JS中ES6 中新增的任務隊列(promise)是在事件循環之上的,事件循環每次 tick 後會查看 ES6 的任務隊列中是否有任務要執行,也就是 ES6 的任務隊列比事件循環中的任務(事件)隊列優先級更高。

如 Promise 就使用了 ES6 的任務隊列特性。也即在執行完任務棧後首先執行的是任務隊列中的promise任務。其他的上面常見的異步操作加入隊列的時間沒有相應的優先級。

setTimeout(function(){console.log(‘111‘)},0);//事件循環隊列
new Promise(function(resolve,reject){
   console.log("2222");//此處還沒有執行異步操作,執行異步操作及執行回調函數,在promise中即then中的回調
  resolve();
}).then(function(){console.log(‘3333‘)})//promise
console.log("44444");//主線程
//輸出
 2222
 44444//上面的兩個輸出屬於同步操作
 3333//promise加入到隊列的優先級高於setTimeout
 111

  

同時在嵌套異步操作中,會將嵌套的異步加入到下次的任務隊列中,以此類推(如嵌套的promise)

new Promise(function(resolve,reject){
  resolve();
}).then(function(){
    console.log("111");
    return new Promise(function(resolve,reject){
   resolve();
})
}).then(function(){ console.log("222");})

new Promise( function(resolve,reject){
    resolve();
}).then(function(){ console.log("33333");})
//輸出
33333 222  

瀏覽器的渲染過程

看到這裏,首先,應該對瀏覽器內的進程和線程都有一定理解了,那麽接下來,再談談瀏覽器的Browser進程(控制進程)是如何和內核通信的,
這點也理解後,就可以將這部分的知識串聯起來,從頭到尾有一個完整的概念。

如果自己打開任務管理器,然後打開一個瀏覽器,就可以看到:任務管理器中出現了兩個進程(一個是主控進程,一個則是打開Tab頁的渲染進程)
然後在這前提下,看下整個的過程:(簡化了很多)

    • Browser進程收到用戶請求,首先需要獲取頁面內容(譬如通過網絡下載資源),隨後將該任務通過RendererHost接口傳遞給Render進程
    • Renderer進程的Renderer接口收到消息,簡單解釋後,交給渲染線程,然後開始渲染

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

每個瀏覽器內核的渲染流程不一樣,下面我們主要以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的單線程