前端優化-Javascript篇(2.異步加載腳本)

  上篇博客說過腳本後置可使頁面更快的加載,但是這樣的優化仍是有限的,若是腳本須要執行一個耗時的操做,就算後置了它仍是會阻塞後續腳本加載和執行而且阻塞整個頁面。下面介紹非阻塞加載腳本技術也就是異步加載。javascript

非阻塞加載腳本

1.defer(關於defer的一篇好文)
  目前全部瀏覽器都支持defer屬性,可是Chrome和Firefox中只有在加載外部腳本時defer纔會生效,行內腳本使用defer是沒有做用的。而IE中不論什麼狀況,defer都有效。
  defer的做用就是阻止腳本在下載完成後馬上執行,它會讓腳本延遲到全部腳本加載執行完成後,在DOMContentLoaded以前執行,通俗的說就是順序加載延遲執行。雖然都是在DOMContentLoaded以前執行,可是在不一樣瀏覽器之間,執行的各類腳本執行的順序仍是不同的。看下面這個例子:html

<html>
  <meta charset="utf-8">
  <head>
    <script type="text/javascript">
      var result = "" ;
      var head = document.getElementsByTagName("head")[0] ;
      //DOMContentLoaded
      if(window.addEventListener){
        document.addEventListener("DOMContentLoaded",function(){
          result += "DOMContentLoaded\n" ;
        }) ;
      }else{
        document.attachEvent("onDOMContentLoaded",function(){
          result += "DOMContentLoaded\n" ;
        }) ;
      }
      window.onload = function(){
          result += "window loaded\n";
          //console.log("window loaded") ;
      } ;
    </script>
    <!--頭部行內延遲腳本-->
    <script type="text/javascript" defer = "defer">
      result += "Head Inline Script defer\n" ;
    </script>
    <!--頭部行內腳本-->
    <script type="text/javascript">
      result += "Head Inline Script\n" ;
    </script>
    <!--頭部外部延遲腳本 External Head Script defer-->
    <script type="text/javascript" src = "external_head_defer.js" defer="defer"></script>
    <!--頭部行內腳本 External Head Script-->
    <script type="text/javascript" src = "external_head.js"></script>
  </head>
  <body>
    <button>SHOW</button>
    <!--Body行內延遲腳本-->  
    <script type="text/javascript" defer = "defer">
      result += "Body Inline Script defer\n" ;
    </script>
    <!--Body行內腳本-->
    <script type="text/javascript">
      result += "Body Inline Script\n" ;
    </script>
    <!--Body外部延遲腳本 External Body Script defer-->
    <script type="text/javascript" defer = "defer" src = "external_body_defer.js"></script>
    <!--Body外部腳本 External Body Script-->
    <script type="text/javascript" src = "external_body.js"></script>
    <script type="text/javascript">
      document.getElementsByTagName("button")[0].onclick = function(){console.log(result);} ;
    </script>
  </body>
</html>

運行結果以下:
Defer
從上面能夠看出幾個問題:
  首先,IE9如下不支持DOMContentLoaded(後面會說明這個狀況)
  其次,驗證了上面說的Chrome和Firefox行內腳本不支持defer屬性
  最後,defer確實達到了延遲執行的目的,沒有阻塞後面腳本的加載和執行。可是耗時的操做仍是會阻塞DOMContentLoaded事件,而大多數狀況下你們都會把頁面初始化的腳本附加在DOMContentLoaded事件上,因此defer方法仍是不能很好解決這個問題。java

2.Script DOM
  這是最經常使用也是如今廣泛的解決方法。它只須要簡單幾句話就能夠實現腳本的異步加載,而且全部瀏覽器都支持這個方法。可是在每一個瀏覽器中,執行仍是略有不一樣。看下面這個例子:segmentfault

