這篇文章主要介紹“JavaScript運(yùn)行機(jī)制及原理是什么”,在日常操作中,相信很多人在JavaScript運(yùn)行機(jī)制及原理是什么問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”JavaScript運(yùn)行機(jī)制及原理是什么”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!
創(chuàng)新互聯(lián)建站“設(shè)計(jì)定江山,服務(wù)贏天下“的思想,用細(xì)節(jié)和態(tài)度獲得客戶的認(rèn)可與同行的尊重,服務(wù)是創(chuàng)新互聯(lián)建站企業(yè)文化中重要的核心思想,每一位員工要致力成為客戶心中堅(jiān)實(shí)的服務(wù)后盾。

簡(jiǎn)單地說(shuō),JavaScript解析引擎就是能夠“讀懂”JavaScript代碼,并準(zhǔn)確地給出代碼運(yùn)行結(jié)果的一段程序。
比方說(shuō),當(dāng)你寫(xiě)了 var a = 1 + 1; 這樣一段代碼,JavaScript引擎做的事情就是看懂(解析)你這段代碼,并且將a的值變?yōu)?。
學(xué)過(guò)編譯原理的人知道,對(duì)于靜態(tài)語(yǔ)言來(lái)說(shuō)(如Java、C++、C),處理上述這些事情的叫編譯器(Compiler),相應(yīng)地對(duì)于JavaScript這樣的動(dòng)態(tài)語(yǔ)言則叫解釋器(Interpreter)。這兩者的區(qū)別用一句話來(lái)概括就是:編譯器是將源代碼編譯為另外一種代碼(比如機(jī)器碼,或者字節(jié)碼),而解釋器是直接解析并將代碼運(yùn)行結(jié)果輸出。 比方說(shuō),firebug的console就是一個(gè)JavaScript的解釋器。
但是,現(xiàn)在很難去界定說(shuō),JavaScript引擎它到底算是個(gè)解釋器還是個(gè)編譯器,因?yàn)椋热缦馰8(Chrome的JS引擎),它其實(shí)為了提高JS的運(yùn)行性能,在運(yùn)行之前會(huì)先將JS編譯為本地的機(jī)器碼(native machine code),然后再去執(zhí)行機(jī)器碼(這樣速度就快很多)。
JavaScript引擎是一段程序,我們寫(xiě)的JavaScript代碼也是程序,如何讓程序去讀懂程序呢?這就需要定義規(guī)則。比如,之前提到的var a = 1 + 1;,它表示:
左邊var代表了這是申明(declaration),它聲明了a這個(gè)變量
右邊的+表示要將1和1做加法
中間的等號(hào)表示了這是個(gè)賦值語(yǔ)句
最后的分號(hào)表示這句語(yǔ)句結(jié)束了
上述這些就是規(guī)則,有了它就等于有了衡量的標(biāo)準(zhǔn),JavaScript引擎就可以根據(jù)這個(gè)標(biāo)準(zhǔn)去解析JavaScript代碼了。那么這里的ECMAScript就是定義了這些規(guī)則。其中ECMAScript 262這份文檔,就是對(duì)JavaScript這門(mén)語(yǔ)言定義了一整套完整的標(biāo)準(zhǔn)。其中包括:
var,if,else,break,continue等是JavaScript的關(guān)鍵詞
abstract,int,long等是JavaScript保留詞
怎么樣算是數(shù)字、怎么樣算是字符串等等
定義了操作符(+,-,>,<等)
定義了JavaScript的語(yǔ)法
定義了對(duì)表達(dá)式,語(yǔ)句等標(biāo)準(zhǔn)的處理算法,比如遇到==該如何處理
??
標(biāo)準(zhǔn)的JavaScript引擎就會(huì)根據(jù)這套文檔去實(shí)現(xiàn),注意這里強(qiáng)調(diào)了標(biāo)準(zhǔn),因?yàn)橐灿胁话凑諛?biāo)準(zhǔn)來(lái)實(shí)現(xiàn)的,比如IE的JS引擎。這也是為什么JavaScript會(huì)有兼容性的問(wèn)題。至于為什么IE的JS引擎不按照標(biāo)準(zhǔn)來(lái)實(shí)現(xiàn),就要說(shuō)到瀏覽器大戰(zhàn)了,這里就不贅述了,自行Google之。
所以,簡(jiǎn)單的說(shuō),ECMAScript定義了語(yǔ)言的標(biāo)準(zhǔn),JavaScript引擎根據(jù)它來(lái)實(shí)現(xiàn),這就是兩者的關(guān)系。
簡(jiǎn)單地說(shuō),JavaScript引擎是瀏覽器的組成部分之一。因?yàn)闉g覽器還要做很多別的事情,比如解析頁(yè)面、渲染頁(yè)面、Cookie管理、歷史記錄等等。那么,既然是組成部分,因此一般情況下JavaScript引擎都是瀏覽器開(kāi)發(fā)商自行開(kāi)發(fā)的。比如:IE9的Chakra、Firefox的TraceMonkey、Chrome的V8等等。
從而也看出,不同瀏覽器都采用了不同的JavaScript引擎。因此,我們只能說(shuō)要深入了解哪個(gè)JavaScript引擎。
JavaScript語(yǔ)言的一大特點(diǎn)就是單線程,也就是說(shuō),同一個(gè)時(shí)間只能做一件事。那么,為什么JavaScript不能有多個(gè)線程呢?這樣能提高效率啊。
JavaScript的單線程,與它的用途有關(guān)。作為瀏覽器腳本語(yǔ)言,JavaScript的主要用途是與用戶互動(dòng),以及操作DOM。這決定了它只能是單線程,否則會(huì)帶來(lái)很復(fù)雜的同步問(wèn)題。比如,假定JavaScript同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?
所以,為了避免復(fù)雜性,從一誕生,JavaScript就是單線程,這已經(jīng)成了這門(mén)語(yǔ)言的核心特征,將來(lái)也不會(huì)改變。
為了利用多核CPU的計(jì)算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個(gè)新標(biāo)準(zhǔn)并沒(méi)有改變JavaScript單線程的本質(zhì)。
進(jìn)程是cpu資源分配的最小單位,進(jìn)程可以包含多個(gè)線程。 瀏覽器就是多進(jìn)程的,每打開(kāi)的一個(gè)瀏覽器窗口就是一個(gè)進(jìn)程。
線程是cpu調(diào)度的最小單位,同一進(jìn)程下的各個(gè)線程之間共享程序的內(nèi)存空間。
可以把進(jìn)程看做一個(gè)倉(cāng)庫(kù),線程是可以運(yùn)輸?shù)呢涇?chē),每個(gè)倉(cāng)庫(kù)有屬于自己的多輛貨車(chē)為倉(cāng)庫(kù)服務(wù)(運(yùn)貨),每個(gè)倉(cāng)庫(kù)可以同時(shí)由多輛車(chē)同時(shí)拉貨,但是每輛車(chē)同一時(shí)間只能干一件事,就是運(yùn)輸本次的貨物。
核心點(diǎn):
進(jìn)程是 cpu 資源分配的最小單位(是能擁有資源和獨(dú)立運(yùn)行的最小單位)
線程是 cpu 調(diào)度的最小單位(線程是建立在進(jìn)程的基礎(chǔ)上的一次程序運(yùn)行單位,一個(gè)進(jìn)程中可以有多個(gè)線程)
不同進(jìn)程之間也可以通信,不過(guò)代價(jià)較大。
瀏覽器是多進(jìn)程的
理解了進(jìn)程與線程了區(qū)別后,接下來(lái)對(duì)瀏覽器進(jìn)行一定程度上的認(rèn)識(shí):(先看下簡(jiǎn)化理解)
瀏覽器之所以能夠運(yùn)行,是因?yàn)橄到y(tǒng)給它的進(jìn)程分配了資源(cpu、內(nèi)存)
簡(jiǎn)單點(diǎn)理解,每打開(kāi)一個(gè)Tab頁(yè),就相當(dāng)于創(chuàng)建了一個(gè)獨(dú)立的瀏覽器進(jìn)程。
以 Chrome 為例,它的多個(gè)標(biāo)簽頁(yè),然后可以在Chrome的任務(wù)管理器中看到有多個(gè)進(jìn)程(分別是每一個(gè) Tab 頁(yè)面有一個(gè)獨(dú)立的進(jìn)程,以及一個(gè)主進(jìn)程),在 Windows 的任務(wù)管理器中也可以看出。
注意:在這里瀏覽器應(yīng)該也有自己的優(yōu)化機(jī)制,有時(shí)候打開(kāi)多個(gè)tab頁(yè)后,可以在Chrome任務(wù)管理器中看到,有些進(jìn)程被合并了 (所以每一個(gè)Tab標(biāo)簽對(duì)應(yīng)一個(gè)進(jìn)程并不一定是絕對(duì)的)
知道了瀏覽器是多進(jìn)程后,再來(lái)看看它到底包含哪些進(jìn)程:(為了簡(jiǎn)化理解,僅列舉主要進(jìn)程)
(1)Browser 進(jìn)程:瀏覽器的主進(jìn)程(負(fù)責(zé)協(xié)調(diào)、主控),只有一個(gè)。作用:
負(fù)責(zé)瀏覽器界面顯示,與用戶交互。如前進(jìn),后退等
負(fù)責(zé)各個(gè)頁(yè)面的管理,創(chuàng)建和銷(xiāo)毀其他進(jìn)程
將 Renderer 進(jìn)程得到的內(nèi)存中的 Bitmap,繪制到用戶界面上
網(wǎng)絡(luò)資源的管理,下載等
(2)第三方插件進(jìn)程:每種類(lèi)型的插件對(duì)應(yīng)一個(gè)進(jìn)程,僅當(dāng)使用該插件時(shí)才創(chuàng)建
(3)GPU 進(jìn)程:最多一個(gè),用于 3D 繪制等
(4)瀏覽器渲染進(jìn)程(瀏覽器內(nèi)核,Renderer進(jìn)程,內(nèi)部是多線程的):默認(rèn)每個(gè) Tab 頁(yè)面一個(gè)進(jìn)程,互不影響。主要作用為:頁(yè)面渲染,腳本執(zhí)行,事件處理等
強(qiáng)化記憶:在瀏覽器中打開(kāi)一個(gè)網(wǎng)頁(yè)相當(dāng)于新起了一個(gè)進(jìn)程(進(jìn)程內(nèi)有自己的多線程)
當(dāng)然,瀏覽器有時(shí)會(huì)將多個(gè)進(jìn)程合并(譬如打開(kāi)多個(gè)空白標(biāo)簽頁(yè)后,會(huì)發(fā)現(xiàn)多個(gè)空白標(biāo)簽頁(yè)被合并成了一個(gè)進(jìn)程)
相比于單進(jìn)程瀏覽器,多進(jìn)程有如下優(yōu)點(diǎn):
避免單個(gè) page crash 影響整個(gè)瀏覽器
避免第三方插件 crash 影響整個(gè)瀏覽器
多進(jìn)程充分利用多核優(yōu)勢(shì)
方便使用沙盒模型隔離插件等進(jìn)程,提高瀏覽器穩(wěn)定性
簡(jiǎn)單理解:
如果瀏覽器是單進(jìn)程,那么某個(gè) Tab 頁(yè)崩潰了,就影響了整個(gè)瀏覽器,體驗(yàn)有多差;同理如果是單進(jìn)程,插件崩潰了也會(huì)影響整個(gè)瀏覽器。
當(dāng)然,內(nèi)存等資源消耗也會(huì)更大,有點(diǎn)空間換時(shí)間的意思。再大的內(nèi)存也不夠 Chrome 吃的,內(nèi)存泄漏問(wèn)題現(xiàn)在已經(jīng)改善了一些了,也僅僅是改善,還有就是會(huì)導(dǎo)致耗電增加。
重點(diǎn)來(lái)了,我們可以看到,上面提到了這么多的進(jìn)程,那么,對(duì)于普通的前端操作來(lái)說(shuō),最終要的是什么呢?答案是渲染進(jìn)程。
可以這樣理解,頁(yè)面的渲染,JS 的執(zhí)行,事件的循環(huán),都在這個(gè)進(jìn)程內(nèi)進(jìn)行。接下來(lái)重點(diǎn)分析這個(gè)進(jìn)程
請(qǐng)牢記,瀏覽器的渲染進(jìn)程是多線程的(JS 引擎是單線程的)
那么接下來(lái)看看它都包含了哪些線程(列舉一些主要常駐線程):
負(fù)責(zé)渲染瀏覽器界面,解析 HTML,CSS,構(gòu)建 DOM 樹(shù)和 RenderObject 樹(shù)(簡(jiǎn)單理解為 CSS 形成的樣式樹(shù),F(xiàn)lutter 核心之一),布局和繪制等。
當(dāng)界面需要重繪(Repaint)或由于某種操作引發(fā)回流(reflow)時(shí),該線程就會(huì)執(zhí)行
注意,GUI 渲染線程與 JS 引擎線程是互斥的,當(dāng) JS 引擎執(zhí)行時(shí) GUI 線程會(huì)被掛起(凍結(jié)),GUI 更新會(huì)被保存在一個(gè)隊(duì)列中 等到 JS 引擎空閑時(shí) 立即被執(zhí)行。
也稱(chēng)為 JS 內(nèi)核,負(fù)責(zé)處理 Javascript 腳本程序。(例如 V8 引擎)
JS 引擎線程負(fù)責(zé)解析 Javascript 腳本,運(yùn)行代碼。
JS 引擎一直等待著任務(wù)隊(duì)列中任務(wù)的到來(lái),然后加以處理,一個(gè) Tab 頁(yè)(renderer 進(jìn)程)中無(wú)論什么時(shí)候都只有一個(gè) JS 線程在運(yùn)行 JS 程序
同樣注意,GUI 渲染線程與 JS 引擎線程是互斥的,所以如果 JS 執(zhí)行的時(shí)間過(guò)長(zhǎng),這樣就會(huì)造成頁(yè)面的渲染不連貫,導(dǎo)致頁(yè)面渲染加載阻塞。
歸屬于瀏覽器而不是 JS 引擎,用來(lái)控制事件循環(huán)(可以理解,JS 引擎自己都忙不過(guò)來(lái),需要瀏覽器另開(kāi)線程協(xié)助)
當(dāng) JS 引擎要執(zhí)行代碼塊如 setTimeOut 時(shí)(也可來(lái)自瀏覽器內(nèi)核的其他線程,如鼠標(biāo)點(diǎn)擊、Ajax 異步請(qǐng)求等),會(huì)將對(duì)應(yīng)任務(wù)添加到事件線程中。并且會(huì)負(fù)責(zé)排序
當(dāng)對(duì)應(yīng)的事件符合觸發(fā)條件被觸發(fā)時(shí),該線程會(huì)把事件添加到待處理隊(duì)列的隊(duì)尾,等待 JS 引擎的處理
注意,由于 JS 的單線程關(guān)系,所以這些待處理隊(duì)列中的事件都得排隊(duì)等待 JS 引擎處理(當(dāng) JS 引擎空閑時(shí)才會(huì)去執(zhí)行)
這里可以簡(jiǎn)單理解為,它負(fù)責(zé)管理一堆事件和一個(gè)“事件隊(duì)列”,只有在事件隊(duì)列的任務(wù) JS 引擎才會(huì)在空閑的時(shí)候去執(zhí)行,而它要做的,就是負(fù)責(zé)當(dāng)某個(gè)事件被觸發(fā)時(shí),把它加入到事件隊(duì)列。例如鼠標(biāo)單擊。
傳說(shuō)中的 setInterval 與 setTimeout 所在的線程
瀏覽器定時(shí)計(jì)數(shù)器并不是由 JavaScript 引擎計(jì)數(shù)的,(因?yàn)?JavaScript 引擎是單線程的,如果處于阻塞線程狀態(tài)就會(huì)影響記計(jì)時(shí)的準(zhǔn)確)
因此通過(guò)單獨(dú)線程來(lái)計(jì)時(shí)并觸發(fā)定時(shí),計(jì)時(shí)完畢后,添加到事件隊(duì)列中(對(duì)應(yīng) 事件觸發(fā)線程 的“事件符合觸發(fā)條件被觸發(fā)時(shí)”),等待 JS 引擎空閑后執(zhí)行。
注意,W3C 在 HTML 標(biāo)準(zhǔn)中規(guī)定,規(guī)定要求 setTimeout 中低于 4ms 的時(shí)間間隔算為 4ms。
在 XMLHttpRequest 在連接后是通過(guò)瀏覽器新開(kāi)一個(gè)線程請(qǐng)求
將檢測(cè)到狀態(tài)變更時(shí),如果設(shè)置有回調(diào)函數(shù),異步線程就產(chǎn)生狀態(tài)變更事件,將這個(gè)回調(diào)再放入事件隊(duì)列中。再由 JavaScript 引擎執(zhí)行。
看到這里,首先,應(yīng)該對(duì)瀏覽器內(nèi)的進(jìn)程和線程都有一定理解了,那么接下來(lái),再談?wù)劄g覽器的 Browser 進(jìn)程(控制進(jìn)程)是如何和內(nèi)核通信的, 這點(diǎn)也理解后,就可以將這部分的知識(shí)串聯(lián)起來(lái),從頭到尾有一個(gè)完整的概念。
如果自己打開(kāi)任務(wù)管理器,然后打開(kāi)一個(gè)瀏覽器,就可以看到:任務(wù)管理器中出現(xiàn)了兩個(gè)進(jìn)程(一個(gè)是主控進(jìn)程,一個(gè)則是打開(kāi) Tab 頁(yè)的渲染進(jìn)程), 然后在這前提下,看下整個(gè)的過(guò)程:(簡(jiǎn)化了很多)
Browser 進(jìn)程收到用戶請(qǐng)求,首先需要獲取頁(yè)面內(nèi)容(譬如通過(guò)網(wǎng)絡(luò)下載資源),隨后將該任務(wù)通過(guò) RendererHost 接口傳遞給 Render (內(nèi)核)進(jìn)程
Renderer 進(jìn)程的 Renderer 接口收到消息,簡(jiǎn)單解釋后,交給渲染線程,然后開(kāi)始渲染
渲染線程接收請(qǐng)求,加載網(wǎng)頁(yè)并渲染網(wǎng)頁(yè),這其中可能需要 Browser 進(jìn)程獲取資源和需要 GPU 進(jìn)程來(lái)幫助渲染
當(dāng)然可能會(huì)有 JS 線程操作 DOM(這樣可能會(huì)造成回流并重繪)
最后 Render 進(jìn)程將結(jié)果傳遞給 Browser 進(jìn)程
Browser 進(jìn)程接收到結(jié)果并將結(jié)果繪制出來(lái)
這里繪一張簡(jiǎn)單的圖:(很簡(jiǎn)化)
到了這里,已經(jīng)對(duì)瀏覽器的運(yùn)行有了一個(gè)整體的概念,接下來(lái),先簡(jiǎn)單梳理一些概念
由于 JavaScript 是可操縱 DOM 的,如果在修改這些元素屬性同時(shí)渲染界面(即 JS 線程和 UI 線程同時(shí)運(yùn)行),那么渲染線程前后獲得的元素?cái)?shù)據(jù)就可能不一致了。
因此為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果,瀏覽器設(shè)置 GUI 渲染線程與 JS 引擎為互斥的關(guān)系,當(dāng) JS 引擎執(zhí)行時(shí) GUI 線程會(huì)被掛起, GUI 更新則會(huì)被保存在一個(gè)隊(duì)列中等到 JS 引擎線程空閑時(shí)立即被執(zhí)行。
從上述的互斥關(guān)系,可以推導(dǎo)出,JS 如果執(zhí)行時(shí)間過(guò)長(zhǎng)就會(huì)阻塞頁(yè)面。
譬如,假設(shè) JS 引擎正在進(jìn)行巨量的計(jì)算,此時(shí)就算 GUI 有更新,也會(huì)被保存到隊(duì)列中,等待 JS 引擎空閑后執(zhí)行。 然后,由于巨量計(jì)算,所以 JS 引擎很可能很久很久后才能空閑,自然會(huì)感覺(jué)到巨卡無(wú)比。
所以,要盡量避免 JS 執(zhí)行時(shí)間過(guò)長(zhǎng),這樣就會(huì)造成頁(yè)面的渲染不連貫,導(dǎo)致頁(yè)面渲染加載阻塞的感覺(jué)。
解決這種問(wèn)題,除了將運(yùn)算放在后端,如果避不開(kāi),并且巨量計(jì)算還和 UI 有關(guān)系,那么我的思路就是使用 setTimeout 將任務(wù)分割,中間給出一點(diǎn)空閑時(shí)間讓 JS 引擎去處理下 UI,不至于頁(yè)面直接卡死。
如果直接決定最低要求 HTML5+ 的版本,那么可以看看下面的 WebWorker。
前文中有提到 JS 引擎是單線程的,而且 JS 執(zhí)行時(shí)間過(guò)長(zhǎng)會(huì)阻塞頁(yè)面,那么 JS 就真的對(duì) cpu 密集型計(jì)算無(wú)能為力么?
所以,后來(lái) HTML5 中支持了Web Worker。
MDN 的官方解釋是:
Web Worker 為 Web 內(nèi)容在后臺(tái)線程中運(yùn)行腳本提供了一種簡(jiǎn)單的方法。
線程可以執(zhí)行任務(wù)而不干擾用戶界面,一個(gè) worker 是使用一個(gè)構(gòu)造函數(shù)創(chuàng)建的一個(gè)對(duì)象(Worker())運(yùn)行一個(gè)命名的 JavaScript 文件(這個(gè)文件包含將在工作線程中運(yùn)行的代碼)。
workers 運(yùn)行在另一個(gè)全局上下文中,不同于當(dāng)前的 window。
因此,使用 window 快捷方式獲取當(dāng)前全局的范圍(而不是 self)在一個(gè) Worker 內(nèi)將返回錯(cuò)誤
這樣理解下:
創(chuàng)建 Worker 時(shí),JS 引擎向?yàn)g覽器申請(qǐng)開(kāi)一個(gè)子線程(子線程是瀏覽器開(kāi)的,完全受主線程控制,而且不能操作 DOM)
JS 引擎線程與 worker 線程間通過(guò)特定的方式通信(postMessage API,需要通過(guò)序列化對(duì)象來(lái)與線程交互特定的數(shù)據(jù))
所以,如果有非常耗時(shí)的工作,請(qǐng)單獨(dú)開(kāi)一個(gè) Worker 線程,這樣里面不管如何翻天覆地都不會(huì)影響 JS 引擎主線程, 只待計(jì)算出結(jié)果后,將結(jié)果通信給主線程即可,perfect!
而且注意下,JS 引擎是單線程的,這一點(diǎn)的本質(zhì)仍然未改變,Worker 可以理解是瀏覽器給 JS 引擎開(kāi)的外掛,專(zhuān)門(mén)用來(lái)解決那些大量計(jì)算問(wèn)題。
其它,關(guān)于 Worker 的詳解就不是本文的范疇了,因此不再贅述。
既然都到了這里,就再提一下 SharedWorker(避免后續(xù)將這兩個(gè)概念搞混)
WebWorker 只屬于某個(gè)頁(yè)面,不會(huì)和其他頁(yè)面的 Render 進(jìn)程(瀏覽器內(nèi)核進(jìn)程)共享
所以 Chrome 在 Render 進(jìn)程中(每一個(gè) Tab 頁(yè)就是一個(gè) Render 進(jìn)程)創(chuàng)建一個(gè)新的線程來(lái)運(yùn)行 Worker 中的 JavaScript 程序。
SharedWorker 是瀏覽器所有頁(yè)面共享的,不能采用與 Worker 同樣的方式實(shí)現(xiàn),因?yàn)樗浑`屬于某個(gè) Render 進(jìn)程,可以為多個(gè) Render 進(jìn)程共享使用
所以 Chrome 瀏覽器為 SharedWorker 單獨(dú)創(chuàng)建一個(gè)進(jìn)程來(lái)運(yùn)行 JavaScript 程序,在瀏覽器中每個(gè)相同的 JavaScript 只存在一個(gè) SharedWorker 進(jìn)程,不管它被創(chuàng)建多少次。
看到這里,應(yīng)該就很容易明白了,本質(zhì)上就是進(jìn)程和線程的區(qū)別。SharedWorker 由獨(dú)立的進(jìn)程管理,WebWorker 只是屬于 Render 進(jìn)程下的一個(gè)線程。
補(bǔ)充下瀏覽器的渲染流程(簡(jiǎn)單版本)
為了簡(jiǎn)化理解,前期工作直接省略成:
瀏覽器輸入 url,瀏覽器主進(jìn)程接管,開(kāi)一個(gè)下載線程,然后進(jìn)行 http 請(qǐng)求(略去 DNS 查詢(xún),IP 尋址等等操作),然后等待響應(yīng),獲取內(nèi)容,隨后將內(nèi)容通過(guò) RendererHost 接口轉(zhuǎn)交給 Renderer 進(jìn)程
瀏覽器渲染流程開(kāi)始
瀏覽器器內(nèi)核拿到內(nèi)容后,渲染大概可以劃分成以下幾個(gè)步驟:
解析 html 建立 dom 樹(shù)
解析 css 構(gòu)建 render 樹(shù)(將 CSS 代碼解析成樹(shù)形的數(shù)據(jù)結(jié)構(gòu),然后結(jié)合 DOM 合并成 render 樹(shù))
布局 render 樹(shù)(Layout/reflow),負(fù)責(zé)各元素尺寸、位置的計(jì)算
繪制 render 樹(shù)(paint),繪制頁(yè)面像素信息
瀏覽器會(huì)將各層的信息發(fā)送給 GPU,GPU 會(huì)將各層合成(composite),顯示在屏幕上。
所有詳細(xì)步驟都已經(jīng)略去,渲染完畢后就是 load 事件了,之后就是自己的 JS 邏輯處理了。
既然略去了一些詳細(xì)的步驟,那么就提一些可能需要注意的細(xì)節(jié)把。
上面提到,渲染完畢后會(huì)觸發(fā) load 事件,那么你能分清楚 load 事件與 DOMContentLoaded 事件的先后么?
很簡(jiǎn)單,知道它們的定義就可以了:
當(dāng) DOMContentLoaded 事件觸發(fā)時(shí),僅當(dāng) DOM 加載完成,不包括樣式表,圖片,async 腳本等。
當(dāng) onload 事件觸發(fā)時(shí),頁(yè)面上所有的 DOM,樣式表,腳本,圖片都已經(jīng)加載完成了,也就是渲染完畢了
所以,順序是:DOMContentLoaded -> load
這里說(shuō)的是頭部引入 css 的情況
首先,我們都知道:css 是由單獨(dú)的下載線程異步下載的。
然后再說(shuō)下幾個(gè)現(xiàn)象:
css 加載不會(huì)阻塞 DOM 樹(shù)解析(異步加載時(shí) DOM 照常構(gòu)建)
但會(huì)阻塞 render 樹(shù)渲染(渲染時(shí)需等 css 加載完畢,因?yàn)?render 樹(shù)需要 css 信息)
這可能也是瀏覽器的一種優(yōu)化機(jī)制。因?yàn)槟慵虞d css 的時(shí)候,可能會(huì)修改下面 DOM 節(jié)點(diǎn)的樣式,如果 css 加載不阻塞 render 樹(shù)渲染的話,那么當(dāng) css 加載完之后, render 樹(shù)可能又得重新重繪或者回流了,這就造成了一些沒(méi)有必要的損耗。
所以干脆就先把 DOM 樹(shù)的結(jié)構(gòu)先解析完,把可以做的工作做完,然后等你 css 加載完之后, 在根據(jù)最終的樣式來(lái)渲染 render 樹(shù),這種做法性能方面確實(shí)會(huì)比較好一點(diǎn)。
渲染步驟中就提到了 composite 概念。
可以簡(jiǎn)單的這樣理解,瀏覽器渲染的圖層一般包含兩大類(lèi):普通圖層 以及 復(fù)合圖層
首先,普通文檔流內(nèi)可以理解為一個(gè)復(fù)合圖層(這里稱(chēng)為 默認(rèn)復(fù)合層,里面不管添加多少元素,其實(shí)都是在同一個(gè)復(fù)合圖層中)
其次,absolute 布局(fixed 也一樣),雖然可以脫離普通文檔流,但它仍然屬于 默認(rèn)復(fù)合層。
然后,可以通過(guò) 硬件加速 的方式,聲明一個(gè) 新的復(fù)合圖層,它會(huì)單獨(dú)分配資源 (當(dāng)然也會(huì)脫離普通文檔流,這樣一來(lái),不管這個(gè)復(fù)合圖層中怎么變化,也不會(huì)影響 默認(rèn)復(fù)合層 里的回流重繪)
可以簡(jiǎn)單理解下:GPU 中,各個(gè)復(fù)合圖層是單獨(dú)繪制的,所以互不影響,這也是為什么某些場(chǎng)景硬件加速效果一級(jí)棒
可以Chrome DevTools --> More Tools --> Rendering --> Layer borders中看到,黃色的就是復(fù)合圖層信息
將該元素變成一個(gè)復(fù)合圖層,就是傳說(shuō)中的硬件加速技術(shù)
最常用的方式:translate3d、translateZ
opacity 屬性/過(guò)渡動(dòng)畫(huà)(需要?jiǎng)赢?huà)執(zhí)行的過(guò)程中才會(huì)創(chuàng)建合成層,動(dòng)畫(huà)沒(méi)有開(kāi)始或結(jié)束后元素還會(huì)回到之前的狀態(tài))
will-chang 屬性(這個(gè)比較偏僻),一般配合 opacity 與 translate 使用, 作用是提前告訴瀏覽器要變化,這樣瀏覽器會(huì)開(kāi)始做一些優(yōu)化工作(這個(gè)最好用完后就釋放)
video、 iframe、 canvas、 webgl 等元素
其它,譬如以前的 flash 插件
absolute和硬件加速的區(qū)別
可以看到,absolute 雖然可以脫離普通文檔流,但是無(wú)法脫離默認(rèn)復(fù)合層。 所以,就算 absolute 中信息改變時(shí)不會(huì)改變普通文檔流中 render 樹(shù), 但是,瀏覽器最終繪制時(shí),是整個(gè)復(fù)合層繪制的,所以 absolute 中信息的改變,仍然會(huì)影響整個(gè)復(fù)合層的繪制。 (瀏覽器會(huì)重繪它,如果復(fù)合層中內(nèi)容多,absolute 帶來(lái)的繪制信息變化過(guò)大,資源消耗是非常嚴(yán)重的)
而硬件加速直接就是在另一個(gè)復(fù)合層了(另起爐灶),所以它的信息改變不會(huì)影響默認(rèn)復(fù)合層 (當(dāng)然了,內(nèi)部肯定會(huì)影響屬于自己的復(fù)合層),僅僅是引發(fā)最后的合成(輸出視圖)
一般一個(gè)元素開(kāi)啟硬件加速后會(huì)變成復(fù)合圖層,可以獨(dú)立于普通文檔流中,改動(dòng)后可以避免整個(gè)頁(yè)面重繪,提升性能,但是盡量不要大量使用復(fù)合圖層,否則由于資源消耗過(guò)度,頁(yè)面反而會(huì)變的更卡
使用硬件加速時(shí),盡可能的使用 index,防止瀏覽器默認(rèn)給后續(xù)的元素創(chuàng)建復(fù)合層渲染
具體的原理時(shí)這樣的: webkit CSS3 中,如果這個(gè)元素添加了硬件加速,并且 index 層級(jí)比較低, 那么在這個(gè)元素的后面其它元素(層級(jí)比這個(gè)元素高的,或者相同的,并且 releative 或 absolute 屬性相同的), 會(huì)默認(rèn)變?yōu)閺?fù)合層渲染,如果處理不當(dāng)會(huì)極大的影響性能
簡(jiǎn)單點(diǎn)理解,其實(shí)可以認(rèn)為是一個(gè)隱式合成的概念:如果 a 是一個(gè)復(fù)合圖層,而且 b 在 a 上面,那么 b 也會(huì)被隱式轉(zhuǎn)為一個(gè)復(fù)合圖層,這點(diǎn)需要特別注意。
到此時(shí),已經(jīng)是屬于瀏覽器頁(yè)面初次渲染完畢后的事情,JS 引擎的一些運(yùn)行機(jī)制分析。
注意,這里不談 可執(zhí)行上下文,VO,scop chain 等概念(這些完全可以整理成另一篇文章了),這里主要是結(jié)合 Event Loop 來(lái)談 JS 代碼是如何執(zhí)行的。
讀這部分的前提是已經(jīng)知道了 JS 引擎是單線程,而且這里會(huì)用到上文中的幾個(gè)概念:
JS 引擎線程
事件觸發(fā)線程
定時(shí)觸發(fā)器線程
然后再理解一個(gè)概念:
JS 分為同步任務(wù)和異步任務(wù)
同步任務(wù)都在主線程上執(zhí)行,形成一個(gè) 執(zhí)行棧
主線程之外,事件觸發(fā)線程管理著一個(gè) 任務(wù)隊(duì)列,只要異步任務(wù)有了運(yùn)行結(jié)果,就在 任務(wù)隊(duì)列 之中放置一個(gè)事件。
一旦 執(zhí)行棧 中的所有同步任務(wù)執(zhí)行完畢(此時(shí) JS 引擎空閑),系統(tǒng)就會(huì)讀取 任務(wù)隊(duì)列,將可運(yùn)行的異步任務(wù)添加到可執(zhí)行棧中,開(kāi)始執(zhí)行。
看圖:
看到這里,應(yīng)該就可以理解了:為什么有時(shí)候 setTimeout 推入的事件不能準(zhǔn)時(shí)執(zhí)行?因?yàn)榭赡茉谒迫氲绞录斜頃r(shí),主線程還不空閑,正在執(zhí)行其它代碼, 所以自然有誤差。

上圖大致描述就是:
主線程運(yùn)行時(shí)會(huì)產(chǎn)生執(zhí)行棧, 棧中的代碼調(diào)用某些 api 時(shí),它們會(huì)在事件隊(duì)列中添加各種事件(當(dāng)滿足觸發(fā)條件后,如 ajax 請(qǐng)求完畢)
而棧中的代碼執(zhí)行完畢,就會(huì)讀取事件隊(duì)列中的事件,去執(zhí)行那些回調(diào),如此循環(huán)
注意,總是要等待棧中的代碼執(zhí)行完畢后才會(huì)去讀取事件隊(duì)列中的事件
上述事件循環(huán)機(jī)制的核心是:JS 引擎線程和事件觸發(fā)線程
但事件上,里面還有一些隱藏細(xì)節(jié),譬如調(diào)用 setTimeout 后,是如何等待特定時(shí)間后才添加到事件隊(duì)列中的?
是 JS 引擎檢測(cè)的么?當(dāng)然不是了。它是由定時(shí)器線程控制(因?yàn)?JS 引擎自己都忙不過(guò)來(lái),根本無(wú)暇分身)
為什么要單獨(dú)的定時(shí)器線程?因?yàn)?JavaScript 引擎是單線程的,如果處于阻塞線程狀態(tài)就會(huì)影響記計(jì)時(shí)的準(zhǔn)確,因此很有必要單獨(dú)開(kāi)一個(gè)線程用來(lái)計(jì)時(shí)。
什么時(shí)候會(huì)用到定時(shí)器線程?當(dāng)使用 setTimeout 或 setInterval 時(shí),它需要定時(shí)器線程計(jì)時(shí),計(jì)時(shí)完成后就會(huì)將特定的事件推入事件隊(duì)列中。
用 setTimeout 模擬定期計(jì)時(shí)和直接用 setInterval 是有區(qū)別的。
因?yàn)槊看?setTimeout 計(jì)時(shí)到后就會(huì)去執(zhí)行,然后執(zhí)行一段時(shí)間后才會(huì)繼續(xù) setTimeout,中間就多了誤差 (誤差多少與代碼執(zhí)行時(shí)間有關(guān))
而 setInterval 則是每次都精確的隔一段時(shí)間推入一個(gè)事件,但是,事件的實(shí)際執(zhí)行時(shí)間不一定就準(zhǔn)確,還有可能是這個(gè)事件還沒(méi)執(zhí)行完畢,下一個(gè)事件就來(lái)了。
而且 setInterval 有一些比較致命的問(wèn)題就是:
累計(jì)效應(yīng),如果 setInterval 代碼在再次添加到隊(duì)列之前還沒(méi)有完成執(zhí)行, 就會(huì)導(dǎo)致定時(shí)器代碼連續(xù)運(yùn)行好幾次,而之間沒(méi)有間隔。 就算正常間隔執(zhí)行,多個(gè) setInterval 的代碼執(zhí)行時(shí)間可能會(huì)比預(yù)期小(因?yàn)榇a執(zhí)行需要一定時(shí)間)
譬如像 iOS 的 webview,或者 Safari 等瀏覽器中都有一個(gè)特點(diǎn),在滾動(dòng)的時(shí)候是不執(zhí)行JS的,如果使用了 setInterval,會(huì)發(fā)現(xiàn)在滾動(dòng)結(jié)束后會(huì)執(zhí)行多次由于滾動(dòng)不執(zhí)行 JS 積攢回調(diào),如果回調(diào)執(zhí)行時(shí)間過(guò)長(zhǎng),就會(huì)非常容器造成卡頓問(wèn)題和一些不可知的錯(cuò)誤(這一塊后續(xù)有補(bǔ)充,setInterval 自帶的優(yōu)化,不會(huì)重復(fù)添加回調(diào))
而且把瀏覽器最小化顯示等操作時(shí),setInterval 并不是不執(zhí)行程序, 它會(huì)把 setInterval 的回調(diào)函數(shù)放在隊(duì)列中,等瀏覽器窗口再次打開(kāi)時(shí),一瞬間全部執(zhí)行
所以,鑒于這么多問(wèn)題,目前一般認(rèn)為的最佳方案是:用 setTimeout 模擬 setInterval,或者特殊場(chǎng)合直接用 requestAnimationFrame
補(bǔ)充:JS 高程中有提到,JS 引擎會(huì)對(duì) setInterval 進(jìn)行優(yōu)化,如果當(dāng)前事件隊(duì)列中有 setInterval 的回調(diào),不會(huì)重復(fù)添加。
上文中將 JS 事件循環(huán)機(jī)制梳理了一遍,在 ES5 的情況是夠用了,但是在 ES6 盛行的現(xiàn)在,仍然會(huì)遇到一些問(wèn)題,譬如下面這題:
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');嗯哼,它的正確執(zhí)行順序是這樣子的:
script start script end promise1 promise2 setTimeout
為什么呢?因?yàn)?Promise 里有了一個(gè)一個(gè)新的概念:microtask
或者,進(jìn)一步,JS 中分為兩種任務(wù)類(lèi)型:macrotask 和 microtask,在 ECMAScript 中,microtask 稱(chēng)為 jobs,macrotask 可稱(chēng)為task。
它們的定義?區(qū)別?簡(jiǎn)單點(diǎn)可以按如下理解:
可以理解是每次執(zhí)行棧執(zhí)行的代碼就是一個(gè)宏任務(wù)(包括每次從事件隊(duì)列中獲取一個(gè)事件回調(diào)并放到執(zhí)行棧中執(zhí)行)
每一個(gè) task 會(huì)從頭到尾將這個(gè)任務(wù)執(zhí)行完畢,不會(huì)執(zhí)行其它
瀏覽器為了能夠使得 JS 內(nèi)部 task 與 DOM 任務(wù)能夠有序的執(zhí)行,會(huì)在一個(gè) task 執(zhí)行結(jié)束后,在下一個(gè) task 執(zhí)行開(kāi)始前,對(duì)頁(yè)面進(jìn)行重新渲染 (task -> 渲染 -> task -> …)
可以理解是在當(dāng)前 task 執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)
也就是說(shuō),在當(dāng)前 task 任務(wù)后,下一個(gè) task 之前,在渲染之前
所以它的響應(yīng)速度相比 setTimeout(setTimeout 是 task)會(huì)更快,因?yàn)闊o(wú)需等渲染
也就是說(shuō),在某一個(gè) macrotask 執(zhí)行完后,就會(huì)將在它執(zhí)行期間產(chǎn)生的所有 microtask 都執(zhí)行完畢(在渲染前)
macrotask:主代碼塊,setTimeout,setInterval 等(事件隊(duì)列中的每一個(gè)事件都是一個(gè) macrotask)
microtask:Promise,process.nextTick 等
補(bǔ)充:在 node 環(huán)境下,process.nextTick 的優(yōu)先級(jí)高于 Promise,也就是可以簡(jiǎn)單理解為:在宏任務(wù)結(jié)束后會(huì)先執(zhí)行微任務(wù)隊(duì)列中的 nextTickQueue 部分,然后才會(huì)執(zhí)行微任務(wù)中的 Promise 部分。
再根據(jù)線程來(lái)理解下:
(1)macrotask 中的事件都是放在一個(gè)事件隊(duì)列中的,而這個(gè)隊(duì)列由事件觸發(fā)線程維護(hù)
(2)microtask 中的所有微任務(wù)都是添加到微任務(wù)隊(duì)列(Job Queues)中,等待當(dāng)前 macrotask 執(zhí)行完畢后執(zhí)行,而這個(gè)隊(duì)列由 JS 引擎線程維護(hù) (這點(diǎn)由自己理解+推測(cè)得出,因?yàn)樗窃谥骶€程下無(wú)縫執(zhí)行的)
所以,總結(jié)下運(yùn)行機(jī)制:
執(zhí)行一個(gè)宏任務(wù)(棧中沒(méi)有就從事件隊(duì)列中獲取)
執(zhí)行過(guò)程中如果遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊(duì)列中
宏任務(wù)執(zhí)行完畢后,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)
當(dāng)前宏任務(wù)執(zhí)行完畢,開(kāi)始檢查渲染,然后GUI線程接管渲染
渲染完畢后,JS線程繼續(xù)接管,開(kāi)始下一個(gè)宏任務(wù)(從事件隊(duì)列中獲取)
如圖:
另外,請(qǐng)注意下 Promise 的 polyfill 與官方版本的區(qū)別:
官方版本中,是標(biāo)準(zhǔn)的 microtask 形式
polyfill,一般都是通過(guò) setTimeout 模擬的,所以是 macrotask 形式
注意,有一些瀏覽器執(zhí)行結(jié)果不一樣(因?yàn)樗鼈兛赡馨?microtask 當(dāng)成 macrotask 來(lái)執(zhí)行了), 但是為了簡(jiǎn)單,這里不描述一些不標(biāo)準(zhǔn)的瀏覽器下的場(chǎng)景(但記住,有些瀏覽器可能并不標(biāo)準(zhǔn))
補(bǔ)充:使用MutationObserver實(shí)現(xiàn)microtask
MutationObserver可以用來(lái)實(shí)現(xiàn)microtask (它屬于microtask,優(yōu)先級(jí)小于Promise, 一般是Promise不支持時(shí)才會(huì)這樣做)
它是HTML5中的新特性,作用是:監(jiān)聽(tīng)一個(gè)DOM變動(dòng), 當(dāng)DOM對(duì)象樹(shù)發(fā)生任何變動(dòng)時(shí),Mutation Observer會(huì)得到通知
像以前的Vue源碼中就是利用它來(lái)模擬nextTick的, 具體原理是,創(chuàng)建一個(gè)TextNode并監(jiān)聽(tīng)內(nèi)容變化, 然后要nextTick的時(shí)候去改一下這個(gè)節(jié)點(diǎn)的文本內(nèi)容, 如下:
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)
}不過(guò),現(xiàn)在的Vue(2.5+)的nextTick實(shí)現(xiàn)移除了MutationObserver的方式(據(jù)說(shuō)是兼容性原因), 取而代之的是使用MessageChannel (當(dāng)然,默認(rèn)情況仍然是Promise,不支持才兼容的)。
MessageChannel屬于宏任務(wù),優(yōu)先級(jí)是:MessageChannel->setTimeout, 所以Vue(2.5+)內(nèi)部的nextTick與2.4及之前的實(shí)現(xiàn)是不一樣的,需要注意下。
到此,關(guān)于“JavaScript運(yùn)行機(jī)制及原理是什么”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!
當(dāng)前標(biāo)題:JavaScript運(yùn)行機(jī)制及原理是什么
文章鏈接:http://www.chinadenli.net/article46/iphgeg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供標(biāo)簽優(yōu)化、品牌網(wǎng)站建設(shè)、商城網(wǎng)站、面包屑導(dǎo)航、微信小程序、營(yíng)銷(xiāo)型網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)