瀏覽器內部 進程線程 筆記

本文引用於:http://www.dailichun.com/2018/01/21/js_singlethread_eventloop.html

1.瀏覽器都包含哪些進程?

Browser進程:瀏覽器的主進程(負責協調、主控),只有一個。作用有

  • 負責瀏覽器界面顯示,與用戶交互。如前進,後退等

  • 負責各個頁面的管理,創建和銷燬其他進程

  • 將Renderer進程得到的內存中的Bitmap,繪製到用戶界面上

  • 網絡資源的管理,下載等

第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建

GPU進程:最多一個,用於3D繪製等

瀏覽器渲染進程(瀏覽器內核)(Renderer進程,內部是多線程的):默認每個Tab頁面一個進程,互不影響。主要作用爲頁面渲染,腳本執行,事件處理等

2.瀏覽器多進程的優勢

  • 避免單個page crash影響整個瀏覽器

  • 避免第三方插件crash影響整個瀏覽器

  • 多進程充分利用多核優勢

  • 方便使用沙盒模型隔離插件等進程,提高瀏覽器穩定性

3.重點是瀏覽器內核(渲染進程)

  1. GUI渲染線程

    • 負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。

    • 當界面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該線程就會執行

    • 注意,GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(相當於被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閒時立即被執行。

  2. JS引擎線程

    • 也稱爲JS內核,負責處理Javascript腳本程序。(例如V8引擎)

    • 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引擎執行。

4.簡單梳理下瀏覽器渲染流程

本來是直接計劃開始談JS運行機制的,但想了想,既然上述都一直在談瀏覽器,直接跳到JS可能再突兀,因此,中間再補充下瀏覽器的渲染流程(簡單版本)

爲了簡化理解,前期工作直接省略成:(要展開的或完全可以寫另一篇超長文)

瀏覽器器內核拿到內容後,渲染大概可以劃分成以下幾個步驟:

  1. 解析html建立dom樹

  2. 解析css構建render樹(將CSS代碼解析成樹形的數據結構,然後結合DOM合併成render樹)

  3. 佈局render樹(Layout/reflow),負責各元素尺寸、位置的計算

  4. 繪製render樹(paint),繪製頁面像素信息

  5. 瀏覽器會將各層的信息發送給GPU,GPU會將各層合成(composite),顯示在屏幕上。

所有詳細步驟都已經略去,渲染完畢後就是load事件了,之後就是自己的JS邏輯處理了

既然略去了一些詳細的步驟,那麼就提一些可能需要注意的細節把。

這裏重繪參考來源中的一張圖:(參考來源第一篇)

5.從Event Loop談JS的運行機制

到此時,已經是屬於瀏覽器頁面初次渲染完畢後的事情,JS引擎的一些運行機制分析。

注意,這裏不談可執行上下文VOscop chain等概念(這些完全可以整理成另一篇文章了),這裏主要是結合Event Loop來談JS代碼是如何執行的。

讀這部分的前提是已經知道了JS引擎是單線程,而且這裏會用到上文中的幾個概念:(如果不是很理解,可以回頭溫習)

  • JS引擎線程

  • 事件觸發線程

  • 定時觸發器線程

然後再理解一個概念:

  • JS分爲同步任務和異步任務

  • 同步任務都在主線程上執行,形成一個執行棧

  • 主線程之外,事件觸發線程管理着一個任務隊列,只要異步任務有了運行結果,就在任務隊列之中放置一個事件。

  • 一旦執行棧中的所有同步任務執行完畢(此時JS引擎空閒),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中,開始執行。

看圖

看到這裏,應該就可以理解了:爲什麼有時候setTimeout推入的事件不能準時執行?因爲可能在它推入到事件列表時,主線程還不空閒,正在執行其它代碼, 所以自然有誤差。

事件循環機制進一步補充

這裏就直接引用一張圖片來協助理解:(參考自Philip Roberts的演講《Help, I’m stuck in an event-loop》)

上圖大致描述就是:

  • 主線程運行時會產生執行棧, 棧中的代碼調用某些api時,它們會在事件隊列中添加各種事件(當滿足觸發條件後,如ajax請求完畢)

  • 而棧中的代碼執行完畢,就會讀取事件隊列中的事件,去執行那些回調

  • 如此循環

  • 注意,總是要等待棧中的代碼執行完畢後纔會去讀取事件隊列中的事件

6.單獨說說定時器

上述事件循環機制的核心是:JS引擎線程和事件觸發線程

但事件上,裏面還有一些隱藏細節,譬如調用setTimeout後,是如何等待特定時間後才添加到事件隊列中的?

是JS引擎檢測的麼?當然不是了。它是由定時器線程控制(因爲JS引擎自己都忙不過來,根本無暇分身)

爲什麼要單獨的定時器線程?因爲JavaScript引擎是單線程的, 如果處於阻塞線程狀態就會影響記計時的準確,因此很有必要單獨開一個線程用來計時。

什麼時候會用到定時器線程?當使用setTimeoutsetInterval,它需要定時器線程計時,計時完成後就會將特定的事件推入事件隊列中。

譬如:

這段代碼的作用是當1000毫秒計時完畢後(由定時器線程計時),將回調函數推入事件隊列中,等待主線程執行

這段代碼的效果是最快的時間內將回調函數推入事件隊列中,等待主線程執行

注意:

  • 執行結果是:先beginhello!

  • 雖然代碼的本意是0毫秒後就推入事件隊列,但是W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算爲4ms。 (不過也有一說是不同瀏覽器有不同的最小時間設定)

  • 就算不等待4ms,就算假設0毫秒就推入事件隊列,也會先執行begin(因爲只有可執行棧內空了後纔會主動讀取事件隊列)

 

7.setTimeout而不是setInterval

用setTimeout模擬定期計時和直接用setInterval是有區別的。

因爲每次setTimeout計時到後就會去執行,然後執行一段時間後纔會繼續setTimeout,中間就多了誤差 (誤差多少與代碼執行時間有關)

而setInterval則是每次都精確的隔一段時間推入一個事件 (但是,事件的實際執行時間不一定就準確,還有可能是這個事件還沒執行完畢,下一個事件就來了)

而且setInterval有一些比較致命的問題就是:

  • 累計效應(上面提到的),如果setInterval代碼在(setInterval)再次添加到隊列之前還沒有完成執行, 就會導致定時器代碼連續運行好幾次,而之間沒有間隔。 就算正常間隔執行,多個setInterval的代碼執行時間可能會比預期小(因爲代碼執行需要一定時間)

  • 而且把瀏覽器最小化顯示等操作時,setInterval並不是不執行程序, 它會把setInterval的回調函數放在隊列中,等瀏覽器窗口再次打開時,一瞬間全部執行時

所以,鑑於這麼多但問題,目前一般認爲的最佳方案是:用setTimeout模擬setInterval,或者特殊場合直接用requestAnimationFrame

補充:JS高程中有提到,JS引擎會對setInterval進行優化,如果當前事件隊列中有setInterval的回調,不會重複添加。不過,仍然是有很多問題。。。

7.事件循環進階:macrotask與microtask

這段參考了參考來源中的第2篇文章(英文版的),(加了下自己的理解重新描述了下), 強烈推薦有英文基礎的同學直接觀看原文,作者描述的很清晰,示例也很不錯,如下:

https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

上文中將JS事件循環機制梳理了一遍,在ES5的情況是夠用了,但是在ES6盛行的現在,仍然會遇到一些問題,譬如下面這題:

嗯哼,它的正確執行順序是這樣子的:

爲什麼呢?因爲Promise裏有了一個一個新的概念:microtask

或者,進一步,JS中分爲兩種任務類型:macrotaskmicrotask,在ECMAScript中,microtask稱爲jobs,macrotask可稱爲task

它們的定義?區別?簡單點可以按如下理解:

  • macrotask(又稱之爲宏任務),可以理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)

    • 每一個task會從頭到尾將這個任務執行完畢,不會執行其它

    • 瀏覽器爲了能夠使得JS內部task與DOM任務能夠有序的執行,會在一個task執行結束後,在下一個 task 執行開始前,對頁面進行重新渲染 (task->渲染->task->...

  • microtask(又稱爲微任務),可以理解是在當前 task 執行結束後立即執行的任務

    • 也就是說,在當前task任務後,下一個task之前,在渲染之前

    • 所以它的響應速度相比setTimeout(setTimeout是task)會更快,因爲無需等渲染

    • 也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的所有microtask都執行完畢(在渲染前)

分別很麼樣的場景會形成macrotask和microtask呢?

  • macrotask:主代碼塊,setTimeout,setInterval等(可以看到,事件隊列中的每一個事件都是一個macrotask)

  • microtask:Promise,process.nextTick等

補充:在node環境下,process.nextTick的優先級高於Promise,也就是可以簡單理解爲:在宏任務結束後會先執行微任務隊列中的nextTickQueue部分,然後纔會執行微任務中的Promise部分。

另外,setImmediate則是規定:在下一次Event Loop(宏任務)時觸發(所以它是屬於優先級較高的宏任務), (Node.js文檔中稱,setImmediate指定的回調函數,總是排在setTimeout前面), 所以setImmediate如果嵌套的話,是需要經過多個Loop才能完成的, 而不會像process.nextTick一樣沒完沒了。

參考:https://segmentfault.com/q/1010000011914016

再根據線程來理解下:

  • macrotask中的事件都是放在一個事件隊列中的,而這個隊列由事件觸發線程維護

  • microtask中的所有微任務都是添加到微任務隊列(Job Queues)中,等待當前macrotask執行完畢後執行,而這個隊列由JS引擎線程維護 (這點由自己理解+推測得出,因爲它是在主線程下無縫執行的)

所以,總結下運行機制:

  • 執行一個宏任務(棧中沒有就從事件隊列中獲取)

  • 執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中

  • 宏任務執行完畢後,立即執行當前微任務隊列中的所有微任務(依次執行)

  • 當前宏任務執行完畢,開始檢查渲染,然後GUI線程接管渲染

  • 渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)

