JavaScript 之 最佳位置選擇

      Javascript 文件(下面簡稱腳本文件)須要被HTML文件引用才能在瀏覽器中運行。在HTML文件中能夠經過不一樣的方式來引用腳本文件,咱們須要關注的是,這些方式的具體實現和這些方式可能會帶來的性能問題。javascript

      首先,引用腳本必須用到<script>標籤,咱們須要瞭解<script>標籤的特性,引述書中做者原話:css

當瀏覽器遇到(內嵌)<script>標籤時,當前瀏覽器無從獲知Javascript是否會修改頁面內容。所以,這時瀏覽器會中止處理頁面,先執行Javascript代碼,而後再繼續解析和渲染頁面。一樣的狀況也發生在使用 src 屬性加在Javascript的過程當中(即外鏈 Javascript ),瀏覽器必須先花時間下載外鏈文件中的代碼,而後解析並執行它。在這個過程當中,頁面渲染和用戶交互徹底被阻塞了。html

      經過上述描述,可知:每當瀏覽器解析到<script>標籤(不管內嵌仍是外鏈)時,瀏覽器都會優先下載、解析並執行該標籤中的Javascript代碼,而阻塞了其後全部頁面內容的下載和渲染。java

  下面爲四種引用腳本的方式:數組

  1. 慣例的作法:在head標籤內插入<script>標籤

      然而這種常規的作法卻隱藏着嚴重的性能問題。根據上述對<script>標籤特性的描述,咱們知道,在該示例中,當瀏覽器解析到<script>標籤(第4行)時,瀏覽器會中止解析其後的內容,而優先下載腳本文件,並執行其中的代碼,這意味着,其後的「樣式文件」和「<body>標籤」都沒法被加載,因爲<body>標籤沒法被加載,那麼頁面天然就沒法渲染了。所以在該javascript代碼徹底執行完以前,頁面都是一片空白。瀏覽器

  注意:app

  (1)頁面的渲染和javascript代碼的執行是一塊兒顯示出來的。這說明頁面最開始出現的空白正是因爲javascript文件阻塞特性引發的(爲了突出這一現象,可外鏈了幾個較大的js庫)。這是由於若是javascript文件沒有阻塞頁面渲染的話,頁面的渲染通常會先於javascript文件的加載(通常來講頁面所須要的的css樣式文件和html文件的體積會遠遠小於javascript文件,若是沒有被阻塞,它們會先於javascript文件下載好,而後當即被瀏覽器解析出來)。函數

  (2)圖片的加載是在 javascript 執行以後纔開始的,即javascript阻塞了圖片的加載。性能

  2. 經典的作法

      既然<script>標籤會阻塞其後內容的加載,那麼將<script>標籤放到全部頁面內容以後不就能夠避免這種糟糕的情況了嗎?ui

      將全部的<script>標籤儘量地放到<body>標籤底部(body後面),以儘可能避免對頁面其他部分下載的影響。

  此時頁面渲染先於腳本文件的執行,說明腳本文件再也不阻塞頁面渲染了(包括css文件和img等文件的下載) 然而做者在後面又介紹了另外一種方式——動態加載腳本。起初我不太明白,把腳本放到<body>底部就行了,爲何還須要動態腳本?多翻了幾次書才發現原來本身忽略了做者的一段話:

(將腳本放到<body>標籤底部時)儘管腳本下載會阻塞另外一個腳本,可是頁面的大部份內容已經下載完成並顯示給用戶…

      便是說,雖然在IE8+瀏覽器上已經實現了腳本並行下載,但在某些瀏覽器中(即便腳本文件放到了<body>標籤底部),頁面中腳本還是一個接着一個加載的。既是,瀏覽器先加載完file1,再去加載file2,最後才輪到file3。雖然此時腳本已經不影響其餘頁面內容了,但咱們也一樣但願腳本之間實現並行下載(即同時開始下載),因而下面給出動態加載腳本的方法來實現這一想法。

  3. 動態腳本

      經過文檔對象模型(DOM),咱們能夠幾乎能夠頁面任意地方建立<script>標籤:

    <script type="text/javascript">
         var script = document.createElement('script'); 
         script.type='text/javascript'; 
         script.src='file1.js'; 
         document.getElementsByTagName('head')[0].appendChild(script);
    </script>

      上述代碼動態建立了一個外鏈file1的<script>標籤,並將其添加到<head>標籤內。這種技術的重點在於:

