1. 程式人生 > >解析JavaScript非同步載入

解析JavaScript非同步載入

本篇文章給大家分享了作者在學習JavaScript非同步載入中遇到的問題,總結後給出了詳細的處理方案,寫的十分的全面細緻,具有一定的參考價值,對此有需要的朋友可以參考學習下。如有不足之處,歡迎批評指正。

同步載入的問題

預設的js是同步載入的,這裡的“載入”可以理解成是解析、執行,而不是“下載”,在最新版本的瀏覽器中,瀏覽器對於程式碼請求的資源都是瀑布式的載入,而不是阻塞式的,但是js的執行總是阻塞的。這會引起什麼問題呢?如果我的index頁面要載入一些js,但是其中的某個請求遲遲得不到響應,於是阻塞了後面的js程式碼的執行(同步載入),同時頁面渲染也不能繼續(如果js引入是在head標籤後)。

<script type="text/javascript" src='http://china-addthis.googlecode.com/svn/trunk/addthis.js'></script>

<script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>

this is a test

比如上面的這段程式碼,儲存為index.html檔案,頁面的主體是一個簡單的字串,但是程式碼執行後頁面遲遲都是空白,為何?因為請求的js遲遲無法載入(可能由於谷歌被牆等原因),於是阻塞了後面的程式碼的執行,頁面得不到渲染。可能你會提議,把js程式碼放到前不就能先渲染頁面了!好方法,我們嘗試著將js放後面:

this is a test
<script type="text/javascript" src='http://china-addthis.googlecode.com/svn/trunk/addthis.js'></script>
<script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>

頁面瞬間被渲染,“this is a test"也很快出現在前臺,世界似乎平靜了,可是:

this is a test
<script type="text/javascript" src='http://china-addthis.googlecode.com/svn/trunk/addthis.js'></script>
<script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>
<script type="text/javascript">
 console.log('hello world');
</script>//歡迎加入前端全棧開發交流圈一起學習交流:864305860

在前面程式碼的基礎上簡單加了一段程式碼,但是"hello world"遲遲無法在控制檯輸出,顯然前面的js請求阻塞了後面程式碼的載入,我們恍然大悟,改變js的載入位置只能改變頁面的渲染,然而對於js的載入並沒有什麼卵用,js還是會阻塞。

實現js非同步載入

我們的要求似乎很簡單,能在頁面載入的同時,在控制檯輸出字串即可,再講的通俗一點,就是在請求第一段谷歌提供的js的同時,繼續執行下面的js,也就是實現js的非同步載入。
最常見的做法是動態生成script標籤:

<body>
 this is a test
 <script type="text/javascript">
  ~function() {
   var s = document.createElement('script');
   s.src = 'http://china-addthis.googlecode.com/svn/trunk/addthis.js';
   document.body.appendChild(s);
  }();//歡迎加入前端全棧開發交流圈一起學習交流:864305860
 </script>
 <script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>
 <script type="text/javascript">
  console.log('hello world');
 </script>
</body>

但是還是有點問題,這種載入方式在載入執行完之前會阻止 onload 事件的觸發,而現在很多頁面的程式碼都在 onload 時還要執行額外的渲染工作等,所以還是會阻塞部分頁面的初始化處理:

<body>
 this is a test
 <script type="text/javascript">
  ~function() {
   // function async_load() {
    var s = document.createElement('script');
    s.src = 'http://china-addthis.googlecode.com/svn/trunk/addthis.js';
    document.body.appendChild(s);
   // }//歡迎加入前端全棧開發交流圈一起學習交流:864305860
   // window.addEventListener('load', async_load, false);
  }();
 
  window.onload = function() {
   var txt = document.createTextNode(' hello world');
   document.body.appendChild(txt);
  };//歡迎加入前端全棧開發交流圈一起學習交流:864305860
 </script>
 <script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>
</body>

比如上面的程式碼不能很好地渲染”hello world”,我們只需將註釋去掉就可以了,讓谷歌提供的js在onload 時才開始非同步載入。這樣就解決了阻塞 onload 事件觸發的問題。

補充DOMContentLoaded 與 OnLoad 事件 DOMContentLoaded : 頁面(document)已經解析完成,頁面中的dom元素已經可用。但是頁面中引用的圖片、subframe可能還沒有載入完。 OnLoad:頁面的所有資源都載入完畢(包括圖片)。瀏覽器的載入進度在這時才停止。這兩個時間點將頁面載入的timeline分成了三個階段。
以上似乎能較好解決這個問題,但是html5提供了更簡便的方法,async屬性!

this is a test
<script type="text/javascript" src='http://china-addthis.googlecode.com/svn/trunk/addthis.js' async='async'></script>
<script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>
<script type="text/javascript">
 console.log('hello world');
