HTML中的script標籤研究

楊軍軍發表於2015-05-30

Script 的堵塞(block)特性

Scripts without async or defer attributes, as well as inline scripts, are fetched and executed immediately, before the browser continues to parse the page. - MDN

the blocking nature of JavaScript, which is to say that nothing else can happen while JavaScript code is being executed. In fact, most browsers use a single process for both user interface (UI) updates and JavaScript execution, so only one can happen at any given moment in time. The longer JavaScript takes to execute, the longer it takes before the browser is free to respond to user input. - Nicholas C. Zakas「High Performance JavaScript 」

上面引用兩段話的意思大致是,當瀏覽器解析DOM文件時,一旦遇到 script 標籤(沒有defer 和 async 屬性)就會立即下載並執行,與此同時瀏覽器對文件的解析將會停止,直到 script 程式碼執行完成。出現這種堵塞行為一方面是因為瀏覽器的UI渲染,互動行為等都是單執行緒操作,另一方是因為 script 裡面的程式碼可能會影響到後面文件的解析,比如下面的程式碼:

html<script type="text/javascript">
  document.write("The date is " + (new Date()).toDateString());
</script>

這個堵塞特性會嚴重的影響使用者體驗,下面是幾種優化方案:

  • 儘量把指令碼往文件的後面放,以減少對文件的堵塞,最好放在 </body> 前面。
  • 儘量把指令碼按照它們的依賴關係放在一個檔案中

不過更好的方法是下面的非堵塞載入指令碼(Nonblocking Scripts)的方法:

1. Deferred Script (延遲指令碼)

Script 有一個 defer 屬性,擁有這個屬性的script表明這個script不會修改DOM,因此這段指令碼會在文件樹全部解析完成後觸發( to be executed after the document has been parsed). 但這個屬性並不被所有的瀏覽器支援。

2. Dynamic Script Elements (動態指令碼)

原理就是使用指令碼建立 script 元素,設定 src 需為要動態新增指令碼的 URL,再把這個 script 新增到DOM中。有時我們需要動態指令碼載入完成後再執行某些操作,這就需要我們在指令碼載入完成後新增一個回撥函式,這個可以通過 script 的 onload 事件實現。下面的實現程式碼:

jsfunction loadJS(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);
}

有時我們需要我們動態載入的指令碼按照我們載入的順序執行,但上面的實現並不能保證這一點,載入的指令碼在下載完成後就會立即執行,而不會按照我們定義的順序。要解決這個問題也不難,可以參照下面的程式碼:

jsloadJS('a.js', function(){
  loadJS('b.js', function(){
    loadJS('c.js', function(){
      app.init();
    })
  })
})

當有大量的指令碼需要動態新增時,這樣寫也會遇到問題;另外的解決方案是利用一些現成的庫,比如 LABjs

3. XMLHttpRequest Script Injection (XHR動態插入)

原理是利用XMLHttpReques(XHR)物件,動態獲取一段JS程式碼,然後插入文件。
相對其他方法來說的一個優點是可以“懶執行”,也就是JS程式碼已經先下載好了並沒有執行,可以在需要的來執行(?)(之前的動態指令碼在下載後會立即執行)。實現程式碼:

jsfunction xhrLoadJS (url, callback){
  var xhr = new XMLHttpRequest();
  xhr.open('get', url, true);
  xhr.onreadystatechange = function(){
    if(xhr.readyState == 4){
      if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){
        var script = document.createElement('script');
        script.type = 'text/script';
        script.text = xhr.responseText;
        eval(xhr.responseText);  // 執行程式碼
        document.body.appendChild(script);
        callback();
      }
    }
  }
  xhr.send(null);
}

缺點是不能跨域請求

參考

  1. Javascript 裝載和執行
  2. MDN Script元素
  3. Nicholas C. Zakas 所著的「High Performance JavaScript 」的第一章 "Loading and Execution"

相關文章