defer、async屬性以及JS異步加載並執行解決方案

優化腳本文件的加載提高頁面的加載速度,一直是前端工程師提高頁面加載速度很重要的一條。因爲涉及到各個瀏覽器對解析腳本文件的不同機制,以及加載腳本會阻塞其他資源和文件的加載。當瀏覽器解析器遇到<script>時,會立即加載(加載:下載,解析和執行),瀏覽器對其他資源和文檔的加載會停止。爲了提高頁面的加載速度,得讓JS不阻塞其他資源的加載。

Webkit 和 Firefox 對JS的執行過程進行了優化,增加了“預解析”這個過程,“預解析”過程不會修改DOM樹,所以可以跟其他解析過程並行,該過程由預解析器去完成,而可能會改變DOM樹執行過程則由主解析器來完成,在通過解析過程瞭解JavaScript文章中有提到的JS的“預解析”過程,此過程應該就是由瀏覽器的預解析器完成,預解析器還負責解析樣式表和圖片。

另一方面,瀏覽器同事請求http的數量也是有一定限制的,加載js不像加載樣式那樣是並行的。樣式表是構建呈現樹的一部分,瀏覽器在解析頁面結構是由DOM樹和呈現樹兩部分組成,而解析執行樣式表只會改變樣式表不會更改DOM樹,呈現樹跟DOM樹雖然是相對應的,但並非一一對應。因此,也就沒有必要停止對其他資源和文檔的加載了。

提高頁面加載速度的最簡單快速的方法就是將腳本文件放到body底部。但這並不是提高頁面加載速度最優方案的方案,接下來我們介紹其他方案。

首先來介紹一下<script>時能讓腳本延遲和異步執行的兩個屬性:defer和async。

Defer、Async屬性

  • defer是html4.0中定義的,該屬性使得瀏覽器能延遲腳本的執行,等文檔完成解析完成後會按照他們在文檔出現順序再去下載解析。也就是說defer屬性的<script>就類似於將<script>放在body的效果。
  • async是HTML5新增的屬性,IE10和瀏覽器都是支持該屬性的。該屬性的作用是讓腳本能異步加載,也就是說當瀏覽器遇到async屬性的<script>時瀏覽器加載css一樣是異步加載的。

支持async屬性的瀏覽器貌似沒什麼問題,但是defer屬性在各個瀏覽器中支持程度有點不同。測試代碼如下

<script type="text/javascript" defer>
    alert('defer')
</script>
<script type="text/javascript">
    alert('script')
</script>
<script type="text/javascript">
    window.onload = function(){
        alert('onload')
    }
</script>
        
defer測試代碼,可將代碼複製到本地自己測試,外部腳本src引入,內聯腳本直接粘帖

運行以上代碼,得出以下結論:

  • 外部JS在各個瀏覽器裏運行結果跟定義的執行順序正常,alert信息會按照 script->defer->onload順序彈出;
  • 內聯腳本,如果腳本都是IE9/8/7/6按照定義的順序彈出信息,其他瀏覽器則按照 defer->script->onload 順序彈出信息,表示defer失效。
  • 而如果有多個內聯defer腳本、在body和head都有分佈或者在iframe中也有內聯defer腳本,則在IE6中表現一致。

如果想給腳本增加defer屬性讓其延遲加載的話,最好是外部腳本,內聯的defer不僅多數瀏覽器不支持,而且IE6的表現也不一致。

所以將腳本放在body底部比給腳本增加defer屬性讓腳本延遲加載更好,就像yslow建議的那樣:put style top,put script bottom。

瀏覽器的在遇到defer和async屬性的<script>的瀏覽器執行過程如下(以下摘自javascript權威指南):

  1. WEB瀏覽器創建Document對象,並且開始解析WEB頁面,解析HTML元素和它們的文本內容後添加Element對象和Text節點到文檔中。這個過程的readystate的屬性值是“loading”
  2. 當HTML解析器遇到沒有async和defer屬性的<script>時,它把這些元素添加到文檔中,然後執行行內或外部腳本。這些腳本會同步執行,並且在腳本下載(如果需要)和執行解析器會暫停。這樣腳本就可以用document.write()來把文本插入到輸入流中。解析器恢復時這些文本會成爲文檔的一部分。同步腳本經常單定義函數和註冊後面使用的註冊事件處理程序,但它們可以遍歷和操作文檔樹,因爲在它們執行時已經存在了。這樣同步腳本可以看到他自己的<script>元素和它們之前的文檔內容
  3. 當解析器遇到了設置async屬性的<script>元素時,它開始下載腳本,並繼續解析文檔。腳本會在它下載完成後儘快執行,但是解析器沒有停下來等他下載。異步腳本禁止document.write()方法。它們可以看到自己的<script>元素和它之前的所有文檔元素,並且可能或乾脆不可能訪問其他的文檔內容。
  4. 當文檔完成解析,document.readyState屬性變成“interactive”。
  5. 所有有defer屬性的腳本,會被它們在文檔的裏的出現順序執行。異步腳本可能也會在這個時間執行。延遲腳本能訪問完整的文檔樹,禁止使用document.write()方法。
  6. 瀏覽器在Document對象上觸發DOMContentLoaded事件。這標誌着程序執行從同步腳本執行階段轉到異步事件驅動階段。但要注意,這時可能還有異步腳本沒有執行完成。
  7. 這時,文檔已經完全解析完成,但是瀏覽器可能還在等待其他內容載入,如圖片。當所有這些內容完成載入時,並且所有異步腳本完成載入和執行,document.readyState屬性變爲“complete”,WEB瀏覽器出發Window對象上的load事件。
  8. 從此刻起,會調用異步事件,以異步響應用戶輸入事件,網絡事件,計算器過期等。

