看法有限,若有描述不當之處,請幫忙及時指出,若有錯誤,會及時修正。css
----------超長文+多圖預警,須要花費很多時間。----------html
若是看完本文後,還對進程線程傻傻分不清,不清楚瀏覽器多進程、瀏覽器內核多線程、JS單線程、JS運行機制的區別。那麼請回復我,必定是我寫的還不夠清晰,我來改。。。前端
----------正文開始----------vue
最近發現有很多介紹JS單線程運行機制的文章,可是發現不少都僅僅是介紹某一部分的知識,並且各個地方的說法還不統一,容易形成困惑。
所以準備梳理這塊知識點,結合已有的認知,基於網上的大量參考資料,
從瀏覽器多進程到JS單線程,將JS引擎的運行機制系統的梳理一遍。node
展示形式:因爲是屬於系統梳理型,就沒有由淺入深了,而是從頭至尾的梳理知識體系,
重點是將關鍵節點的知識點串聯起來,而不是僅僅剖析某一部分知識。git
內容是:從瀏覽器進程,再到瀏覽器內核運行,再到JS引擎單線程,再到JS事件循環機制,從頭至尾系統的梳理一遍,擺脫碎片化,造成一個知識體系github
目標是:看完這篇文章後,對瀏覽器多進程,JS單線程,JS事件循環機制這些都能有必定理解,
有一個知識體系骨架,而不是似懂非懂的感受。web
另外,本文適合有必定經驗的前端人員,新手請規避,避免受到過多的概念衝擊。能夠先存起來,有了必定理解後再看,也能夠分紅多批次觀看,避免過分疲勞。ajax
瀏覽器是多進程的canvas
梳理瀏覽器內核中線程之間的關係
簡單梳理下瀏覽器渲染流程
從Event Loop談JS的運行機制
線程和進程區分不清,是不少新手都會犯的錯誤,沒有關係。這很正常。先看看下面這個形象的比喻:
- 進程是一個工廠,工廠有它的獨立資源 - 工廠之間相互獨立 - 線程是工廠中的工人,多個工人協做完成任務 - 工廠內有一個或多個工人 - 工人之間共享空間
再完善完善概念:
- 工廠的資源 -> 系統分配的內存(獨立的一塊內存) - 工廠之間的相互獨立 -> 進程之間相互獨立 - 多個工人協做完成任務 -> 多個線程在進程中協做完成任務 - 工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成 - 工人之間共享空間 -> 同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)
而後再鞏固下:
若是是windows電腦中,能夠打開任務管理器,能夠看到有一個後臺進程列表。對,那裏就是查看進程的地方,並且能夠看到每一個進程的內存資源信息以及cpu佔有率。
因此,應該更容易理解了:進程是cpu資源分配的最小單位(系統會給它分配內存)
最後,再用較爲官方的術語描述一遍:
tips
理解了進程與線程了區別後,接下來對瀏覽器進行必定程度上的認識:(先看下簡化理解)
關於以上幾點的驗證,請再第一張圖:
圖中打開了Chrome
瀏覽器的多個標籤頁,而後能夠在Chrome的任務管理器
中看到有多個進程(分別是每個Tab頁面有一個獨立的進程,以及一個主進程)。
感興趣的能夠自行嘗試下,若是再多打開一個Tab頁,進程正常會+1以上
注意:在這裏瀏覽器應該也有本身的優化機制,有時候打開多個tab頁後,能夠在Chrome任務管理器中看到,有些進程被合併了
(因此每個Tab標籤對應一個進程並不必定是絕對的)
知道了瀏覽器是多進程後,再來看看它到底包含哪些進程:(爲了簡化理解,僅列舉主要進程)
Browser進程:瀏覽器的主進程(負責協調、主控),只有一個。做用有
瀏覽器渲染進程(瀏覽器內核)(Renderer進程,內部是多線程的):默認每一個Tab頁面一個進程,互不影響。主要做用爲
強化記憶:在瀏覽器中打開一個網頁至關於新起了一個進程(進程內有本身的多線程)
固然,瀏覽器有時會將多個進程合併(譬如打開多個空白標籤頁後,會發現多個空白標籤頁被合併成了一個進程),如圖
另外,能夠經過Chrome的更多工具 -> 任務管理器
自行驗證
相比於單進程瀏覽器,多進程有以下優勢:
簡單點理解:若是瀏覽器是單進程,那麼某個Tab頁崩潰了,就影響了整個瀏覽器,體驗有多差;同理若是是單進程,插件崩潰了也會影響整個瀏覽器;並且多進程還有其它的諸多優點。。。
固然,內存等資源消耗也會更大,有點空間換時間的意思。
重點來了,咱們能夠看到,上面提到了這麼多的進程,那麼,對於普通的前端操做來講,最終要的是什麼呢?答案是渲染進程
能夠這樣理解,頁面的渲染,JS的執行,事件的循環,都在這個進程內進行。接下來重點分析這個進程
請牢記,瀏覽器的渲染進程是多線程的(這點若是不理解,請回頭看進程和線程的區分)
終於到了線程這個概念了😭,好親切。那麼接下來看看它都包含了哪些線程(列舉一些主要常駐線程):
GUI渲染線程
JS引擎線程
事件觸發線程
注意,因爲JS的單線程關係,因此這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時纔會去執行)
定時觸發器線程
setInterval
與setTimeout
所在線程異步http請求線程
看到這裏,若是以爲累了,能夠先休息下,這些概念須要被消化,畢竟後續將提到的事件循環機制就是基於事件觸發線程
的,因此若是僅僅是看某個碎片化知識,
可能會有一種似懂非懂的感受。要完成的梳理一遍才能快速沉澱,不易遺忘。放張圖鞏固下吧:
再說一點,爲何JS引擎是單線程的?額,這個問題其實應該沒有標準答案,譬如,可能僅僅是由於因爲多線程的複雜性,譬如多線程操做通常要加鎖,所以最初設計時選擇了單線程。。。
看到這裏,首先,應該對瀏覽器內的進程和線程都有必定理解了,那麼接下來,再談談瀏覽器的Browser進程(控制進程)是如何和內核通訊的,
這點也理解後,就能夠將這部分的知識串聯起來,從頭至尾有一個完整的概念。
若是本身打開任務管理器,而後打開一個瀏覽器,就能夠看到:任務管理器中出現了兩個進程(一個是主控進程,一個則是打開Tab頁的渲染進程),
而後在這前提下,看下整個的過程:(簡化了不少)
Renderer進程的Renderer接口收到消息,簡單解釋後,交給渲染線程,而後開始渲染
這裏繪一張簡單的圖:(很簡化)
看完這一整套流程,應該對瀏覽器的運做有了必定理解了,這樣有了知識架構的基礎後,後續就方便往上填充內容。
這塊再往深處講的話就涉及到瀏覽器內核源碼解析了,不屬於本文範圍。
若是這一塊要深挖,建議去讀一些瀏覽器內核源碼解析文章,或者能夠先看看參考下來源中的第一篇文章,寫的不錯
到了這裏,已經對瀏覽器的運行有了一個總體的概念,接下來,先簡單梳理一些概念
因爲JavaScript是可操縱DOM的,若是在修改這些元素屬性同時渲染界面(即JS線程和UI線程同時運行),那麼渲染線程先後得到的元素數據就可能不一致了。
所以爲了防止渲染出現不可預期的結果,瀏覽器設置GUI渲染線程與JS引擎爲互斥的關係,當JS引擎執行時GUI線程會被掛起,
GUI更新則會被保存在一個隊列中等到JS引擎線程空閒時當即被執行。
從上述的互斥關係,能夠推導出,JS若是執行時間過長就會阻塞頁面。
譬如,假設JS引擎正在進行巨量的計算,此時就算GUI有更新,也會被保存到隊列中,等待JS引擎空閒後執行。
而後,因爲巨量計算,因此JS引擎極可能好久好久後才能空閒,天然會感受到巨卡無比。
因此,要儘可能避免JS執行時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞的感受。
前文中有提到JS引擎是單線程的,並且JS執行時間過長會阻塞頁面,那麼JS就真的對cpu密集型計算無能爲力麼?
因此,後來HTML5中支持了Web Worker
。
MDN的官方解釋是:
Web Worker爲Web內容在後臺線程中運行腳本提供了一種簡單的方法。線程能夠執行任務而不干擾用戶界面
一個worker是使用一個構造函數建立的一個對象(e.g. Worker()) 運行一個命名的JavaScript文件
這個文件包含將在工做線程中運行的代碼; workers 運行在另外一個全局上下文中,不一樣於當前的window 所以,使用 window快捷方式獲取當前全局的範圍 (而不是self) 在一個 Worker 內將返回錯誤
這樣理解下:
因此,若是有很是耗時的工做,請單獨開一個Worker線程,這樣裏面無論如何翻天覆地都不會影響JS引擎主線程,
只待計算出結果後,將結果通訊給主線程便可,perfect!
並且注意下,JS引擎是單線程的,這一點的本質仍然未改變,Worker能夠理解是瀏覽器給JS引擎開的外掛,專門用來解決那些大量計算問題。
其它,關於Worker的詳解就不是本文的範疇了,所以再也不贅述。
既然都到了這裏,就再提一下SharedWorker
(避免後續將這兩個概念搞混)
WebWorker只屬於某個頁面,不會和其餘頁面的Render進程(瀏覽器內核進程)共享
SharedWorker是瀏覽器全部頁面共享的,不能採用與Worker一樣的方式實現,由於它不隸屬於某個Render進程,能夠爲多個Render進程共享使用
看到這裏,應該就很容易明白了,本質上就是進程和線程的區別。SharedWorker由獨立的進程管理,WebWorker只是屬於render進程下的一個線程
原本是直接計劃開始談JS運行機制的,但想了想,既然上述都一直在談瀏覽器,直接跳到JS可能再突兀,所以,中間再補充下瀏覽器的渲染流程(簡單版本)
爲了簡化理解,前期工做直接省略成:(要展開的或徹底能夠寫另外一篇超長文)
- 瀏覽器輸入url,瀏覽器主進程接管,開一個下載線程, 而後進行 http請求(略去DNS查詢,IP尋址等等操做),而後等待響應,獲取內容, 隨後將內容經過RendererHost接口轉交給Renderer進程 - 瀏覽器渲染流程開始
瀏覽器器內核拿到內容後,渲染大概能夠劃分紅如下幾個步驟:
全部詳細步驟都已經略去,渲染完畢後就是load
事件了,以後就是本身的JS邏輯處理了
既然略去了一些詳細的步驟,那麼就提一些可能須要注意的細節把。
這裏重繪參考來源中的一張圖:(參考來源第一篇)
上面提到,渲染完畢後會觸發load
事件,那麼你能分清楚load
事件與DOMContentLoaded
事件的前後麼?
很簡單,知道它們的定義就能夠了:
(譬如若是有async加載的腳本就不必定完成)
(渲染完畢了)
因此,順序是:DOMContentLoaded -> load
這裏說的是頭部引入css的狀況
首先,咱們都知道:css是由單獨的下載線程異步下載的。
而後再說下幾個現象:
這可能也是瀏覽器的一種優化機制。
由於你加載css的時候,可能會修改下面DOM節點的樣式,
若是css加載不阻塞render樹渲染的話,那麼當css加載完以後,
render樹可能又得從新重繪或者回流了,這就形成了一些沒有必要的損耗。
因此乾脆就先把DOM樹的結構先解析完,把能夠作的工做作完,而後等你css加載完以後,
在根據最終的樣式來渲染render樹,這種作法性能方面確實會比較好一點。
渲染步驟中就提到了composite
概念。
能夠簡單的這樣理解,瀏覽器渲染的圖層通常包含兩大類:普通圖層
以及複合圖層
首先,普通文檔流內能夠理解爲一個複合圖層(這裏稱爲默認複合層
,裏面無論添加多少元素,其實都是在同一個複合圖層中)
其次,absolute佈局(fixed也同樣),雖然能夠脫離普通文檔流,但它仍然屬於默認複合層
。
而後,能夠經過硬件加速
的方式,聲明一個新的複合圖層
,它會單獨分配資源
(固然也會脫離普通文檔流,這樣一來,無論這個複合圖層中怎麼變化,也不會影響默認複合層
裏的迴流重繪)
能夠簡單理解下:GPU中,各個複合圖層是單獨繪製的,因此互不影響,這也是爲何某些場景硬件加速效果一級棒
能夠Chrome源碼調試 -> More Tools -> Rendering -> Layer borders
中看到,黃色的就是複合圖層信息
以下圖。能夠驗證上述的說法
如何變成複合圖層(硬件加速)
將該元素變成一個複合圖層,就是傳說中的硬件加速技術
translate3d
、translateZ
opacity
屬性/過渡動畫(須要動畫執行的過程當中纔會建立合成層,動畫沒有開始或結束後元素還會回到以前的狀態)will-chang
屬性(這個比較偏僻),通常配合opacity與translate使用(並且經測試,除了上述能夠引起硬件加速的屬性外,其它屬性並不會變成複合層),做用是提早告訴瀏覽器要變化,這樣瀏覽器會開始作一些優化工做(這個最好用完後就釋放)
<video><iframe><canvas><webgl>
等元素absolute和硬件加速的區別
能夠看到,absolute雖然能夠脫離普通文檔流,可是沒法脫離默認複合層。
因此,就算absolute中信息改變時不會改變普通文檔流中render樹,
可是,瀏覽器最終繪製時,是整個複合層繪製的,因此absolute中信息的改變,仍然會影響整個複合層的繪製。
(瀏覽器會重繪它,若是複合層中內容多,absolute帶來的繪製信息變化過大,資源消耗是很是嚴重的)
而硬件加速直接就是在另外一個複合層了(另起爐竈),因此它的信息改變不會影響默認複合層
(固然了,內部確定會影響屬於本身的複合層),僅僅是引起最後的合成(輸出視圖)
複合圖層的做用?
通常一個元素開啓硬件加速後會變成複合圖層,能夠獨立於普通文檔流中,改動後能夠避免整個頁面重繪,提高性能
可是儘可能不要大量使用複合圖層,不然因爲資源消耗過分,頁面反而會變的更卡
硬件加速時請使用index
使用硬件加速時,儘量的使用index,防止瀏覽器默認給後續的元素建立複合層渲染
具體的原理時這樣的:
**webkit CSS3中,若是這個元素添加了硬件加速,而且index層級比較低,
那麼在這個元素的後面其它元素(層級比這個元素高的,或者相同的,而且releative或absolute屬性相同的),
會默認變爲複合層渲染,若是處理不當會極大的影響性能**
簡單點理解,其實能夠認爲是一個隱式合成的概念:若是a是一個複合圖層,並且b在a上面,那麼b也會被隱式轉爲一個複合圖層,這點須要特別注意
另外,這個問題能夠在這個地址看到重現(原做者分析的挺到位的,直接上連接):
到此時,已是屬於瀏覽器頁面初次渲染完畢後的事情,JS引擎的一些運行機制分析。
注意,這裏不談可執行上下文
,VO
,scop chain
等概念(這些徹底能夠整理成另外一篇文章了),這裏主要是結合Event Loop
來談JS代碼是如何執行的。
讀這部分的前提是已經知道了JS引擎是單線程,並且這裏會用到上文中的幾個概念:(若是不是很理解,能夠回頭溫習)
而後再理解一個概念:
執行棧
任務隊列
,只要異步任務有了運行結果,就在任務隊列
之中放置一個事件。執行棧
中的全部同步任務執行完畢(此時JS引擎空閒),系統就會讀取任務隊列
,將可運行的異步任務添加到可執行棧中,開始執行。看圖:
看到這裏,應該就能夠理解了:爲何有時候setTimeout推入的事件不能準時執行?由於可能在它推入到事件列表時,主線程還不空閒,正在執行其它代碼,
因此天然有偏差。
這裏就直接引用一張圖片來協助理解:(參考自Philip Roberts的演講《Help, I'm stuck in an event-loop》)
上圖大體描述就是:
棧中的代碼調用某些api時,它們會在事件隊列中添加各類事件(當知足觸發條件後,如ajax請求完畢)
上述事件循環機制的核心是:JS引擎線程和事件觸發線程
但事件上,裏面還有一些隱藏細節,譬如調用setTimeout
後,是如何等待特定時間後才添加到事件隊列中的?
是JS引擎檢測的麼?固然不是了。它是由定時器線程控制(由於JS引擎本身都忙不過來,根本無暇分身)
爲何要單獨的定時器線程?由於JavaScript引擎是單線程的, 若是處於阻塞線程狀態就會影響記計時的準確,所以頗有必要單獨開一個線程用來計時。
何時會用到定時器線程?當使用setTimeout
或setInterval
時,它須要定時器線程計時,計時完成後就會將特定的事件推入事件隊列中。
譬如:
setTimeout(function(){ console.log('hello!'); }, 1000);
這段代碼的做用是當1000
毫秒計時完畢後(由定時器線程計時),將回調函數推入事件隊列中,等待主線程執行
setTimeout(function(){ console.log('hello!'); }, 0); console.log('begin');
這段代碼的效果是最快的時間內將回調函數推入事件隊列中,等待主線程執行
注意:
begin
後hello!
(不過也有一說是不一樣瀏覽器有不一樣的最小時間設定)
begin
(由於只有可執行棧內空了後纔會主動讀取事件隊列)用setTimeout模擬按期計時和直接用setInterval是有區別的。
由於每次setTimeout計時到後就會去執行,而後執行一段時間後纔會繼續setTimeout,中間就多了偏差
(偏差多少與代碼執行時間有關)
而setInterval則是每次都精確的隔一段時間推入一個事件
(可是,事件的實際執行時間不必定就準確,還有多是這個事件還沒執行完畢,下一個事件就來了)
並且setInterval有一些比較致命的問題就是:
就會致使定時器代碼連續運行好幾回,而之間沒有間隔。
就算正常間隔執行,多個setInterval的代碼執行時間可能會比預期小(由於代碼執行須要必定時間)
它會把setInterval的回調函數放在隊列中,等瀏覽器窗口再次打開時,一瞬間所有執行時
因此,鑑於這麼多但問題,目前通常認爲的最佳方案是:用setTimeout模擬setInterval,或者特殊場合直接用requestAnimationFrame
補充:JS高程中有提到,JS引擎會對setInterval進行優化,若是當前事件隊列中有setInterval的回調,不會重複添加。不過,仍然是有不少問題。。。
這段參考了參考來源中的第2篇文章(英文版的),(加了下本身的理解從新描述了下),
強烈推薦有英文基礎的同窗直接觀看原文,做者描述的很清晰,示例也很不錯,以下:
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
上文中將JS事件循環機制梳理了一遍,在ES5的狀況是夠用了,可是在ES6盛行的如今,仍然會遇到一些問題,譬以下面這題:
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end');
嗯哼,它的正確執行順序是這樣子的:
script start script end promise1 promise2 setTimeout
爲何呢?由於Promise裏有了一個一個新的概念:microtask
或者,進一步,JS中分爲兩種任務類型:macrotask
和microtask
,在ECMAScript中,microtask稱爲jobs
,macrotask可稱爲task
它們的定義?區別?簡單點能夠按以下理解:
macrotask(又稱之爲宏任務),能夠理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)
(`task->渲染->task->...`)
microtask(又稱爲微任務),能夠理解是在當前 task 執行結束後當即執行的任務
分別很麼樣的場景會造成macrotask和microtask呢?
__補充:在node環境下,process.nextTick的優先級高於Promise__,也就是能夠簡單理解爲:在宏任務結束後會先執行微任務隊列中的nextTickQueue部分,而後纔會執行微任務中的Promise部分。
參考:https://segmentfault.com/q/1010000011914016
再根據線程來理解下:
(這點由本身理解+推測得出,由於它是在主線程下無縫執行的)
因此,總結下運行機制:
如圖:
另外,請注意下Promise
的polyfill
與官方版本的區別:
注意,有一些瀏覽器執行結果不同(由於它們可能把microtask當成macrotask來執行了),
可是爲了簡單,這裏不描述一些不標準的瀏覽器下的場景(但記住,有些瀏覽器可能並不標準)
20180126補充:使用MutationObserver實現microtask
MutationObserver能夠用來實現microtask
(它屬於microtask,優先級小於Promise,
通常是Promise不支持時纔會這樣作)
它是HTML5中的新特性,做用是:監聽一個DOM變更,
當DOM對象樹發生任何變更時,Mutation Observer會獲得通知
像之前的Vue源碼中就是利用它來模擬nextTick的,
具體原理是,建立一個TextNode並監聽內容變化,
而後要nextTick的時候去改一下這個節點的文本內容,
以下:(Vue的源碼,未修改)
var counter = 1 var observer = new MutationObserver(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實現移除了MutationObserver的方式(聽說是兼容性緣由),
取而代之的是使用MessageChannel
(固然,默認狀況仍然是Promise,不支持才兼容的)。
MessageChannel屬於宏任務,優先級是:MessageChannel->setTimeout
,
因此Vue(2.5+)內部的nextTick與2.4及以前的實現是不同的,須要注意下。
這裏不展開,能夠看下http://www.javashuo.com/article/p-uvlubpli-gp.html
看到這裏,不知道對JS的運行機制是否是更加理解了,從頭至尾梳理,而不是就某一個碎片化知識應該是會更清晰的吧?
同時,也應該注意到了JS根本就沒有想象的那麼簡單,前端的知識也是無窮無盡,層出不窮的概念、N多易忘的知識點、各式各樣的框架、
底層原理方面也是能夠無限的往下深挖,而後你就會發現,你知道的太少了。。。
另外,本文也打算先告一段落,其它的,如JS詞法解析,可執行上下文以及VO等概念就不繼續在本文中寫了,後續能夠考慮另開新的文章。
最後,喜歡的話,就請給個贊吧!