另外,請注意下Promisepolyfill與官方版本的區別:

  • 官方版本中,是標準的microtask形式

  • polyfill,一般都是通過setTimeout模擬的,所以是macrotask形式

  • 請特別注意這兩點區別

注意,有一些瀏覽器執行結果不一樣(因爲它們可能把microtask當成macrotask來執行了), 但是爲了簡單,這裏不描述一些不標準的瀏覽器下的場景(但記住,有些瀏覽器可能並不標準)

20180126補充:使用MutationObserver實現microtask

MutationObserver可以用來實現microtask (它屬於microtask,優先級小於Promise, 一般是Promise不支持時纔會這樣做)

它是HTML5中的新特性,作用是:監聽一個DOM變動, 當DOM對象樹發生任何變動時,Mutation Observer會得到通知

像以前的Vue源碼中就是利用它來模擬nextTick的, 具體原理是,創建一個TextNode並監聽內容變化, 然後要nextTick的時候去改一下這個節點的文本內容, 如下:(Vue的源碼,未修改)

不過,現在的Vue(2.5+)的nextTick實現移除了MutationObserver的方式(據說是兼容性原因), 取而代之的是使用MessageChannel (當然,默認情況仍然是Promise,不支持才兼容的)。

MessageChannel屬於宏任務,優先級是:MessageChannel->setTimeout, 所以Vue(2.5+)內部的nextTick與2.4及之前的實現是不一樣的,需要注意下。

這裏不展開,可以看下https://juejin.im/post/5a1af88f5188254a701ec230

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章