JS異步加載

瞭解瀏覽器在遇到async、defer屬性的腳本執行順序,我們可以利用這兩個屬性來改善JS的阻塞問題,使用這兩個屬性會有幾種可能的情況:

  • defer爲true:延遲加載腳本,在文檔完成解析完成開始執行,並且在DOMContentLoaded事件之前執行完成。
  • async爲true:異步加載腳本,下載完畢後再執行,在window的load事件之前執行完成

利用這兩個屬性異步加載js,還得了解它們的毛病:

  • 使用defer屬性,最好是外部的script
  • 使用defer、async的腳本禁止使用document.write()方法
  • 當腳本嘗試訪問的樣式屬性可能尚未加載的樣式表時,瀏覽器會禁止該腳本等待樣式表加載完成,這等於樣式表阻塞了腳本的執行。所以使用defer、async的腳本最好不要請求樣式信息時。

不管是使用defer還是async屬性,都需要首先將頁面中的js文件進行整理,各個腳本文件之間的依賴性,哪些文件可以延遲加載等等,做好js代碼的合併和拆分,然後再根據頁面需要合理的使用這兩個屬性。defer屬性聲明這個腳本中將不會有 document.write 或 dom 修改。

當所有腳本解析完成後,JavaScript進入第二個階段,這個階段的是異步的,並且由事件驅動的。在事件驅動階段,WEB瀏覽器調用事件處理程序函數,來響應異步發生的事件。調用事件處理函數通常是用戶輸入,網絡活動,運行和JavaScript中的錯誤來觸發。

通過註冊事件處理程序函數來處理程序,註冊的事件在發生時異步調用這些函數,setTimeout()和setInterval()也都是異步的。所以頁面內容中有內聯script放在setTimeout()執行是異步JS的一種方法,當然將代碼程序放在DOMReady內執行也是異步加載的方法。兩者都將代碼執行階段放在了事件驅動階段。

在dom中創建的script標籤在瀏覽器中則是異步,如下:

function delay_js(src){
    var objScript = document.createElement('script');
    objScript.setAttribute('src', src);
    objScript.setAttribute('type', 'text/javascript');
    document.body.appendChild(objScript);
    return objScript;
}
        
異步加載JS

以上代碼異步加載的JS下載是跟其他一樣是並行的,但是執行階段還是會阻止頁面渲染,延長了window.onload的事件。怎麼樣才能下載和執行JS都不阻塞頁面的渲染呢,如下:

function loadjs(src, succ) {
    var elem = delay_js(src);
    if ((navigator.userAgent.indexOf('MSIE') == -1) ? false: true) {
        elem.onreadystatechange = function() {
            if (/loaded|complete/.test(this.readyState)){
                succ()
            }
        };
    }else{
        elem.onload = function(){
            succ();
        }
    }
    elem.onerror = function() {};
}
        
JS異步下載+執行方案

代碼分析:

  • 非IE瀏覽器能捕捉到script的onload事件,而IE捕捉不到 script.onload 事件,所以只能藉助script.onreadystatechange.
  • 檢測onreadystatechange狀態中,IE7/8最後一個狀態就只是loaded,而IE6中最後一個狀態可能 complete 也可能是loaded,所以用正則loaded|complete兩個狀態都檢測。

異步加載JS的問題是無法使用 document.write 輸出文檔內容,因爲根本無法確定 document.write 應該輸出到什麼位置,但還是可以在DOMReady之後執行操作dom

異步加載的其他方法

除了DOMContentLoaded 與 OnLoad 事件、async屬性以及defer屬性script能解決JS異步加載外,還有其他方法可以異步加載JS:

  • 通過ajax獲取js內容,然後eval執行。

    var xhrObj = getXHRObject();
     xhrObj.onreadystatechange =
       function() {
         if ( xhrObj.readyState != 4 ) return;
         eval(xhrObj.responseText);
       };
     xhrObj.open('GET', 'A.js', true);
     xhrObj.send('');
                        
  • 通過創建iframe:創建並插入iframe元素。
    var iframe = document.createElement('iframe');
    document.body.appendChild(iframe);
    var doc = iframe.contentWindow.document;
    doc.open().write('<body οnlοad="insertJS()">');
    doc.close();
                        

    此方法存在跨域問題,如果父頁面域名修改,則通過javascript協議執行同樣域名升級語句。

  • 頁內 js 的內容被註釋不會執行,但是在需要的時候去掉註釋,eval執行js,
  • 在頁面中document.write Script Tag
  • 用 setTimeout 延遲0秒 與 其它方法組合

還有之前說的事件驅動階段執行以及增加defer或者async屬性都是異步加載JS的方法,用哪一種方法就得我們按照需求各自取捨了。但是不管採用何種方法都不能保證執行順序,都需要將所有js內容按模塊化的方式來切分,哪些模塊存在依賴關係,哪些模塊可以異步,JS怎麼切分得斟酌斟酌再斟酌

發佈了38 篇原創文章 · 獲贊 65 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章