</script>//歡迎加入前端全棧開發交流圈一起學習交流:864305860

async是html5的新屬性,async 屬性規定一旦指令碼可用,則會非同步執行(一旦下載完畢就會立刻執行)。
需要注意的是async 屬性僅適用於外部指令碼(只有在使用 src 屬性時)
defer屬性常常和async一起提起:

this is a test
<script type="text/javascript" src='http://china-addthis.googlecode.com/svn/trunk/addthis.js' defer='defer'></script>
<script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'></script>
<script type="text/javascript">
 console.log('hello world');
</script>//歡迎加入前端全棧開發交流圈一起學習交流:864305860

似乎實現效果差不多,但是真的一樣嗎?我們來看看defer屬性的定義。
以前的defer只支援ie的hack,現在html5的出現開始全面支援defer。defer 屬性規定當頁面已完成載入後,才會執行指令碼。defer 屬性僅適用於外部指令碼(只有在使用 src 屬性時)。ps:ie支援的defer似乎並非如此,因為對ie無感,不深究,有興趣的可以去查閱相關資料。

既然async和defer經常一起出現,那麼辨析一下吧!
如果沒有async和defer屬性(賦值為true,下同),那麼瀏覽器會立即執行當前的js指令碼,阻塞後面的指令碼;如果有async屬性,載入和渲染後續文件元素的過程將和當前js的載入與執行並行進行(非同步);如果有defer屬性,那麼載入後續文件元素的過程將和 script.js 的載入並行進行(非同步),但是 script.js 的執行要在所有元素(DOM)解析完成之後,DOMContentLoaded 事件觸發之前完成。

藍色線代表網路讀取,紅色線代表執行時間,這倆都是針對指令碼的;綠色線代表 HTML 解析。
此圖告訴我們以下幾個要點(摘自defer和async的區別):

  • defer 和 async 在網路讀取(下載)這塊兒是一樣的,都是非同步的(相較於 HTML 解析)
  • 它倆的差別在於指令碼下載完之後何時執行,顯然 defer 是最接近我們對於應用指令碼載入和執行的要求的
  • 關於 defer,此圖未盡之處在於它是按照載入順序執行指令碼的,這一點要善加利用
  • async 則是一個亂序執行的主,反正對它來說指令碼的載入和執行是緊緊挨著的,所以不管你宣告的順序如何,只要它載入完了就會立刻執行
  • 仔細想想,async 對於應用指令碼的用處不大,因為它完全不考慮依賴(哪怕是最低階的順序執行),不過它對於那些可以不依賴任何指令碼或不被任何指令碼依賴的指令碼來說卻是非常合適的,最典型的例子:Google Analytics

但是在我看來(以下個人理解,如有出入還望指出),defer在非同步載入上的應用並不會比async廣。async的英文解釋是非同步,該屬性作用在指令碼上,使得指令碼載入(下載)完後隨即開始執行,和動態插入script標籤作用類似(async只支援h5,後者能相容瀏覽器);而defer的英文解釋是延遲,作用也和字面解釋類似,延遲指令碼的執行,使得dom元素載入完後才開始有序執行指令碼,因為有序,所以會帶來另一個問題:

this is a test
<script type="text/javascript" src='http://china-addthis.googlecode.com/svn/trunk/addthis.js' defer='defer'></script>//歡迎加入前端全棧開發交流圈一起學習交流:864305860
<script type="text/javascript" src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js' defer='defer'></script>
<script type="text/javascript" src='index.js' defer='defer'></script>
console.log('hello world');

如果執行這段程式碼,控制檯的“hello world”也會遲遲得不到結果。所以我覺得還是async好用,如果要考慮依賴的話,可以選擇requirejs、seajs等模組載入器。

JavaScript的非同步載入還有一些方式,比如:AJAX eval(使用AJAX得到指令碼內容,然後通過eval(xmlhttp.responseText)來執行指令碼)、iframe方式等。

結語

感謝您的觀看,如有不足之處,歡迎批評指正。

為了幫助大家讓學習變得輕鬆、高效,給大家免費分享一大批資料,幫助大家在成為全棧工程師,乃至架構師的路上披荊斬棘。在這裡給大家推薦一個前端全棧學習交流圈:864305860 歡迎大家進群交流討論,學習交流,共同進步。

當真正開始學習的時候難免不知道從哪入手,導致效率低下影響繼續學習的信心。

但最重要的是不知道哪些技術需要重點掌握,學習時頻繁踩坑,最終浪費大量時間,所以有有效資源還是很有必要的。

最後祝福所有遇到瓶疾且不知道怎麼辦的前端程式設計師們,祝福大家在往後的工作與面試中一切順利。