[TOC]css
**- 什麼是進程** 狹義定義:進程是正在運行的程序的實例(an instance of a computer program that is being executed)。 廣義定義:進程是一個具備必定獨立功能的程序關於某個數據集合的一次運行活動。它是操做系統動態執行的基本單元,在傳統的操做系統中,進程既是基本的分配單元,也是基本的執行單元。 **- 什麼是線程** 線程(英語:thread)是操做系統可以進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運做單位。一條線程指的是進程中一個單一順序的控制流,一個進程中能夠併發多個線程,每條線程並行執行不一樣的任務。
在當代面向線程設計的計算機結構中,進程是線程的容器。html
一個進程有一個或多個線程,線程之間共同完成進程分配下來的任務。先看個形象的比喻:前端
- 進程是一個工廠,工廠有它的獨立資源 - 工廠之間相互獨立 - 線程是工廠中的工人,多個工人協做完成任務 - 工廠內有一個或多個工人 - 工人之間共享空間
再完善完善概念:ios
- 工廠的資源 -> 系統分配的內存(獨立的一塊內存) - 工廠之間的相互獨立 -> 進程之間相互獨立 - 多個工人協做完成任務 -> 多個線程在進程中協做完成任務 - 工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成 - 工人之間共享空間 -> 同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)
而後再鞏固下:es6
能夠打開任務管理器,能夠看到有一個後臺進程列表。這裏就是查看進程的地方,並且能夠看到每一個進程的內存資源信息以及cpu佔有率。web
進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位),線程是cpu調度的最小單位(線程是創建在進程的基礎上的一次程序運行單位)。ajax
通常通用的說法:單線程與多線程,都是指在一個進程內的單和多。(因此核心仍是得屬於一個進程才行)canvas
理解了進程與線程了區別後,接下來對瀏覽器進行必定程度上的認識:(先看下簡化理解)segmentfault
圖中打開了Chrome瀏覽器的多個標籤頁,而後能夠在Chrome的任務管理器中看到有多個進程(分別是每個Tab頁面有一個獨立的進程,以及一個主進程)。api
注意:在這裏瀏覽器應該也有本身的優化機制,有時候打開多個tab頁後,能夠在Chrome任務管理器中看到,有些進程被合併了(譬如打開多個空白標籤頁後,會發現多個空白標籤頁被合併成了一個進程),因此每個Tab標籤對應一個進程並不必定是絕對的。
除了瀏覽器的標籤頁進程以外,瀏覽器還有一些其餘進程來輔助支撐標籤頁的進程,主要包含哪些進程:(爲了簡化理解,僅列舉主要進程)
Browser進程:瀏覽器的主進程(負責協調、主控),只有一個。做用有
瀏覽器渲染進程(瀏覽器內核)(Renderer進程,內部是多線程的):默認每一個Tab頁面一個進程,互不影響。主要做用爲
強化記憶:在瀏覽器中打開一個網頁至關於新起了一個進程(進程內有本身的多線程)
相比於單進程瀏覽器,多進程有以下優勢:
簡單點理解:若是瀏覽器是單進程,那麼某個Tab頁崩潰了,就影響了整個瀏覽器,體驗有多差;同理若是是單進程,插件崩潰了也會影響整個瀏覽器;
瀏覽器內核,即咱們的渲染進程,有名Renderer進程,咱們頁面的渲染,js的執行,事件的循環都在這一進程內進行,也就是說,該進程下面擁有着多個線程,靠着這些現成共同完成渲染任務。
對於普通的前端操做來講,頁面的渲染,JS的執行,事件的循環,都在這個進程內進行。瀏覽器的渲染進程是多線程的。
1.GUI渲染線程【圖形用戶界面(Graphical User Interface,簡稱 GUI,又稱圖形用戶接口)】
2.JS引擎線程
3.事件觸發線程
4.定時觸發器線程
5.異步http請求線程
爲何JS引擎是單線程的?爲何須要異步? 單線程又是如何實現異步的呢? 查看【連接描述】
若是本身打開任務管理器,而後打開一個瀏覽器,就能夠看到:任務管理器中出現了兩個進程(一個是主控進程,一個則是打開Tab頁的渲染進程),
而後在這前提下,看下整個的過程:(簡化了不少)
Renderer進程的Renderer接口收到消息,簡單解釋後,交給渲染線程,而後開始渲染
這裏繪一張簡單的圖:(很簡化)
JavaScript做爲一門客戶端的腳本語言,主要的任務是處理用戶的交互,而用戶的交互無非就是響應DOM的增刪改,使用事件隊列的形式,一次事件循環只處理一個事件響應,使得腳本執行相對連續。若是JS引擎被設計爲多線程的,那麼DOM之間必然會存在資源競爭,那麼語言的實現會變得很是臃腫,在客戶端跑起來,資源的消耗和性能將會是不太樂觀的,故設計爲單線程的形式,並附加一些其餘的線程來實現異步的形式,這樣運行成本相對於使用JS多線程來講下降了不少。
因爲JavaScript是可操縱DOM的,若是在修改這些元素屬性同時渲染界面(即JS線程和UI線程同時運行),那麼渲染線程先後得到的元素數據就可能不一致了。
所以爲了防止渲染出現不可預期的結果,瀏覽器設置GUI渲染線程與JS引擎爲互斥的關係,當JS引擎執行時GUI線程會被掛起,
GUI更新則會被保存在一個隊列中等到JS引擎線程空閒時當即被執行。
從上述的互斥關係,能夠推導出,JS若是執行時間過長就會阻塞頁面。
譬如,假設JS引擎正在進行巨量的計算,此時就算GUI有更新,也會被保存到隊列中,等待JS引擎空閒後執行。
而後,因爲巨量計算,因此JS引擎極可能好久好久後才能空閒,天然會感受到巨卡無比。
因此,要儘可能避免JS執行時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞的感受。
css加載是否會阻塞dom樹渲染
這裏說的是頭部引入css
的狀況
首先,咱們都知道:css
是由單獨的下載線程異步下載的。
而後還有幾個現象:
css
加載不會阻塞DOM
樹解析(異步加載時dom
照常構建)render
樹渲染(渲染時須要等css
加載完畢,由於render
樹須要css
信息)這可能也是瀏覽器的一種優化機制
由於你加載css
的時候,可能會修改下面DOM
節點的樣式,若是css
加載不阻塞render
樹渲染的話,那麼當css
加載完以後,render
樹可能又得從新重繪或者回流了,這就形成了一些沒有必要的損耗
因此乾脆把DOM
樹的結構先解析完,把能夠作的工做作完,而後等css
加載完以後,在根據最終的樣式來渲染render
樹,這種作法確實對性能好一點。
事件觸發線程、定時觸發器線程、異步HTTP請求線程三個線程有一個共同點,那就是使用回調函數的形式,當知足了特定的條件,這些回調函數會被執行。這些回調函數被瀏覽器內核理解成事件,在瀏覽器內核中擁有一個事件隊列,這三個線程當知足了內部特定的條件,會將這些回調函數添加到事件隊列中,等待JS引擎空閒執行。例如異步HTTP請求線程,線程若是檢測到請求的狀態變動,若是設置有回調函數,回調函數會被添加事件隊列中,等待JS引擎空閒了執行。
可是,JS引擎對事件隊列(宏任務)與JS引擎內的任務(微任務)執行存在着前後循序,當每執行完一個事件隊列的時間,JS引擎會檢測內部是否有未執行的任務,若是有,將會優先執行(微任務)。
由於JS引擎是單線程的,當JS執行時間過長會頁面阻塞,那麼JS就真的對CPU密集型計算無能爲力麼?
因此,後來HTML5中支持了 Web Worker。
來自MDN的官方解釋
Web Workers 使得一個Web應用程序能夠在與主執行線程分離的後臺線程中運行一個腳本操做。這樣作的好處是能夠在一個單獨的線程中執行費時的處理任務,從而容許主(一般是UI)線程運行而不被阻塞/放慢。
這樣理解下:
建立Worker
時,JS
引擎向瀏覽器申請開一個子線程(子線程是瀏覽器開的,徹底受主線程控制,並且不能操做DOM
)JS
引擎線程與worker
線程間經過特定的方式通訊(postMessage API
,須要經過序列化對象來與線程交互特定的數據)
因此,若是有很是耗時的工做,請單獨開一個Worker
線程,這樣裏面無論如何翻天覆地都不會影響JS
引擎主線程,只待計算出結果後,將結果通訊給主線程便可,perfect!
並且注意下,JS
引擎是單線程的,這一點的本質仍然未改變,Worker
能夠理解是瀏覽器給JS
引擎開的外掛,專門用來解決那些大量計算問題。
注意點:
因此,若是須要進行一些高耗時的計算時,能夠單獨開啓一個WebWorker線程,這樣無論這個WebWorker子線程怎麼密集計算、怎麼阻塞,都不會影響JS引擎主線程,只須要等計算結束,將結果經過postMessage傳輸給主線程就能夠了。
另外,還有個東西叫 SharedWorker
,與WebWorker在概念上所不一樣。
Chrome
在Render
進程中(每個Tab
頁就是一個render
進程)建立一個新的線程來運行Worker
中的JavaScript
程序。SharedWorker由進程管理,WebWorker是某一個Renderer進程下的線程。
看到這裏,應該就很容易明白了,本質上就是進程和線程的區別。SharedWorker
由獨立的進程管理,WebWorker
只是屬於render
進程下的一個線程
每一個瀏覽器內核的渲染流程不同,下面咱們主要以webkit
爲主。
首先是渲染的前奏:
在說渲染以前,須要理解一些概念:
reflow
。reflow 會從 <html> 這個 root frame 開始遞歸往下,依次計算全部的結點幾何尺寸和位置。reflow 幾乎是沒法避免的。如今界面上流行的一些效果,好比樹狀目錄的摺疊、展開(實質上是元素的顯 示與隱藏)等,都將引發瀏覽器的 reflow。鼠標滑過、點擊……只要這些行爲引發了頁面上某些元素的佔位面積、定位方式、邊距等屬性的變化,都會引發它內部、周圍甚至整個頁面的從新渲 染。一般咱們都沒法預估瀏覽器到底會 reflow 哪一部分的代碼,它們都彼此相互影響着。注意:display:none
的節點不會被加入Render Tree,而visibility: hidden
則會,因此display:none
會觸發reflow
,visibility: hidden
會觸發repaint
。
瀏覽器內核拿到響應報文以後,渲染大概分爲如下步驟
詳細步驟略去,大概步驟以下,渲染完畢後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 -> load
渲染步驟就提到了composite
概念;瀏覽器渲染的圖層通常包含兩大類:普通圖層以及複合圖層。
absolute
佈局(fixed
也同樣),雖然能夠脫離文檔流,但它仍然屬於默認複合層能夠簡單理解下:GPU
中,各個複合圖層是單獨繪製的,因此互不影響,這也是爲何某些場景硬件加速效果一級棒
如何變成複合圖層(硬件加速)
將元素變成一個複合圖層,就是傳說中的硬件加速技術
translate3d
,translatez
opacity
屬性/過渡動畫(須要動畫執行的過程當中纔會建立合成層,動畫沒有開始或結束後元素還會回到以前的狀態)will-chang
屬性(這個比較偏僻),通常配合opacity
與translate
使用(並且經測試,除了上述能夠引起硬件加速的屬性外,其它屬性並不會變成複合層),做用是提早告訴瀏覽器要變化,這樣瀏覽器會開始作一些優化工做(這個最好用完後就釋放)<video><iframe><canvas><webgl>
等元素flash
插件absolute和硬件加速的區別
能夠看到,absolute
雖然能夠脫離普通文檔流,可是沒法脫離默認複合層。
因此,就算absolute
中信息改變時不會改變普通文檔流中render
樹,可是,瀏覽器最終繪製時,是整個複合層繪製的,因此absolute
中信息的改變,仍然會影響整個複合層的繪製。(瀏覽器會重繪它,若是複合層中內容多,absolute
帶來的繪製信息變化過大,資源消耗是很是嚴重的)
而硬件加速直接就是在另外一個複合層了(另起爐竈),因此它的信息改變不會影響默認複合層(固然了,內部確定會影響屬於本身的複合層),僅僅是引起最後的合成(輸出視圖)
複合圖層的做用
通常一個元素開啓硬件加速後會變成複合圖層,能夠獨立於普通文檔流中,改動後能夠避免整個頁面重繪,提高性能。
可是儘可能不要大量使用複合圖層,不然因爲資源消耗過分,頁面反而會變的更卡。
硬件加速時請使用index
使用硬件加速時,儘量的使用index,防止瀏覽器默認給後續的元素建立複合層渲染
具體的原理是:webkit CSS3
中,若是這個元素添加了硬件加速,而且index
層級比較低,那麼在這個元素的後面其它元素(層級比這個元素高的,或者相同的,而且relective
或absolute
屬性相同的),會默認變爲複合層渲染,若是處理不當會極大的影響性能
簡單點理解,能夠認爲是一個隱式合成的概念:若是a是一個複合層,並且b在a上面,那麼b也會被隱式轉爲一個複合圖層,這點須要特別注意
到此時,已是屬於瀏覽器頁面初次渲染完畢後的事情,JS
引擎的一些運行機制分析。主要是結合Event Loop
來談JS
代碼是如何執行的。
咱們已經知道了JS
引擎是單線程的,知道了JS
引擎線程,事件觸發線程,定時觸發器線程。
而後還須要知道:
JS
分爲同步任務和異步任務JS
引擎空閒),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧,開始執行。看到這裏,應該就能夠理解了:爲何有時候setTimeOut
推入的事件不能準時執行?由於可能在它推入到事件列表時,主線程還不空閒,正在執行其它代碼,因此就必須等待,天然有偏差。
主線程在運行時會產生執行棧,棧中的代碼調用某些api
時,它們會在事件隊列中添加各類事件(當知足觸發條件後,如ajax
請求完畢)。而當棧中的代碼執行完畢,就會去讀取事件隊列中的事件,去執行那些回調,如此循環。
上面事件循環機制的核心是:JS
引擎線程和事件觸發線程
調用setTimeout
後,是由定時器線程控制等到特定時間後添加到事件隊列的,由於JS
引擎是單線程的,若是處於阻塞線程狀態就會影響計時準確,所以頗有必要另開一個線程用來計時。
當使用setTimout
或setInterval
時,須要定時器線程計時,計時完成後就會將特定的事件推入事件隊列中。
如:
setTimeout(()=>console.log('hello!),1000) //等1000毫秒計時完畢後(由定時器線程計時),將回調函數推入事件隊列中,等待主線程執行 setTimeout(()=>{ console.log('hello') },0) console.log('begin')
這段代碼的效果是最快的時間內將回調函數推入事件隊列中,等待主線程執行。
注意:
begin
,後hello
0
毫秒就推入事件隊列,可是W3C
在HTML
標準中規定,規定要求setTimeout
中低於4ms
的時間間隔算爲4ms
4ms
,就算假設0
毫秒就推入事件隊列,也會先執行begin
(由於只能可執行棧內空了後纔會主動讀取事件隊列)setInterval
用setTimeout
模擬按期計時和直接用setInterval
是有區別的:
setTimeout
計時到後就會去執行,而後執行一段時間後纔會繼續setTimeout
,中間就多了偏差setInterval
則是每次都精確的隔一段時間推入一個事件(可是,事件的實際執行時間不必定就準確,還有多是這個事件還沒執行完畢,下一個事件就來了)並且setInterval
有一些比較致命的問題:
setInterval
代碼在setInterval
再次添加到隊列以前尚未完成執行,就會致使定時器代碼連續運行好幾回,而之間沒有間隔,就算正常間隔執行,多個setInterval
的代碼執行時間可能會比預期小(由於代碼執行須要必定時間)ios
的webview
,或者safari
等瀏覽器中都有一人特色,在滾動的時候是不執行JS
的,若是使用了setInterval
,會發如今滾動結束後會執行屢次因爲滾動不執行JS
積攢回調,若是回調執行時間過長,就會很是容易形成卡頓問題和一些不可知的錯誤(setInterval
自帶的優化,若是當前事件隊列中有setInterval
的回調,不會重複添加回調)setInterval
並非不執行程序,它會把setInterval
的回調函數放在隊列中,等瀏覽器窗口再次打開時,一瞬間所有執行因此,至於這麼問題,通常認爲的最佳方案是:用setTimeout
模擬setInterval
或者特殊場合直接用requestAnimationFrame
Promise
時代的microtask
與macrotask
在es6
盛行的如今,能夠看下這題:
console.log('script start'); setTimeout(()=>{ console.log('setTimeout') },0); Promise.resolve() .then(()=>console.log('promise1')) .then(()=>console.log('promise2')) console.log('script end') //執行結果: script start script end promise1 promise2 setTimeout
由於promise
有一個新的概念microtask
.或者能夠說JS
中分爲兩種任務:macrotask
和microtask
;
理解以下:
macrotask
(又叫宏任務),主代碼塊,setTimeout
,setInterval
等(能夠看到,事件隊列中的每個事件都是一個macrotask
)macrotask
會從頭至尾將這個任務執行完畢,不會執行其它JS
內部macrotask
與DOM
任務可以有序的執行,會在一個macrotask
執行結束後,在下一個macrotask
執行開始前,對頁面進行從新渲染(task
->渲染->task
->...)microtask
(又叫微任務),Promise
,process.nextTick
等。macrotask
執行結束後當即執行的任務macrotask
任務後,下一個macrotask
以前,在渲染以前setTimeout
(setTimeout
是macrotask
)會更快由於無需等待渲染macrotask
執行完成後,就會將在它執行期間產生的全部microtask
都執行完畢(在渲染前)注意:在Node
環境下,process.nextTick
的優先級高於promise
.也就是:在宏任務結束後會先執行微任務隊列中的nextTick
部分,而後纔會執行微任務中的promise
部分。
另外,setImmediate
則是規定:在下一次Event Loop
(宏任務)時觸發(因此它是屬於優先級較高的宏任務),(Node.js
文檔中稱,setImmediate
指定的回調函數,老是排在setTimeout
前面),因此setImmediate
若是嵌套的話,是須要通過多個Loop
才能完成的,而不會像process.nextTick
同樣沒完沒了。
能夠理解:
macrotask
中的事件都是放在一個事件隊列中的,而這個隊列由事件觸發線程維護.microtask
中的全部微任務都是添加到微任務隊列中,等待當前macrotask
執行完後執行,而這個隊列由JS
引擎線程維護。因此:
JS
線程繼續接管,開始下一個宏任務(從事件隊列中獲取)new Promise裏的函數是直接執行的算作主程序裏,並且.then後面的纔會放到微任務中。
另外,請注意下Promise的polyfill與官方版本的區別:
官方版本中,是標準的microtask
形式polyfill
,通常都是經過setTimeout
模擬的,因此是macrotask
形式
請特別注意這兩點區別
注意,有一些瀏覽器執行結果不同(由於它們可能把microtask
當成macrotask
來執行了),可是爲了簡單,這裏不描述一些不標準的瀏覽器下的場景(但記住,有些瀏覽器可能並不標準)
Mutation Observer
能夠用來實現microtask
(它屬於microtask
,優先級小於Promise
,通常是Promise
不支持時纔會這樣作)
它是HTML5
中的新特性,做用是:監聽一個DOM
變更,當DOM
對象樹發生任何變更時,Mutation Observer
會獲得通知
像之前的Vue
源碼中就是利用它來模擬nextTick
的,具體原理是,建立一個TextNode
並監聽內容變化,而後要nextTick
的時候去改一下這個節點的文本內容,以下:(Vue
的源碼,未修改)
var counter=1 var observer=newMutationObserver(nextTickHandler) var textNode=document.createTextNode(String(counter)) observer.observe(textNode,{characterData:true}) timerFunc=()=>{ counter=(counter+1)%2 textNode.data=String(counter) }
不過,如今的Vue(2.5+)
的nextTick
實現移除了Mutation Observer
的方式(聽說是兼容性緣由),取而代之的是使用MessageChannel
(固然,默認狀況仍然是Promise
,不支持才兼容的)。
MessageChannel
屬於宏任務,優先級是:setImmediate->MessageChannel->setTimeout
,因此Vue(2.5+)
內部的nextTick
與2.4
及以前的實現是不同的,須要注意下。