前言
在稀土掘金上看到這一次雳攘,徹底弄懂 JavaScript 執(zhí)行機制和從瀏覽器多進程到JS單線程带兜,JS運行機制最全面的一次梳理兩篇關于JS執(zhí)行機制的文章,受益匪淺吨灭,記得剛面試我現(xiàn)在這家公司的時候刚照,面試官就問了我一些執(zhí)行順序的問題,之前沒有系統(tǒng)的總結過喧兄,只用了些事例來回答无畔,最近覺得要系統(tǒng)的梳理之前的積累的一些知識了,先從JS執(zhí)行機制開始吧
javascript特點
js的特點在于它是單線程的吠冤,即同一時間只能做同一件事情浑彰,為什么js不能像java一樣是多線程呢?
主要原因在于應用場景不一樣拯辙,作為瀏覽器腳本語言郭变,更多的是處理DOM的操作和與用戶的交互。假如js創(chuàng)建多線程涯保,一個線程添加一個DOM節(jié)點诉濒,另一個線程刪除當前這個DOM節(jié)點,瀏覽器就無法知道是以那個線程的操作為主了夕春。為了使同步問題變得不那么復雜未荒,在設計之初,js就是按照單線程來設計的及志。h5的API中提出web worker標準茄猫,允許創(chuàng)建多線程狈蚤,但是子線程是受主線程控制的,而且不能操作DOM。
進程和線程
工廠的資源 -> 系統(tǒng)分配的內存(獨立的一塊內存)
工廠之間的相互獨立 -> 進程之間相互獨立
多個工人協(xié)作完成任務 -> 多個線程在進程中協(xié)作完成任務
工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成
工人之間共享空間 -> 同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數(shù)據集躬窜、堆等)
瀏覽器是多進程的
- 瀏覽器之所以能夠運行啡专,是因為系統(tǒng)給它的進程分配了資源(cpu、內存)
- 每打開一個Tab頁愉阎,就相當于創(chuàng)建了一個獨立的瀏覽器進程
瀏覽器內核(渲染進程)
如頁面的渲染、js的執(zhí)行、事件的循環(huán)幻捏,瀏覽器的渲染進程是多線程的
主要包括:
- GUI渲染線程
- 負責渲染瀏覽器界面,解析HTML命咐,CSS篡九,構建DOM樹和Render Object樹,布局和繪制等
- 界面需要重繪(Repaint)或由于某種操作引發(fā)回流(Reflow)時
- GUI渲染線程和JS引擎線程時互斥的
- JS引擎線程
- JS內核醋奠,負責處理JS腳本程序(V8引擎)
- JS引擎線程解析js腳本榛臼,運行代碼。
- JS引擎一直等待著任務隊列中任務的到來窜司,再加以處理沛善,一個Tab頁面(渲染進程)無論什么時候只有一個JS線程再運行JS程序。
- 如果JS執(zhí)行時間過長塞祈,就會造成頁面的渲染不連貫金刁,導致頁面渲染加載阻塞。
- 事件觸發(fā)線程
- 用于控制事件循環(huán)(JS引擎忙不過來议薪,需要瀏覽器另開線程協(xié)助)
- 當JS引擎執(zhí)行代碼塊如setTimeOut(也可以來自瀏覽器內核的其他線程尤蛮,如鼠標點擊,ajax異步請求等)會將對應的任務添加到事件線程中
- 當對應的事件符合觸發(fā)條件被觸發(fā)時斯议,該線程會把事件添加到待處理隊列的隊尾产捞,等待JS引擎的處理
- JS的單線程關系,這些等待處理隊列中的事件都得排隊等待JS引擎處理完才會執(zhí)行
- 定時觸發(fā)器線程
- setInterval與setTimeout所在線程
- 瀏覽器定時計數(shù)器并不是由JS引擎計數(shù)的(js引擎是單線程的捅位,如果處于阻塞線程狀態(tài)就會影響計時的準確性)
- 通過單獨線程來計時并觸發(fā)定時(計時完畢后轧葛,添加到事件隊列中,等待JS引擎空閑后再執(zhí)行)
- 注意艇搀,W3C在HTML標準中規(guī)定尿扯,規(guī)定要求setTimeout中低于4ms的時間間隔算為4ms
- 異步HTTP請求線程
- 在XMLHTTPRequest在連接后通過瀏覽器新開一個線程請求
- 將檢測到狀態(tài)變更時,如果設置有回調函數(shù)焰雕,異步線程就產生狀態(tài)變更事件衷笋,將這個回調放入事件隊列中,再由js引擎執(zhí)行矩屁。
事件循環(huán)機制基于事件觸發(fā)線程的
瀏覽器渲染流程
瀏覽器輸入URL辟宗,瀏覽器主進程接管爵赵,開啟一個下載線程
DNS查詢
IP尋址
HTTP請求
等待響應,獲取內容
將內容通過RendererHost接口轉交給Renderer進程
瀏覽器渲染流程開始
瀏覽器內核拿到內容后泊脐,渲染大概分成以下步驟:
- 解析html建立dom樹
2.解析css構建render樹(將CSS代碼解析成樹形的數(shù)據結構空幻,然后結合DOM合成render樹)
3.布局render樹(layout/reflow),負責各元素尺寸,位置的計算
4.繪制render樹容客,繪制頁面像素信息
5.瀏覽器會將各層的信息發(fā)送給GPU秕铛,GPU會將各層合成,顯示在屏幕上
Event Loop談JS的運行機制
- JS分成同步任務和異步任務
- 同步任務都在主線程上執(zhí)行缩挑,形成一個執(zhí)行棧
- 主線程外但两,事件觸發(fā)線程管理一個任務隊列,只要有異步任務有了運行結果供置,就在任務隊列中放置一個事件
- 一旦執(zhí)行棧中所有的同步任務執(zhí)行完成(JS引擎空閑)谨湘,系統(tǒng)會讀取任務隊列,將可運行的異步任務添加到可執(zhí)行棧中芥丧,開始執(zhí)行
應該就可以理解了:為什么有時候setTimeout推入的事件不能準時執(zhí)行紧阔?因為可能在它推入到事件列表時,主線程還不空閑娄柳,正在執(zhí)行其它代碼寓辱, 所以自然有誤差
時間循環(huán):macrotask與microtask
- macrotask(宏任務):每次執(zhí)行棧執(zhí)行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調并放回到執(zhí)行棧中執(zhí)行)
- 每個task會從頭到尾將這個任務執(zhí)行完畢艘绍,不會執(zhí)行其他
- 瀏覽器為了能使得JS內部task與DOM任務能夠有序的執(zhí)行赤拒,會在一個task執(zhí)行結束后,在下一個task執(zhí)行前诱鞠,對頁面進行渲染(task->渲染->task->...)
microtask(微任務)挎挖,當前task執(zhí)行結束后立即執(zhí)行的任務 - 在當前task任務后,下一個task之前航夺,在渲染之前
- 響應速度比setTimeout快蕉朵,因為無需等待渲染
- 在某個macrotask執(zhí)行完后,就會將在他執(zhí)行期間產生的所有microtask都執(zhí)行完畢(在渲染前)
macrotask:主代碼塊阳掐,setTimeout始衅、setInterval、MessageChannel缭保、postMessage汛闸、setImmediate等(事件隊列中的每一個事件都是一個macrotask)
microtask: MutationObsever 、Promise.then艺骂、process.nextTick等
根據線程理解: - macrotask中的事件都是放在一個事件隊列中的诸老,而這個隊列由事件觸發(fā)線程維護
- microtask中的微任務都是添加到微任務隊列中,等待當前macrotask執(zhí)行完畢后執(zhí)行钳恕,這個隊列由JS引擎線程維護(在主線程下無縫執(zhí)行的)
運行機制: - 執(zhí)行一個宏任務(棧中沒有就從事件隊列中獲缺鸱)
- 執(zhí)行過程中如果遇到微任務蹄衷,就 添加到微任務的任務隊列中
- 宏任務執(zhí)行完畢后,立即執(zhí)行當前微任務隊列中的所有微任務
- 當前宏任務執(zhí)行完畢后厘肮,開始檢查渲染愧口,然后GUI線程接管渲染
- 渲染完畢后,JS線程繼續(xù)接管类茂,開始下一個宏任務(從事件隊列中獲鹊鞅啊)