不管在什麼時候啓動下載,文件的下載和執行過程不會阻塞頁面其餘進程(包括腳本加載)。

      然而這種方法也是有缺陷的。這種方法加載的腳本會在下載完成後當即執行,那麼意味着多個腳本之間的運行順序是沒法保證的(除了Firefox和Opera)。當某個腳本對另外一個腳本有依賴關係時,就極可能發生錯誤了。

      好比,寫一個jQuery代碼,須要引入jQuery庫,然而你寫的jQuery代碼文件極可能會先完成下載並當即執行,這時瀏覽器會報錯——‘jQuery未定義’之類的,由於此時jQuery庫還未下載完成。因而作出如下改進:

<script type="text/javascript">
    function loadScript(url,callback){ 
        var script = document.createElement('script'); 
        script.type = "text/javascript"; 
        if(script.readyState){
            //IE 
            script.onreadystatechange = function(){ 
            if(script.readyState=="loaded"||script.readyState=="complete"){ 
                script.onreadystatechange=null; 
                callback(); 
                }
            };
        }
        else
        {
            //其餘瀏覽器 
            script.onload=function(){ 
                callback(); };
        } 
        script.src=url;
        document.getElementsByTagName('head')[0].appendChild(script); 
    }
</script>


      上述代碼改進的地方就是增長了一個回調函數,該函數會在相應腳本文件加載完成後被調用。這樣即可以實現順序加載了,寫法以下(假設file2依賴file1,file1和file3相互獨立):

<script type="text/javascript">
    loadScript('file1.js',function(){ 
      loadScript('file2.js',function(){});
    }); 
    loadScript('file3.js',function(){});
 </script>

      file2會在file1加載完後纔開始加載,保證了在file2執行前file1已經準備穩當。而file1和file3是並行下載的,互不影響。 雖然loadScript函數已經足夠好,但仍是有些不盡人意的地方——經過分析這段代碼,咱們知道,loadScript函數中的順序加載是以腳本的阻塞加載來實現的。而咱們真正想實現的是——腳本同步下載並按相應順序執行,即並行加載並順序執行。這樣不會形成頁面堵塞,但會形成另一個問題:這樣加載的Javascript文件,不在原始的DOM結構之中,所以在DOM-ready(DOMContentLoaded)事件和window.onload事件中指定的回調函數對它無效。

  4. LABjs庫

      LABjs庫能幫咱們真正地實現「並行加載與順序執行」:

      舉一個最簡單的例子,來講明這兩個函數庫的基本用法。更高級的用法,請參閱它們的文檔。

    <script src="script1.js"></script>
    <script src="script2-a.js"></script>
    <script src="script2-b.js"></script>
    <script type="text/javascript">
        initScript1();
        initScript2();
    </script>
    <script src="script3.js"></script>
    <script type="text/javascript">
        initScript3();
    </script>

  上面這段代碼,將依次加載4個javascript文件:script1.js、script2-a.js、script2-b.js和script3.js。在加載完前三個文件後,運行兩個函數initScript1()和initScript2();加載完第四個文件後,再運行函數initScript3()。

      下面,用LABjs對其進行改寫:

    <script src="LAB.js"></script>
    <script type="text/javascript">
    $LAB
     .script("script1.js").wait()
     .script("script2-a.js")
     .script("script2-b.js")
     .wait(function(){
       initScript1();
       initScript2();
     })
     .script("script3.js")
     .wait(function(){
       initScript3();
     });
    </script>

  首先,$LAB對象替代了<script>標籤,而後.script()方法表示加載Javascript文件,不帶參數的.wait()方法表示當即運行剛纔加載的Javascript文件,帶參數的.wait()方法也是當即運行剛纔加載的Javascript文件,可是還運行參數中指定的函數。

      這裏須要注意的是,能夠同時運行多條$LAB鏈,可是它們之間是徹底獨立的,不存在次序關係。若是你要確保一個Javascript文件在另外一個文件以後運行,你只能把它們寫在同一個鏈操做之中。只有當某些腳本是徹底無關的時候,你才應該考慮把它們分紅不一樣的$LAB鏈,表示它們之間不存在相關關係。

      接下來是requireJS的改寫:

    <script src="require.js"></script>
    <script type="text/javascript">
    require(["script1.js", "script2-a.js", "script2-b.js","script3.js"],function(){initScript1(); initScript2();initScript3();});
    </script>

  require()接受兩個參數,第一個數組表示所要加載的Javascript文件,第二個是加載完成後所要運行的回調函數。原生的require()不支持按次序加載,因此四個Javascript文件到底先加載哪一個,沒法事前知道,require()只保證這四個文件所有加載完成以後,纔會運行所指定的回調函數。

      若是按次序加載對你很重要,你可使用官方提供的order插件。

相關文章
相關標籤/搜索