先來看這行代碼:
<script src = "allMyClientSideCode.js"></script>
這有點兒……不怎麼樣。“這該放在哪兒?”開發人員會奇怪,“靠上點,放到<head>標簽裡?還是靠下點,放到<body>標簽裡?”這兩種做法都會讓富腳本站點的下場很淒慘。<head>標簽裡的大腳本會滯壓所有頁面渲染工作,使得用戶在腳本加載完畢之前一直處於“白屏死機”狀態。而<body>標簽末尾的大腳本只會讓用戶看到毫無生命力的靜態頁面,原本應該進行客戶端渲染的地方卻散布著不起作用 的控件和空空如也的方框。
完美解決這個問題需要對腳本分而治之:那些負責讓頁面更好看、更好用的腳本應該立即加載,而那些可以待會兒再加載的腳本稍後再加載。但是怎樣才能既滯壓這些腳本,又能保證它們在被調用時的可用性呢?
一、<script>標簽的再認識
現代浏覽器中的<script>標簽分成了兩種新類型:經典型和非阻塞型。接下來討論如何運用這兩種標簽來盡快加載頁面。
1、阻塞型腳本何去何從?
標准版本的<script>標簽常常被稱作阻塞型標簽。這個詞必須放在上下文中進行理解:現代浏覽器看到阻塞型<script>標簽時,會跳過阻塞點繼續讀取文檔及下載其他資源(腳本和樣式表)。但直到腳本下載完畢並運行之後,浏覽器才會評估阻塞點之後的那些資源。因此,如果網頁文檔的<head>標簽裡有5 個阻塞型<script>標簽,則在所有這5 個腳本均下載完畢並運行之前,用戶除了頁面標題之外看不到任何東西。不僅如此,即便這些腳本運行了,它們也只能看到阻塞點之前的那部分文檔。如果想看到<body>標簽中正等待加載的那些好東西,就必須給像document.onreadystatechange 這樣的事件綁定一個事件處理器。
基於上述原因,現在越來越流行把腳本放在頁面<body>標簽的尾部。這樣,一方面用戶可以更快地看到頁面,另一方面腳本也可以主動親密接觸DOM 而無需等待事件來觸發自己。對大多數腳本而言,這次“搬家”是個巨大的進步。
但並非所有腳本都一樣。在向下搬動腳本之前,請先問自己2 個問題。
上述問題只要有一個答案是肯定的,那麼該腳本就應該放在<head>標簽中,否則就可以放在<body>標簽中,文檔形如:
<html> <head> <!--metadata and stylesheets go here --> <script src="headScripts.js"></scripts> </head> <body> <!-- content goes here --> <script src="bodyScripts.js"></script> </body> </html>
這確實大大縮短了加載時間,但要注意一點,這可能讓用戶有機會在加載bodyScripts.js 之前與頁面交互。
2、 腳本的提前加載與延遲運行
上面建議將大多數腳本放在<body>中,因為這樣既能讓用戶更快地看到網頁,又能避免操控DOM之前綁定“就緒”事件的開銷。但這種方式也有一個缺點,即浏覽器在加載完整個文檔之前無法加載這些腳本,這對那些通過慢速連接傳送的大型文檔來說會是一大瓶頸。
理想情況下,腳本的加載應該與文檔的加載同時進行,並且不影響DOM 的渲染。這樣,一旦文檔就緒就可以運行腳本,因為已經按照<script>標簽的次序加載了相應腳本。
如果大家已經讀到這裡了,那麼一定會迫不及待地想寫一個自定義Ajax 腳本加載器以滿足這樣的需求!不過,大多數浏覽器都支持一個更為簡單的解決方案。
<script defer src = "deferredScript.js">
添加defer(延遲)屬性相當於對浏覽器說:“請馬上開始加載這個腳本吧,但是,請等到文檔就緒且所有此前具有defer 屬性的腳本都結束運行之後再運行它。”在文檔<head>標簽裡放入延遲腳本,既能帶來腳本置於<body>標簽時的全部好處,又能讓大文檔的加載速度大幅提升!
不足之處就是,並非所有浏覽器都支持defer屬性。這意味著,如果想確保自己的延遲腳本能在文檔加載後運行,就必須將所有延遲腳本的代碼都封裝在諸如jQuery 之$(document).ready 之類的結構中。
上一節的頁面例子改進如下:
<html> <head> <!-- metadata and stylesheets go here --> <script src="headScripts.js"></scripts> <script defer src="deferredScripts.js"></script> </head> <body> <!-- content goes here --> </body> </html>
請記住deferredScripts 的封裝很重要,這樣即使浏覽器不支持defer,deferredScripts 也會在文檔就緒事件之後才運行。如果頁面主體內容遠遠超過幾千字節,那麼付出這點代價是完全值得的。
3、 腳本的並行加載
如果你是斤斤計較到毫秒級頁面加載時間的完美主義者,那麼defer也許就像是淡而無味的薄鹽醬油。你可不想一直等到此前所有的defer 腳本都運行結束,當然也肯定不想等到文檔就緒之後才運行這些腳本,你就是想盡快加載並且盡快運行這些腳本。這也正是現代浏覽器提供了async(異步)屬性的原因。
<script async src = "speedyGonzales.js"> <script async src = "roadRunner.js">
如果說defer 讓我們想到一種靜靜等待文檔加載的有序排隊場景,那麼async 就會讓我們想到混亂的無政府狀態。前面給出的那兩個腳本會以任意次序運行,而且只要JavaScript 引擎可用就會立即運行,而不論文檔就緒與否。
對大多數腳本來說,async 是一塊難以下咽的雞肋。async 不像defer那樣得到廣泛的支持。同時,由於異步腳本會在任意時刻運行,它實在太容易引起海森堡蟻蟲之災了(腳本剛好結束加載時就會蟻蟲四起)。
當我們加載一些第三方腳本,而且也不在乎它們誰先運行誰後運行。因此,對這些第三方腳本使用async 屬性,相當於一分錢沒花就提升了它們的運行速度。
上一個頁面示例再添加兩個獨立的第三方小部件,得到的結果如下:
<html> <head> <!-- metadata and stylesheets go here --> <script src="headScripts.js"></scripts> <script src="deferredScripts.js" defer></script> </head> <body> <!-- content goes here --> <script async defer src="feedbackWidget.js"></script> <script async defer src="chatWidget.js"></script> </body> </html>
這個頁面結構清晰展示了腳本的優先次序。對於絕大多數浏覽器,DOM的渲染只會延遲至headScripts.js 結束運行時。進行DOM渲染的同時會在後台加載deferredScripts.js。接著,在DOM 渲染結束時將運行deferredScripts.js 和那兩個小部件腳本。這兩個小部件腳本在那些支持async 的浏覽器中會做無序運行。如果不確定這是否妥當,請勿使用async!
二、可編程的腳本加載
雖然<script>標簽簡單得令人心動,但有些情況確實需要更精致的腳本加載方式。我們可能只想給那些滿足一定條件的用戶加載某個腳本,譬如白金會員或達到一定級別的玩家,也可能只想當用戶單擊激活時才加載某個特性,譬如聊天小部件。
1、直接加載腳本
我們可以用類似下面這樣的代碼來插入<script>標簽。
var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.src = '/js/feature.js'; head.appendChild(script);
稍等,我們如何才能知道腳本何時加載結束呢?我們可以給腳本本身添加一些代碼以觸發事件,但如果要為每個待加載腳本都添加這樣的代碼,那也太鬧心了。或者是另外一種情況,即我們不可能給第三方服務器上的腳本添加這樣的代碼。HTML5 規范定義了一個可以綁定回調的onload 屬性。
script.onload = function() { // 現在可以調用腳本裡定義的函數了 };
不過, IE8 及更老的版本並不支持onload , 它們支持的是onreadystatechange。某些浏覽器在插入<script>標簽時還會出現一些“靈異事件”。而且,這裡甚至還沒談到錯誤處理呢!為了避免
所有這些令人頭疼的問題,在此強烈建議使用腳本加載庫。
三、yepnope的條件加載
yepnope是一個簡單的、輕量級的腳本加載庫(壓縮後的精簡版只有1.7KB),其設計目標就是真誠服務於最常見的動態腳本加載需求。
yepnope 最簡單的用法是,加載腳本並對腳本完成運行這一事件返回一個回調。
yepnope({ load: 'oompaLoompas.js', callback: function() { console.log('oompa-Loompas ready!'); } });
還是無動於衷?下面我們要用yepnope 來並行加載多個腳本並按給定次序運行它們。舉個例子,假設我們想加載Backbone.js,而這個腳本又依賴於Underscore.js。為此,我們只需用數組形式提供這兩個腳本的位置作為加載參數。
yepnope({ load: ['underscore.js', 'backbone.js'], complete: function() { // 這裡是Backbone 的業務邏輯 } });
請注意,這裡使用了complete(完成)而不是callback(回調)。
其差別在於,腳本加載列表中的每個資源均會運行callback,而只有當所有腳本都加載完成後才會運行complete。yepnope 的標志性特征是條件加載。給定test 參數,yepnope 會根據該參數值是否為真而加載不同的資源。舉個例子,可以以一定的准確度判斷用戶是否在用觸摸屏設備,從而據此相應地加載不同的樣式表及腳本。
yepnope({ test: Modernizr.touch, yep: ['touchStyles.css', 'touchApplication.js'], nope: ['mouseStyles.css', 'mouseApplication.js'], complete: function() { // 不管是哪一種情況,應用程序均已就緒! } });
我們只用寥寥幾行代碼就搭好了舞台,可以基於用戶的接入設備而給他們完全不同的使用體驗。當然,不是所有的條件加載都需要備齊yep(是)和nope(否)這兩種測試結果。yepnope 最常見的用法之一就是加載墊片腳本以彌補老式浏覽器缺失的功能。
yepnope({ test: window.json,nope: ['json2.js'], complete: function() { // 現在可以放心地用JSON 了 } });
頁面使用了yepnope 之後應該變成下面這種漂亮的標記結構:
<html> <head> <!-- metadata and stylesheets go here --> <script src="headScripts.js"></scripts> <script src="deferredScripts.js" defer></script> </head> <body> <!-- content goes here --> </body> </html>
很眼熟?這個結構和討論defer 屬性那一節給出的結構一樣,唯一的區別是這裡的某個腳本文件已經拼接了yepnope.js(很可能就在deferredScripts.js 的頂部),這樣就可以獨立地加載那些根據條件再加載的腳本(因為浏覽器需要墊片腳本)和那些想要動態加載的腳本(以便回應用戶的動作)。結果將是一個更小巧的deferredScripts.js。
四、Require.js/AMD 模塊化加載
開發人員想通過腳本加載器讓混亂不堪的富腳本應用變得更規整有序一些,而Require.js 就是這樣一種選擇。Require.js 這個強大的工具包能夠自動和AMD技術一起捋順哪怕最復雜的腳本依賴圖。
現在先來看一個用到Require.js 同名函數的簡單腳本加載示例。
require(['moment'], function(moment) { console.log(moment().format('dddd')); // 星期幾 });
require 函數接受一個由模塊名稱構成的數組,然後並行地加載所有這些腳本模塊。與yepnope 不同,Require.js 不會保證按順序運行目標腳本,只是保證它們的運行次序能滿足各自的依賴性要求,但前提是
這些腳本的定義遵守了AMD(Asynchronous Module Definition,異步模塊定義)規范。
案例一: 加載 JavaScript 文件
<script src="./js/require.js"></script> <script> require(["./js/a.js", "./js/b.js"], function() { myFunctionA(); myFunctionB(); }); </script>
如案例一 所示,有兩個 JavaScript 文件 a.js 和 b.js,裡面各自定義了 myFunctionA 和 myFunctionB 兩個方法,通過下面這個方式可以用 RequireJS 來加載這兩個文件,在 function 部分的代碼可以引用這兩個文件裡的方法。
require 方法裡的這個字符串數組參數可以允許不同的值,當字符串是以”.js”結尾,或者以”/”開頭,或者就是一個 URL 時,RequireJS 會認為用戶是在直接加載一個 JavaScript 文件,否則,當字符串是類似”my/module”的時候,它會認為這是一個模塊,並且會以用戶配置的 baseUrl 和 paths 來加載相應的模塊所在的 JavaScript 文件。配置的部分會在稍後詳細介紹。
這裡要指出的是,RequireJS 默認情況下並沒有保證 myFunctionA 和 myFunctionB 一定是在頁面加載完成以後執行的,在有需要保證頁面加載以後執行腳本時,RequireJS 提供了一個獨立的 domReady 模塊,需要去 RequireJS 官方網站下載這個模塊,它並沒有包含在 RequireJS 中。有了 domReady 模塊,案例一 的代碼稍做修改加上對 domReady 的依賴就可以了。
案例二: 頁面加載後執行 JavaScript
<script src="./js/require.js"></script> <script> require(["domReady!", "./js/a.js", "./js/b.js"], function() { myFunctionA(); myFunctionB(); }); </script>
執行案例二的代碼後,通過 Firebug 可以看到 RequireJS 會在當前的頁面上插入為 a.js 和 b.js 分別聲明了一個 < script> 標簽,用於異步方式下載 JavaScript 文件。async 屬性目前絕大部分浏覽器已經支持,它表明了這個 < script> 標簽中的 js 文件不會阻塞其他頁面內容的下載。
案例三:RequireJS 插入的 < script>
<script type="text/javascript" charset="utf-8" async="" data-requirecontext="_" data-requiremodule="js/a.js" src="js/a.js"></script>
AMD推行一個由Require.js 負責提供的名叫define 的全局函數,該函數有3 個參數:
使用 RequireJS 來定義 JavaScript 模塊
這裡的 JavaScript 模塊與傳統的 JavaScript 代碼不一樣的地方在於它無須訪問全局的變量。模塊化的設計使得 JavaScript 代碼在需要訪問”全局變量”的時候,都可以通過依賴關系,把這些”全局變量”作為參數傳遞到模塊的實現體裡,在實現中就避免了訪問或者聲明全局的變量或者函數,有效的避免大量而且復雜的命名空間管理。
如同 CommonJS 的 AMD 規范所述,定義 JavaScript 模塊是通過 define 方法來實現的。
下面我們先來看一個簡單的例子,這個例子通過定義一個 student 模塊和一個 class 模塊,在主程序中實現創建 student 對象並將 student 對象放到 class 中去。
案例四: student 模塊,student.js
define(function(){ return { createStudent: function(name, gender){ return { name: name, gender: gender }; } }; });
案例五:class 模塊,class.js
define(function() { var allStudents = []; return { classID: "001", department: "computer", addToClass: function(student) { allStudents.push(student); }, getClassSize: function() { return allStudents.length; } }; } );
案例六: 主程序
require(["js/student", "js/class"], function(student, clz) { clz.addToClass(student.createStudent("Jack", "male")); clz.addToClass(student.createStudent("Rose", "female")); console.log(clz.getClassSize()); // 輸出 2 });
student 模塊和 class 模塊都是獨立的模塊,下面我們再定義一個新的模塊,這個模塊依賴 student 和 class 模塊,這樣主程序部分的邏輯也可以包裝進去了。
案例七:依賴 student 和 class 模塊的 manager 模塊,manager.js
define(["js/student", "js/class"], function(student, clz){ return { addNewStudent: function(name, gender){ clz.addToClass(student.createStudent(name, gender)); }, getMyClassSize: function(){ return clz.getClassSize(); } }; });
案例八:新的主程序
require(["js/manager"], function(manager) { manager.addNewStudent("Jack", "male"); manager.addNewStudent("Rose", "female"); console.log(manager.getMyClassSize());// 輸出 2 });
通過上面的代碼示例,我們已經清楚的了解了如何寫一個模塊,這個模塊如何被使用,模塊間的依賴關系如何定義。
其實要想讓自己的站點更快捷,可以異步加載那些暫時用不到的腳本。為此最簡單的做法是審慎地使用defer 屬性和async 屬性。如果要求根據條件來加載腳本,請考慮像yepnope 這樣的腳本加載器。如果站點存在大量相互依賴的腳本,請考慮Require.js。選擇最適合任務的工具,然後使用它,享受它帶來的便捷。
以上就是關於javascript的異步腳本加載的全部內容,想對大家的學習有所幫助。