<html>
  <meta charset="utf-8">
  <head>
    <script type="text/javascript">
      var result = "\n" ;
      var head = document.getElementsByTagName("head")[0] ;
      //DOMContentLoaded
      if(window.addEventListener){
        document.addEventListener("DOMContentLoaded",function(){
          alert("DOMContentLoaded") ;
          result += "DOMContentLoaded\n" ;
        }) ;
      }else{
        document.attachEvent("onDOMContentLoaded",function(){
          alert("DOMContentLoaded") ;
          result += "DOMContentLoaded\n" ;
        }) ;
      }
      window.onload = function(){
          result += "window loaded\n";
      } ;
    </script>
    <!--頭部外部延遲腳本 External Head Script defer-->
    <script type="text/javascript" src = "external_head_defer.js" defer="defer"></script>
    <!--頭部行內腳本 External Head Script-->
    <script type="text/javascript" src = "external_head.js"></script>
  </head>
  <body>
    <button>SHOW</button>
    <script type="text/javascript">
      document.getElementsByTagName("button")[0].onclick = function(){console.log(result);} ;
    </script>
    <script type="text/javascript">
      result += "start\n" ;
      var head = document.getElementsByTagName("head")[0] ;
      var script8 = document.createElement("script") ;
      script8.type = "text/javascript" ;
      script8.onload = function(){alert("done");} ;
      script8.readystatechange = function(){
          if(script8.readyState == "loaded" || script8.readyState == "complete"){
              alert("done") ;
          }
      } ;
      //Body Dynamic Script
      script8.src = "dynamic_body.js" ;
      head.appendChild(script8) ;
      result += "end\n" ;
    </script>    
  </body>
</html>

運行結果以下:
ScriptDom
  下面這張圖是在ScriptDom腳本後面加入一個耗時的腳本,使得這個腳本執行完成後,保證ScriptDOM的腳本處於可執行狀態:瀏覽器

<script type="text/javascript">
    function doSomething(length){
        var start = new Date().getTime() ;
        while((new Date().getTime() - start) < 1000 * length){}
    }
    doSomething(3) ;
</script>

結果以下:
ScriptDOM
運行結果同時也說明了幾個問題:
  首先,ScriptDOM不會阻塞後續腳本的執行,根據start和end 的位置能夠很容易看出。
  其次,在第二張圖的狀況下,ScriptDOM和defer同時均可以執行,在不一樣瀏覽器中它們的優先級的不同的。在Firfox和Chrome中,ScriptDOM的優先級比defer低,而在IE中狀況則相反。
  最後,經過兩種狀況的對比發現,在Chrome中ScriptDOM不會阻塞DOMContentLoaded事件可是會阻塞onload事件;在Firefox中ScriptDOM既會阻塞DOMContentLoaded事件也會阻塞onload事件;而在IE中,狀況則要根據代碼執行狀況來決定。若是在DOMContentLoaded事件或者onload事件觸發以前,ScriptDOM代碼處於可執行狀態,那麼就會阻塞兩個事件;若是在DOMContentLoaded事件或者onload事件觸發以前,ScriptDOM代碼處於不可執行狀態,那麼就不會阻塞兩個事件。總結的來講就是在Chrome和IE中DOMContentLoaded事件不須要等待ScriptDOM執行,而在Firefox中須要等待ScriptDOM執行。服務器

  經過上面兩種方法的對比發現,defer和ScriptDOM都不會阻塞後續腳本的執行。可是相對來講,ScriptDOM在使用上更加靈活並且並不老是阻塞DOMContentLoaded事件,而且ScriptDOM的使用場景主要是在按需加載和模塊加載器上,而通常使用這些技術的時候,頁面已經處於加載完成的狀態,因此對於性能不會有影響。
  app

DOMContentLoaded

  上面說到DOMContentLoaded事件,DOMcontentLoaded是現代瀏覽器才支持的一個事件,萬惡的IE從IE9開始才支持這個事件。那麼在什麼狀況下才會觸發DOMContentLoaded事件呢?DOMContentLoaded會在瀏覽器接收到服務器傳過來的HTML文檔,整個頁面DOM結構加載完成而且全部行內腳本和外部腳本執行完成後觸發 (經過上面異步腳本的例子能夠看出,ScriptDOM異步加載腳本不會阻塞DOMContentLoaded,或者說DOMContentLoaded不須要等待ScriptDOM執行就能夠出發) ,它跟onload事件的區別是,DOMContentLoaded事件不須要等待圖片,ifram和樣式表等資源加載完成就會觸發,而onload事件須要等待整個頁面都加載完成包括各類資源纔會觸發。因此對於咱們來講DOMContentLoaded是一個更有用的事件,由於只要DOM結構加載完成,咱們就能夠經過Javasscript來操做頁面上的DOM節點。
  可是上面關於DOMContentLoaded事件觸發條件的定義只是官方文檔的說法,具體狀況並不老是這樣。
  有時樣式表的加載會阻塞腳本的執行從而阻塞DOMContentLoaded事件,這種狀況通常出如今樣式表後面跟着腳本。也就是說若是把腳本放在樣式表後面,那麼腳本就必須等到樣式表加載完成才能開始執行,這樣就會阻塞頁面的DOMContentLoaded事件。可是這樣作也是有道理的,由於有時候咱們的腳本會處理DOM樣式方面的東西。
  這種阻塞狀況在不一樣瀏覽器上表現也會不同。在IE和Firefox中,無論樣式表後面跟着是行內腳本仍是外部腳本,都會發生阻塞。在Chrome中,只有外部腳本纔會發生阻塞。
  因爲IE在IE9如下不支持DOMContentLoaded事件,因此咱們須要用一些Hack技術來實現這個功能。分兩種狀況來實現:
  1.網頁不嵌套在iframe中
  在IE中咱們能夠經過一個方式來判斷DOM是否加載完成,就是doScroll方法。若是DOM加載完成,那麼咱們就能夠調用document的doScroll方法,不然就會拋出異常。咱們能夠利用這個特性不斷輪詢來作Hack。異步

