整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          小鄭詳解JavaScript的運(yùn)行機(jī)制

          擊右上方紅色按鈕關(guān)注“小鄭搞碼事”,每天都能學(xué)到知識,搞懂一個(gè)問題!

          先來看這段代碼,大家看表示什么意思。

          一眼看過去,非常熟悉,通常我們會(huì)這樣理解:這段代碼表示1秒后,會(huì)執(zhí)行setTimeout里面那個(gè)函數(shù)。

          然而,這種解釋并不準(zhǔn)確, 那應(yīng)該怎么理解呢? 我們先來理解JavaScript的遠(yuǎn)行機(jī)制。

          本文最后我會(huì)給出JavaScript的運(yùn)行機(jī)制完整圖。但在這之前,我想先來給大家解釋幾個(gè)問題。

          1. JavaScript為什么是單線程的?

          JavaScript設(shè)計(jì)的初衷是用在瀏覽器中, 那么,我們來想象一下,如果JavaScript是多線程的話。

          必然可以有兩個(gè)進(jìn)程,process1和process2,那么這兩個(gè)進(jìn)程可以同時(shí)對同一個(gè)DOM進(jìn)行操作。如果這個(gè)時(shí)候,一個(gè)進(jìn)程要?jiǎng)h除這個(gè)DOM,另一個(gè)進(jìn)程要編輯這個(gè)DOM。啟不是矛盾嘛。

          所以,這樣應(yīng)該更好理解,JS為什么是單線程了。

          2. JavaScript為什么需要異步?

          單線程為什么需要異步呢?

          JavaScript如果不存在異步,而是自上而下執(zhí)行,這樣的話,假如上一行解析時(shí)間很長,那么下面的代碼直接就會(huì)被阻塞。這種現(xiàn)象對于用戶來說,意味著"卡死"。嚴(yán)重影響用戶流失,這樣解釋好理解吧,所以JavaScript需要異步處理。

          3. 單線程如何實(shí)現(xiàn)異步呢?

          JavaScript竟然需要異步,那么它是如何實(shí)現(xiàn)異步的呢?

          JavaScript是通過事件循環(huán)(event loop)來實(shí)現(xiàn)的,事件循環(huán)機(jī)制也就是今天要說的JavaScript運(yùn)行機(jī)制。

          (一)同步任務(wù)和異步任務(wù)

          來看一段代碼:

          首先這段代碼輸出結(jié)果是啥?

          輸出:1 3 2

          其中setTimeout需要延遲一段時(shí)間才去執(zhí)行,這類代碼就是異步代碼。

          看到這個(gè)結(jié)果,所以通常我們都這么理解JS的執(zhí)行原理:

          第一,判斷JS是同步還是異步,同步進(jìn)入主線程,異步則進(jìn)入event table。

          第二,異步任務(wù)在event table中注冊函數(shù),當(dāng)滿足觸發(fā)條件后,被推入event queue(事件隊(duì)列)。

          第三,同步任務(wù)進(jìn)入主線程后一直執(zhí)行,直到主線程空閑,才會(huì)去event queue中查看是否有可執(zhí)行的異步任務(wù),如果有就推入主線程。

          按到這個(gè)邏輯,上面這段實(shí)例代碼,是不是就很好理解了。1,3是同步任務(wù)進(jìn)入主要線程,自上而下執(zhí)行,2是異步任務(wù),滿足觸發(fā)條件后,推入事件隊(duì)列,等待主線程有空時(shí)調(diào)用。

          (二)宏任務(wù)(macro-task)和微任務(wù)(micro-task)

          然而,按照同步和異步任務(wù)來理解JS的運(yùn)行機(jī)制似乎并不準(zhǔn)確。

          來看一段代碼。看看它的輸出順序。

          上面這段代碼,按同步和異步的理解,輸出結(jié)果是:2,4,1,3。因?yàn)?,4是同步任務(wù),按順序在主線程自上而下執(zhí)行,而1,3是異步任務(wù),按順序在主線程有空后自先而后執(zhí)行。

          可事實(shí)輸出并不是這個(gè)結(jié)果,而是這樣的:2,4,3,1。為什么呢?來理解一下宏任務(wù)和微任務(wù)。

          寵任務(wù):包括整體script代碼,setTimeout,setInterval。

          微任務(wù):Promise,process.nextTick。

          來看原理圖:

          嗯,對,這就是JS的運(yùn)行機(jī)制。也就是事件循環(huán)。解釋一下:

          第一,執(zhí)行一個(gè)宏任務(wù)(主線程的同步script代碼),過程中如果遇到微任務(wù),就將其放到微任務(wù)的事件隊(duì)列里。

          第二,當(dāng)前宏任務(wù)執(zhí)行完成后,會(huì)查微任務(wù)的事件隊(duì)列,將將全部的微任務(wù)依次執(zhí)行完,再去依次執(zhí)行宏任務(wù)事件隊(duì)列。

          上面代碼中promise的then是一微任務(wù),因此它的執(zhí)行在setTimeout之前。

          需要注意的是:在node環(huán)境下,process.nextTick的優(yōu)先級高于promise。也就是可以簡單理解為,在宏任務(wù)結(jié)束后會(huì)先執(zhí)行微任務(wù)隊(duì)列中的nextTickQueue部分,然后才會(huì)執(zhí)行微任務(wù)中的promise部分。

          所以最后總結(jié)一下,對于文章一開頭提到的那段代碼,我們可以準(zhǔn)確的理解為:

          1秒后,setTimeout里的函數(shù)會(huì)被推入event queue,而event queue(事件隊(duì)列)里的任務(wù),只有在主線程空閑時(shí)才會(huì)執(zhí)行。也就是需要同時(shí)滿足兩個(gè)條件(1)1秒后。(2)主線程必須空閑,這樣1秒后才會(huì)執(zhí)行該函數(shù)。

          現(xiàn)在,關(guān)于JavaScript的運(yùn)行機(jī)制,大家應(yīng)該都理解了,有問題歡迎留言。

          事跟我說他用jQuery取不到頁面上隱藏元素input的值,他的html頁面大概內(nèi)容如下。

          <!DOCTYPE html>
          <html lang="zh">
           
          <head>
          	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
          	<script type="text/javascript" src="jslib/jquery-1.11.2.min.js"></script>
          	<title>淺談Html頁面內(nèi)容執(zhí)行順序</title>
          	<script type="text/javascript">
          		var userId = $('#hiddenUserId').val();
          		var contextPath = $('#hiddenContextPath').val();
          		var userName = $('#hiddenUserName').val();
          	</script>
          </head>
           
          <body>
          	<input type="hidden" id="hiddenUserId" value="101" />
          	<input type="hidden" id="hiddenContextPath" value="/web" />
          	<input type="hidden" id="hiddenUserName" value="小明" />
          </body>
           
          </html>

          頁面中的JS腳本在head中,JS腳本要讀取的input在body中。瀏覽器對html頁面內(nèi)容的加載是順序加載,也就是在html頁面中前面先加載,因此當(dāng)加載到JS腳本時(shí),input還沒有加載到瀏覽器中。JS是一種解釋性的腳本,也是從上而下順序執(zhí)行,由于這段JS代碼是立即執(zhí)行的,所以當(dāng)JS在執(zhí)行的時(shí)候,讀取不到input的值。

          最直接的修改方法是把JS放到網(wǎng)頁的最下面執(zhí)行。

          <!DOCTYPE html>
          <html lang="zh">
           
          <head>
          	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
          	<script type="text/javascript" src="jslib/jquery-1.11.2.min.js"></script>
          	<title>淺談Html頁面內(nèi)容執(zhí)行順序</title>	
          </head>
           
          <body>
          	<input type="hidden" id="hiddenUserId" value="101" />
          	<input type="hidden" id="hiddenContextPath" value="/web" />
          	<input type="hidden" id="hiddenUserName" value="小明" />
          	
          	<script type="text/javascript">
          		var userId = $('#hiddenUserId').val();
          		var contextPath = $('#hiddenContextPath').val();
          		var userName = $('#hiddenUserName').val();
          	</script>
          </body>
           
          </html>

          把JS放到網(wǎng)頁的最下面,這樣在JS執(zhí)行的時(shí)候,網(wǎng)頁內(nèi)容都已經(jīng)加載完畢。把JS放在網(wǎng)頁的最下面方法并不是最好的解決方法,大部分情況JS并不是總能放在網(wǎng)頁的最下面。這時(shí)可以用window的onload事件,onload事件在整個(gè)頁面都加載完成后才觸發(fā),可以把JS腳本放在onload里面執(zhí)行。不同瀏覽器onload事件添加方式也不一樣。

          IE下事件:

          window.attachEvent('onload', function(){
          			var userId = $('#hiddenUserId').val();
          			var contextPath = $('#hiddenContextPath').val();
          			var userName = $('#hiddenUserName').val();
          		});

          Chrome/Firefox等DOM標(biāo)準(zhǔn)事件:

          window.addEventListener('load', function(){
          			var userId = $('#hiddenUserId').val();
          			var contextPath = $('#hiddenContextPath').val();
          			var userName = $('#hiddenUserName').val();
          		});

          由于不同瀏覽器的事件添加方式不一樣,jQuery為我們提供了通用的初始化方法,該方法在頁面加載完成時(shí)觸發(fā)。

          $(function(){
          			var userId = $('#hiddenUserId').val();
          			var contextPath = $('#hiddenContextPath').val();
          			var userName = $('#hiddenUserName').val();
          		});

          上面方法本質(zhì)就是添加onload監(jiān)聽事件。

          最終修改后的頁面

          覽器的“心”

          瀏覽器的“心”,說的就是瀏覽器的內(nèi)核。在研究瀏覽器微觀的運(yùn)行機(jī)制之前,我們首先要對瀏覽器內(nèi)核有一個(gè)宏觀的把握。

          許多工程師因?yàn)闃I(yè)務(wù)需要,免不了需要去處理不同瀏覽器下代碼渲染結(jié)果的差異性。這些差異性正是因?yàn)闉g覽器內(nèi)核的不同而導(dǎo)致的——瀏覽器內(nèi)核決定了瀏覽器解釋網(wǎng)頁語法的方式。

          瀏覽器內(nèi)核可以分成兩部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎。早期渲染引擎和 JS 引擎并沒有十分明確的區(qū)分,但隨著 JS 引擎越來越獨(dú)立,內(nèi)核也成了渲染引擎的代稱(下文我們將沿用這種叫法)。渲染引擎又包括了 HTML 解釋器、CSS 解釋器、布局、網(wǎng)絡(luò)、存儲、圖形、音視頻、圖片解碼器等等零部件。

          目前市面上常見的瀏覽器內(nèi)核可以分為這四種:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。

          大家最耳熟能詳?shù)目赡芫褪?Webkit 內(nèi)核了。很多同學(xué)可能會(huì)聽說過 Chrome 的內(nèi)核就是 Webkit,殊不知 Chrome 內(nèi)核早已迭代為了 Blink。但是換湯不換藥,Blink 其實(shí)也是基于 Webkit 衍生而來的一個(gè)分支,因此,Webkit 內(nèi)核仍然是當(dāng)下瀏覽器世界真正的霸主。

          下面我們就以 Webkit 為例,對現(xiàn)代瀏覽器的渲染過程進(jìn)行一個(gè)深度的剖析。

          開啟瀏覽器渲染“黑盒”

          什么是渲染過程?簡單來說,渲染引擎根據(jù) HTML 文件描述構(gòu)建相應(yīng)的數(shù)學(xué)模型,調(diào)用瀏覽器各個(gè)零部件,從而將網(wǎng)頁資源代碼轉(zhuǎn)換為圖像結(jié)果,這個(gè)過程就是渲染過程(如下圖)。

          從這個(gè)流程來看,瀏覽器呈現(xiàn)網(wǎng)頁這個(gè)過程,宛如一個(gè)黑盒。在這個(gè)神秘的黑盒中,有許多功能模塊,內(nèi)核內(nèi)部的實(shí)現(xiàn)正是這些功能模塊相互配合協(xié)同工作進(jìn)行的。其中我們最需要關(guān)注的,就是HTML 解釋器CSS 解釋器圖層布局計(jì)算模塊視圖繪制模塊JavaScript 引擎這幾大模塊:

          1. HTML 解釋器:將 HTML 文檔經(jīng)過詞法分析輸出 DOM 樹。
          2. CSS 解釋器:解析 CSS 文檔, 生成樣式規(guī)則。
          3. 圖層布局計(jì)算模塊:布局計(jì)算每個(gè)對象的精確位置和大小。
          4. 視圖繪制模塊:進(jìn)行具體節(jié)點(diǎn)的圖像繪制,將像素渲染到屏幕上。
          5. JavaScript 引擎:編譯執(zhí)行 Javascript 代碼。

          瀏覽器渲染過程解析

          有了對零部件的了解打底,我們就可以一起來走一遍瀏覽器的渲染流程了。在瀏覽器里,每一個(gè)頁面的首次渲染都經(jīng)歷了如下階段(圖中箭頭不代表串行,有一些操作是并行進(jìn)行的,下文會(huì)說明):

          • 解析 HTML

          在這一步瀏覽器執(zhí)行了所有的加載解析邏輯,在解析 HTML 的過程中發(fā)出了頁面渲染所需的各種外部資源請求。

          • 計(jì)算樣式

          瀏覽器將識別并加載所有的 CSS 樣式信息與 DOM 樹合并,最終生成頁面 render 樹(:after :before 這樣的偽元素會(huì)在這個(gè)環(huán)節(jié)被構(gòu)建到 DOM 樹中)。

          • 計(jì)算圖層布局

          頁面中所有元素的相對位置信息,大小等信息均在這一步得到計(jì)算。

          • 繪制圖層

          在這一步中瀏覽器會(huì)根據(jù)我們的 DOM 代碼結(jié)果,把每一個(gè)頁面圖層轉(zhuǎn)換為像素,并對所有的媒體文件進(jìn)行解碼。

          • 整合圖層,得到頁面

          最后一步瀏覽器會(huì)合并合各個(gè)圖層,將數(shù)據(jù)由 CPU 輸出給 GPU 最終繪制在屏幕上。(復(fù)雜的視圖層會(huì)給這個(gè)階段的 GPU 計(jì)算帶來一些壓力,在實(shí)際應(yīng)用中為了優(yōu)化動(dòng)畫性能,我們有時(shí)會(huì)手動(dòng)區(qū)分不同的圖層)。

          幾棵重要的“樹”

          上面的內(nèi)容沒有理解透徹?別著急,我們一起來捋一捋這個(gè)過程中的重點(diǎn)——樹!

          為了使渲染過程更明晰一些,我們需要給這些”樹“們一個(gè)特寫:

          1. DOM 樹:解析 HTML 以創(chuàng)建的是 DOM 樹(DOM tree ):渲染引擎開始解析 HTML 文檔,轉(zhuǎn)換樹中的標(biāo)簽到 DOM 節(jié)點(diǎn),它被稱為“內(nèi)容樹”。
          2. CSSOM 樹:解析 CSS(包括外部 CSS 文件和樣式元素)創(chuàng)建的是 CSSOM 樹。CSSOM 的解析過程與 DOM 的解析過程是并行的
          3. 渲染樹:CSSOM 與 DOM 結(jié)合,之后我們得到的就是渲染樹(Render tree )。
          4. 布局渲染樹:從根節(jié)點(diǎn)遞歸調(diào)用,計(jì)算每一個(gè)元素的大小、位置等,給每個(gè)節(jié)點(diǎn)所應(yīng)該出現(xiàn)在屏幕上的精確坐標(biāo),我們便得到了基于渲染樹的布局渲染樹(Layout of the render tree)。
          5. 繪制渲染樹: 遍歷渲染樹,每個(gè)節(jié)點(diǎn)將使用 UI 后端層來繪制。整個(gè)過程叫做繪制渲染樹(Painting the render tree)。

          基于這些“樹”,我們再梳理一番:

          渲染過程說白了,首先是基于 HTML 構(gòu)建一個(gè) DOM 樹,這棵 DOM 樹與 CSS 解釋器解析出的 CSSOM 相結(jié)合,就有了布局渲染樹。最后瀏覽器以布局渲染樹為藍(lán)本,去計(jì)算布局并繪制圖像,我們頁面的初次渲染就大功告成了。

          之后每當(dāng)一個(gè)新元素加入到這個(gè) DOM 樹當(dāng)中,瀏覽器便會(huì)通過 CSS 引擎查遍 CSS 樣式表,找到符合該元素的樣式規(guī)則應(yīng)用到這個(gè)元素上,然后再重新去繪制它。

          有心的同學(xué)可能已經(jīng)在思考了,查表是個(gè)花時(shí)間的活,我怎么讓瀏覽器的查詢工作又快又好地實(shí)現(xiàn)呢?OK,講了這么多原理,我們終于引出了我們的第一個(gè)可轉(zhuǎn)化為代碼的優(yōu)化點(diǎn)——CSS 樣式表規(guī)則的優(yōu)化!

          不做無用功:基于渲染流程的 CSS 優(yōu)化建議

          在給出 CSS 選擇器方面的優(yōu)化建議之前,先告訴大家一個(gè)小知識:CSS 引擎查找樣式表,對每條規(guī)則都按從右到左的順序去匹配。 看如下規(guī)則:

          #myList li {}
          

          這樣的寫法其實(shí)很常見。大家平時(shí)習(xí)慣了從左到右閱讀的文字閱讀方式,會(huì)本能地以為瀏覽器也是從左到右匹配 CSS 選擇器的,因此會(huì)推測這個(gè)選擇器并不會(huì)費(fèi)多少力氣:#myList 是一個(gè) id 選擇器,它對應(yīng)的元素只有一個(gè),查找起來應(yīng)該很快。定位到了 myList 元素,等于是縮小了范圍后再去查找它后代中的 li 元素,沒毛病。

          事實(shí)上,CSS 選擇符是從右到左進(jìn)行匹配的。我們這個(gè)看似“沒毛病”的選擇器,實(shí)際開銷相當(dāng)高:瀏覽器必須遍歷頁面上每個(gè) li 元素,并且每次都要去確認(rèn)這個(gè) li 元素的父元素 id 是不是 myList,你說坑不坑!

          說到坑,不知道大家還記不記得這個(gè)經(jīng)典的通配符:

          * {}
          

          入門 CSS 的時(shí)候,不少同學(xué)拿通配符清除默認(rèn)樣式(我曾經(jīng)也是通配符用戶的一員)。但這個(gè)家伙很恐怖,它會(huì)匹配所有元素,所以瀏覽器必須去遍歷每一個(gè)元素!大家低頭看看自己頁面里的元素個(gè)數(shù),是不是心涼了——這得計(jì)算多少次呀!

          這樣一看,一個(gè)小小的 CSS 選擇器,也有不少的門道!好的 CSS 選擇器書寫習(xí)慣,可以為我們帶來非常可觀的性能提升。根據(jù)上面的分析,我們至少可以總結(jié)出如下性能提升的方案:

          1. 避免使用通配符,只對需要用到的元素進(jìn)行選擇。
          2. 關(guān)注可以通過繼承實(shí)現(xiàn)的屬性,避免重復(fù)匹配重復(fù)定義。
          3. 少用標(biāo)簽選擇器。如果可以,用類選擇器替代,舉個(gè)例子:

          錯(cuò)誤示范:

          #myList li{}
          

          理想:

          .myList_li {}
          

          不要畫蛇添足,id 和 class 選擇器不應(yīng)該被多余的標(biāo)簽選擇器拖后腿。

          錯(cuò)誤示范

          .myList#title
          

          理想:

          #title
          

          減少嵌套。后代選擇器的開銷是最高的,因此我們應(yīng)該盡量將選擇器的深度降到最低(最高不要超過三層),盡可能使用類來關(guān)聯(lián)每一個(gè)標(biāo)簽元素。

          搞定了 CSS 選擇器,萬里長征才剛剛開始的第一步。但現(xiàn)在你已經(jīng)理解了瀏覽器的工作過程,接下來的征程對你來說并不再是什么難題~

          告別阻塞:CSS 與 JS 的加載順序優(yōu)化

          說完了過程,我們來說一說特性。

          HTML、CSS 和 JS,都具有阻塞渲染的特性。

          HTML 阻塞,天經(jīng)地義——沒有 HTML,何來 DOM?沒有 DOM,渲染和優(yōu)化,都是空談。

          那么 CSS 和 JS 的阻塞又是怎么回事呢?

          CSS 的阻塞

          在剛剛的過程中,我們提到 DOM 和 CSSOM 合力才能構(gòu)建渲染樹。這一點(diǎn)會(huì)給性能造成嚴(yán)重影響:默認(rèn)情況下,CSS 是阻塞的資源。瀏覽器在構(gòu)建 CSSOM 的過程中,不會(huì)渲染任何已處理的內(nèi)容。即便 DOM 已經(jīng)解析完畢了,只要 CSSOM 不 OK,那么渲染這個(gè)事情就不 OK(這主要是為了避免沒有 CSS 的 HTML 頁面丑陋地“裸奔”在用戶眼前)。

          我們知道,只有當(dāng)我們開始解析 HTML 后、解析到 link 標(biāo)簽或者 style 標(biāo)簽時(shí),CSS 才登場,CSSOM 的構(gòu)建才開始。很多時(shí)候,DOM 不得不等待 CSSOM。因此我們可以這樣總結(jié):

          CSS 是阻塞渲染的資源。需要將它盡早、盡快地下載到客戶端,以便縮短首次渲染的時(shí)間。

          事實(shí)上,現(xiàn)在很多團(tuán)隊(duì)都已經(jīng)做到了盡早(將 CSS 放在 head 標(biāo)簽里)和盡快(啟用 CDN 實(shí)現(xiàn)靜態(tài)資源加載速度的優(yōu)化)。這個(gè)“把 CSS 往前放”的動(dòng)作,對很多同學(xué)來說已經(jīng)內(nèi)化為一種編碼習(xí)慣。那么現(xiàn)在我們還應(yīng)該知道,這個(gè)“習(xí)慣”不是空穴來風(fēng),它是由 CSS 的特性決定的。

          JS 的阻塞

          不知道大家注意到?jīng)]有,前面我們說過程的時(shí)候,花了很多筆墨去說 HTML、說 CSS。相比之下,JS 的出鏡率也太低了點(diǎn)。

          這當(dāng)然不是因?yàn)?JS 不重要。而是因?yàn)椋谑状武秩具^程中,JS 并不是一個(gè)非登場不可的角色——沒有 JS,CSSOM 和 DOM 照樣可以組成渲染樹,頁面依然會(huì)呈現(xiàn)——即使它死氣沉沉、毫無交互。

          JS 的作用在于修改,它幫助我們修改網(wǎng)頁的方方面面:內(nèi)容、樣式以及它如何響應(yīng)用戶交互。這“方方面面”的修改,本質(zhì)上都是對 DOM 和 CSSDOM 進(jìn)行修改。因此 JS 的執(zhí)行會(huì)阻止 CSSOM,在我們不作顯式聲明的情況下,它也會(huì)阻塞 DOM。

          我們通過一個(gè)例子來理解一下這個(gè)機(jī)制:

          三個(gè) console 的結(jié)果分別為:

          注:本例僅使用了內(nèi)聯(lián) JS 做測試。感興趣的同學(xué)可以把這部分 JS 當(dāng)做外部文件引入看看效果——它們的表現(xiàn)一致。

          第一次嘗試獲取 id 為 container 的 DOM 失敗,這說明 JS 執(zhí)行時(shí)阻塞了 DOM,后續(xù)的 DOM 無法構(gòu)建;第二次才成功,這說明腳本塊只能找到在它前面構(gòu)建好的元素。這兩者結(jié)合起來,“阻塞 DOM”得到了驗(yàn)證。再看第三個(gè) console,嘗試獲取 CSS 樣式,獲取到的是在 JS 代碼執(zhí)行前的背景色(yellow),而非后續(xù)設(shè)定的新樣式(blue),說明 CSSOM 也被阻塞了。那么在阻塞的背后,到底發(fā)生了什么呢?

          我們前面說過,JS 引擎是獨(dú)立于渲染引擎存在的。我們的 JS 代碼在文檔的何處插入,就在何處執(zhí)行。當(dāng) HTML 解析器遇到一個(gè) script 標(biāo)簽時(shí),它會(huì)暫停渲染過程,將控制權(quán)交給 JS 引擎。JS 引擎對內(nèi)聯(lián)的 JS 代碼會(huì)直接執(zhí)行,對外部 JS 文件還要先獲取到腳本、再進(jìn)行執(zhí)行。等 JS 引擎運(yùn)行完畢,瀏覽器又會(huì)把控制權(quán)還給渲染引擎,繼續(xù) CSSOM 和 DOM 的構(gòu)建。 因此與其說是 JS 把 CSS 和 HTML 阻塞了,不如說是 JS 引擎搶走了渲染引擎的控制權(quán)

          現(xiàn)在理解了阻塞的表現(xiàn)與原理,我們開始思考一個(gè)問題。瀏覽器之所以讓 JS 阻塞其它的活動(dòng),是因?yàn)樗恢?JS 會(huì)做什么改變,擔(dān)心如果不阻止后續(xù)的操作,會(huì)造成混亂。但是我們是寫 JS 的人,我們知道 JS 會(huì)做什么改變。假如我們可以確認(rèn)一個(gè) JS 文件的執(zhí)行時(shí)機(jī)并不一定非要是此時(shí)此刻,我們就可以通過對它使用 defer 和 async 來避免不必要的阻塞,這里我們就引出了外部 JS 的三種加載方式。

          JS的三種加載方式

          • 正常模式:
          <script src="index.js"></script>
          

          這種情況下 JS 會(huì)阻塞瀏覽器,瀏覽器必須等待 index.js 加載和執(zhí)行完畢才能去做其它事情。

          • async 模式:
          <script async src="index.js"></script>
          

          async 模式下,JS 不會(huì)阻塞瀏覽器做任何其它的事情。它的加載是異步的,當(dāng)它加載結(jié)束,JS 腳本會(huì)立即執(zhí)行

          • defer 模式:
          <script defer src="index.js"></script>
          

          defer 模式下,JS 的加載是異步的,執(zhí)行是被推遲的。等整個(gè)文檔解析完成、DOMContentLoaded 事件即將被觸發(fā)時(shí),被標(biāo)記了 defer 的 JS 文件才會(huì)開始依次執(zhí)行。

          從應(yīng)用的角度來說,一般當(dāng)我們的腳本與 DOM 元素和其它腳本之間的依賴關(guān)系不強(qiáng)時(shí),我們會(huì)選用 async;當(dāng)腳本依賴于 DOM 元素和其它腳本的執(zhí)行結(jié)果時(shí),我們會(huì)選用 defer。

          通過審時(shí)度勢地向 script 標(biāo)簽添加 async/defer,我們就可以告訴瀏覽器在等待腳本可用期間不阻止其它的工作,這樣可以顯著提升性能。


          主站蜘蛛池模板: 国产未成女一区二区三区| 亚洲视频一区二区在线观看| 亚洲一区二区三区不卡在线播放| 无码夜色一区二区三区| 狠狠色成人一区二区三区| 国产福利一区二区三区在线观看| 综合久久久久久中文字幕亚洲国产国产综合一区首| 亚洲一区二区三区国产精品| 精品欧美一区二区在线观看| 国产一区二区久久久| 日本一道高清一区二区三区| 无码日韩精品一区二区免费| 海角国精产品一区一区三区糖心| 78成人精品电影在线播放日韩精品电影一区亚洲 | 3d动漫精品啪啪一区二区中| 中文字幕色AV一区二区三区| 国产午夜精品一区二区三区不卡| 激情内射亚洲一区二区三区爱妻| 国产爆乳无码一区二区麻豆| 日本一区二区在线| 香蕉久久一区二区不卡无毒影院| 亚洲色无码一区二区三区| 久久久久人妻一区二区三区| 在线电影一区二区| 中文字幕在线观看一区二区三区| 亚洲熟女少妇一区二区| 亚洲av乱码一区二区三区香蕉 | 亚洲国模精品一区| 亚洲一区精品无码| 无码中文字幕一区二区三区| 免费萌白酱国产一区二区| 国产精品美女一区二区视频| 亚洲一区精品视频在线| 国产AV天堂无码一区二区三区| 久久精品国产第一区二区| 精品国产一区二区三区香蕉| 一区二区视频在线免费观看| 无码人妻精品一区二区三区99性| 日韩精品无码人妻一区二区三区| 无码人妻精品一区二区三区在线| 成人精品一区二区三区电影|