function bindReady(handle){
        //判斷是否在iframe中
        try{
            var isFrame = window.frameElement != null ;
        }catch(e){}
        if(document.documentElement.doScroll && !isFrame){
            //輪詢是否能夠調用doScroll方法
            function tryScroll(){
                try{
                    document.documentElement.doScroll("left");
                    handle() ;
                }catch(e){
                    setTimeout(tryScroll,10) ;
                }
            }
            tryScroll() ;
        }
    }

  2.網頁嵌套在iframe中
  若是網頁嵌套在iframe中,那麼是沒法經過doScroll的方法來Hack實現DOMContentLoaded的。咱們能夠經過另一種方式來實現---readystatechange,代碼以下:函數

function bindReady(handle){
        document.onreadystatechange = function(){
            if(document.readyState === "complete" || document.readyState === "loaded"){
                handle() ;
            }
        }
    }

  結合上面的討論,咱們能夠得出一個通用的bindReady方法。性能

//綁定DOMContentLoaded事件,支持綁定多個處理函數
var handleList = [] ;
function onReady(handle){
    //按順序執行處理函數
    var doHandles = function(){
        var length = handleList.length ;
        for(var i = 0 ; i < length ; i ++){
            handleList[i]() ;
        }
    }
    if(handleList.length == 0){
        //在尚未處理函數時,把doHandles註冊到ready上,這樣後面加入的處理函數就能夠一併執行
        bindReady(doHandles) ;
    }
    //把處理函數加入到函數列表中
    handleList.push(handle) ;
}
function bindReady(handle){
    var called = false ;
    var ready = function(){
        //防止重複調用
        if(!called){
            called = true ;
            handle() ;
        }
    }
    if(document.addEventListener){
        //支持DOMcontentLoaded
        document.addEventListener("DOMContentLoaded",ready,false);
    }else if(document.attachEvent){
        //IE
        try{
            var isFrame = window.frameElement != null ;
        }catch(e){}
        //網頁不在iframe中
        if(document.documentElement.doScroll && !isFrame){
            function tryScroll(){
                try{
                    document.documentElement.doScroll("left") ;
                    ready() ;
                }catch(e){
                    setTimeout(tryScroll,10) ;
                }
            }
            tryScroll() ;
        }else{
            //網頁在iframe中
            document.onreadystatechange = function(){
                if(document.readyState === "complete" || document.readyState === "loaded"){
                    ready() ;
                }
            }
        }
    }
    //老式瀏覽器不支持上面兩種事件
    if(window.addEventListener){
        window.addEventListener("load",ready,false) ;
    }else if(window.attachEvent){
        window.attachEvent("onload",ready) ;
    }else{
        //容許綁定多個處理函數
        var fn = window.onload ;
        window.onload = function(){
            fn && fn() ;
            ready() ;
        }
    }
}

說在最後

  說了這麼多,雖然經過腳本後置和異步加載能夠下降腳本加載對頁面的影響,可是就算是實現了異步加載,可是因爲瀏覽器的腳本解析的單線程的,因此腳本執行的時候仍然會阻塞整個頁面(固然除了使用Web Worker),這時候用戶是沒法完成正常交互的,因此要想真正完全的優化頁面加載,還須要從代碼的優化開始。從下一篇開始,我會分享關於這方面的學習。

最後,安利下個人我的博客,歡迎訪問: http://bin-playground.top

相關文章
相關標籤/搜索