跟我學(xué)習(xí)javascript的異步腳本加載
先來(lái)看這行代碼:
<script src = "allMyClientSideCode.js"></script>
這有點(diǎn)兒……不怎么樣。“這該放在哪兒?”開發(fā)人員會(huì)奇怪,“靠上點(diǎn),放到<head>標(biāo)簽里?還是靠下點(diǎn),放到<body>標(biāo)簽里?”這兩種做法都會(huì)讓富腳本站點(diǎn)的下場(chǎng)很凄慘。<head>標(biāo)簽里的大腳本會(huì)滯壓所有頁(yè)面渲染工作,使得用戶在腳本加載完畢之前一直處于“白屏死機(jī)”狀態(tài)。而<body>標(biāo)簽?zāi)┪驳拇竽_本只會(huì)讓用戶看到毫無(wú)生命力的靜態(tài)頁(yè)面,原本應(yīng)該進(jìn)行客戶端渲染的地方卻散布著不起作用 的控件和空空如也的方框。
完美解決這個(gè)問題需要對(duì)腳本分而治之:那些負(fù)責(zé)讓頁(yè)面更好看、更好用的腳本應(yīng)該立即加載,而那些可以待會(huì)兒再加載的腳本稍后再加載。但是怎樣才能既滯壓這些腳本,又能保證它們?cè)诒徽{(diào)用時(shí)的可用性呢?
一、<script>標(biāo)簽的再認(rèn)識(shí)
現(xiàn)代瀏覽器中的<script>標(biāo)簽分成了兩種新類型:經(jīng)典型和非阻塞型。接下來(lái)討論如何運(yùn)用這兩種標(biāo)簽來(lái)盡快加載頁(yè)面。
1、阻塞型腳本何去何從?
標(biāo)準(zhǔn)版本的<script>標(biāo)簽常常被稱作阻塞型標(biāo)簽。這個(gè)詞必須放在上下文中進(jìn)行理解:現(xiàn)代瀏覽器看到阻塞型<script>標(biāo)簽時(shí),會(huì)跳過(guò)阻塞點(diǎn)繼續(xù)讀取文檔及下載其他資源(腳本和樣式表)。但直到腳本下載完畢并運(yùn)行之后,瀏覽器才會(huì)評(píng)估阻塞點(diǎn)之后的那些資源。因此,如果網(wǎng)頁(yè)文檔的<head>標(biāo)簽里有5 個(gè)阻塞型<script>標(biāo)簽,則在所有這5 個(gè)腳本均下載完畢并運(yùn)行之前,用戶除了頁(yè)面標(biāo)題之外看不到任何東西。不僅如此,即便這些腳本運(yùn)行了,它們也只能看到阻塞點(diǎn)之前的那部分文檔。如果想看到<body>標(biāo)簽中正等待加載的那些好東西,就必須給像document.onreadystatechange 這樣的事件綁定一個(gè)事件處理器。
基于上述原因,現(xiàn)在越來(lái)越流行把腳本放在頁(yè)面<body>標(biāo)簽的尾部。這樣,一方面用戶可以更快地看到頁(yè)面,另一方面腳本也可以主動(dòng)親密接觸DOM 而無(wú)需等待事件來(lái)觸發(fā)自己。對(duì)大多數(shù)腳本而言,這次“搬家”是個(gè)巨大的進(jìn)步。
但并非所有腳本都一樣。在向下搬動(dòng)腳本之前,請(qǐng)先問自己2 個(gè)問題。
- 該腳本是否有可能被<body>標(biāo)簽里的內(nèi)聯(lián)JavaScript 直接調(diào)用?答案可能一目了然,但仍值得核查一遍。
- 該腳本是否會(huì)影響已渲染頁(yè)面的外觀?Typekit 宿主字體就是一個(gè)例子。如果把Typekit 腳本放在文檔末尾,那么頁(yè)面文本就會(huì)渲染兩次,即讀取文檔時(shí)即刻渲染,腳本運(yùn)行時(shí)再次渲染。
上述問題只要有一個(gè)答案是肯定的,那么該腳本就應(yīng)該放在<head>標(biāo)簽中,否則就可以放在<body>標(biāo)簽中,文檔形如:
<html> <head> <!--metadata and stylesheets go here --> <script src="headScripts.js"></scripts> </head> <body> <!-- content goes here --> <script src="bodyScripts.js"></script> </body> </html>
這確實(shí)大大縮短了加載時(shí)間,但要注意一點(diǎn),這可能讓用戶有機(jī)會(huì)在加載bodyScripts.js 之前與頁(yè)面交互。
2、 腳本的提前加載與延遲運(yùn)行
上面建議將大多數(shù)腳本放在<body>中,因?yàn)檫@樣既能讓用戶更快地看到網(wǎng)頁(yè),又能避免操控DOM之前綁定“就緒”事件的開銷。但這種方式也有一個(gè)缺點(diǎn),即瀏覽器在加載完整個(gè)文檔之前無(wú)法加載這些腳本,這對(duì)那些通過(guò)慢速連接傳送的大型文檔來(lái)說(shuō)會(huì)是一大瓶頸。
理想情況下,腳本的加載應(yīng)該與文檔的加載同時(shí)進(jìn)行,并且不影響DOM 的渲染。這樣,一旦文檔就緒就可以運(yùn)行腳本,因?yàn)橐呀?jīng)按照<script>標(biāo)簽的次序加載了相應(yīng)腳本。
如果大家已經(jīng)讀到這里了,那么一定會(huì)迫不及待地想寫一個(gè)自定義Ajax 腳本加載器以滿足這樣的需求!不過(guò),大多數(shù)瀏覽器都支持一個(gè)更為簡(jiǎn)單的解決方案。
<script defer src = "deferredScript.js">
添加defer(延遲)屬性相當(dāng)于對(duì)瀏覽器說(shuō):“請(qǐng)馬上開始加載這個(gè)腳本吧,但是,請(qǐng)等到文檔就緒且所有此前具有defer 屬性的腳本都結(jié)束運(yùn)行之后再運(yùn)行它?!痹谖臋n<head>標(biāo)簽里放入延遲腳本,既能帶來(lái)腳本置于<body>標(biāo)簽時(shí)的全部好處,又能讓大文檔的加載速度大幅提升!
不足之處就是,并非所有瀏覽器都支持defer屬性。這意味著,如果想確保自己的延遲腳本能在文檔加載后運(yùn)行,就必須將所有延遲腳本的代碼都封裝在諸如jQuery 之$(document).ready 之類的結(jié)構(gòu)中。
上一節(jié)的頁(yè)面例子改進(jìn)如下:
<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>
請(qǐng)記住deferredScripts 的封裝很重要,這樣即使瀏覽器不支持defer,deferredScripts 也會(huì)在文檔就緒事件之后才運(yùn)行。如果頁(yè)面主體內(nèi)容遠(yuǎn)遠(yuǎn)超過(guò)幾千字節(jié),那么付出這點(diǎn)代價(jià)是完全值得的。
3、 腳本的并行加載
如果你是斤斤計(jì)較到毫秒級(jí)頁(yè)面加載時(shí)間的完美主義者,那么defer也許就像是淡而無(wú)味的薄鹽醬油。你可不想一直等到此前所有的defer 腳本都運(yùn)行結(jié)束,當(dāng)然也肯定不想等到文檔就緒之后才運(yùn)行這些腳本,你就是想盡快加載并且盡快運(yùn)行這些腳本。這也正是現(xiàn)代瀏覽器提供了async(異步)屬性的原因。
<script async src = "speedyGonzales.js"> <script async src = "roadRunner.js">
如果說(shuō)defer 讓我們想到一種靜靜等待文檔加載的有序排隊(duì)場(chǎng)景,那么async 就會(huì)讓我們想到混亂的無(wú)政府狀態(tài)。前面給出的那兩個(gè)腳本會(huì)以任意次序運(yùn)行,而且只要JavaScript 引擎可用就會(huì)立即運(yùn)行,而不論文檔就緒與否。
對(duì)大多數(shù)腳本來(lái)說(shuō),async 是一塊難以下咽的雞肋。async 不像defer那樣得到廣泛的支持。同時(shí),由于異步腳本會(huì)在任意時(shí)刻運(yùn)行,它實(shí)在太容易引起海森堡蟻蟲之災(zāi)了(腳本剛好結(jié)束加載時(shí)就會(huì)蟻蟲四起)。
當(dāng)我們加載一些第三方腳本,而且也不在乎它們誰(shuí)先運(yùn)行誰(shuí)后運(yùn)行。因此,對(duì)這些第三方腳本使用async 屬性,相當(dāng)于一分錢沒花就提升了它們的運(yùn)行速度。
上一個(gè)頁(yè)面示例再添加兩個(gè)獨(dú)立的第三方小部件,得到的結(jié)果如下:
<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>
這個(gè)頁(yè)面結(jié)構(gòu)清晰展示了腳本的優(yōu)先次序。對(duì)于絕大多數(shù)瀏覽器,DOM的渲染只會(huì)延遲至headScripts.js 結(jié)束運(yùn)行時(shí)。進(jìn)行DOM渲染的同時(shí)會(huì)在后臺(tái)加載deferredScripts.js。接著,在DOM 渲染結(jié)束時(shí)將運(yùn)行deferredScripts.js 和那兩個(gè)小部件腳本。這兩個(gè)小部件腳本在那些支持async 的瀏覽器中會(huì)做無(wú)序運(yùn)行。如果不確定這是否妥當(dāng),請(qǐng)勿使用async!
二、可編程的腳本加載
雖然<script>標(biāo)簽簡(jiǎn)單得令人心動(dòng),但有些情況確實(shí)需要更精致的腳本加載方式。我們可能只想給那些滿足一定條件的用戶加載某個(gè)腳本,譬如白金會(huì)員或達(dá)到一定級(jí)別的玩家,也可能只想當(dāng)用戶單擊激活時(shí)才加載某個(gè)特性,譬如聊天小部件。
1、直接加載腳本
我們可以用類似下面這樣的代碼來(lái)插入<script>標(biāo)簽。
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.src = '/js/feature.js';
head.appendChild(script);
稍等,我們?nèi)绾尾拍苤滥_本何時(shí)加載結(jié)束呢?我們可以給腳本本身添加一些代碼以觸發(fā)事件,但如果要為每個(gè)待加載腳本都添加這樣的代碼,那也太鬧心了?;蛘呤橇硗庖环N情況,即我們不可能給第三方服務(wù)器上的腳本添加這樣的代碼。HTML5 規(guī)范定義了一個(gè)可以綁定回調(diào)的onload 屬性。
script.onload = function() {
// 現(xiàn)在可以調(diào)用腳本里定義的函數(shù)了
};
不過(guò), IE8 及更老的版本并不支持onload , 它們支持的是onreadystatechange。某些瀏覽器在插入<script>標(biāo)簽時(shí)還會(huì)出現(xiàn)一些“靈異事件”。而且,這里甚至還沒談到錯(cuò)誤處理呢!為了避免
所有這些令人頭疼的問題,在此強(qiáng)烈建議使用腳本加載庫(kù)。
三、yepnope的條件加載
yepnope是一個(gè)簡(jiǎn)單的、輕量級(jí)的腳本加載庫(kù)(壓縮后的精簡(jiǎn)版只有1.7KB),其設(shè)計(jì)目標(biāo)就是真誠(chéng)服務(wù)于最常見的動(dòng)態(tài)腳本加載需求。
yepnope 最簡(jiǎn)單的用法是,加載腳本并對(duì)腳本完成運(yùn)行這一事件返回一個(gè)回調(diào)。
yepnope({
load: 'oompaLoompas.js',
callback: function() {
console.log('oompa-Loompas ready!');
}
});
還是無(wú)動(dòng)于衷?下面我們要用yepnope 來(lái)并行加載多個(gè)腳本并按給定次序運(yùn)行它們。舉個(gè)例子,假設(shè)我們想加載Backbone.js,而這個(gè)腳本又依賴于Underscore.js。為此,我們只需用數(shù)組形式提供這兩個(gè)腳本的位置作為加載參數(shù)。
yepnope({
load: ['underscore.js', 'backbone.js'],
complete: function() {
// 這里是Backbone 的業(yè)務(wù)邏輯
}
});
請(qǐng)注意,這里使用了complete(完成)而不是callback(回調(diào))。
其差別在于,腳本加載列表中的每個(gè)資源均會(huì)運(yùn)行callback,而只有當(dāng)所有腳本都加載完成后才會(huì)運(yùn)行complete。yepnope 的標(biāo)志性特征是條件加載。給定test 參數(shù),yepnope 會(huì)根據(jù)該參數(shù)值是否為真而加載不同的資源。舉個(gè)例子,可以以一定的準(zhǔn)確度判斷用戶是否在用觸摸屏設(shè)備,從而據(jù)此相應(yīng)地加載不同的樣式表及腳本。
yepnope({
test: Modernizr.touch,
yep: ['touchStyles.css', 'touchApplication.js'],
nope: ['mouseStyles.css', 'mouseApplication.js'],
complete: function() {
// 不管是哪一種情況,應(yīng)用程序均已就緒!
}
});
我們只用寥寥幾行代碼就搭好了舞臺(tái),可以基于用戶的接入設(shè)備而給他們完全不同的使用體驗(yàn)。當(dāng)然,不是所有的條件加載都需要備齊yep(是)和nope(否)這兩種測(cè)試結(jié)果。yepnope 最常見的用法之一就是加載墊片腳本以彌補(bǔ)老式瀏覽器缺失的功能。
yepnope({
test: window.json,nope: ['json2.js'],
complete: function() {
// 現(xiàn)在可以放心地用JSON 了
}
});
頁(yè)面使用了yepnope 之后應(yīng)該變成下面這種漂亮的標(biāo)記結(jié)構(gòu):
<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>
很眼熟?這個(gè)結(jié)構(gòu)和討論defer 屬性那一節(jié)給出的結(jié)構(gòu)一樣,唯一的區(qū)別是這里的某個(gè)腳本文件已經(jīng)拼接了yepnope.js(很可能就在deferredScripts.js 的頂部),這樣就可以獨(dú)立地加載那些根據(jù)條件再加載的腳本(因?yàn)闉g覽器需要墊片腳本)和那些想要?jiǎng)討B(tài)加載的腳本(以便回應(yīng)用戶的動(dòng)作)。結(jié)果將是一個(gè)更小巧的deferredScripts.js。
四、Require.js/AMD 模塊化加載
開發(fā)人員想通過(guò)腳本加載器讓混亂不堪的富腳本應(yīng)用變得更規(guī)整有序一些,而Require.js 就是這樣一種選擇。Require.js 這個(gè)強(qiáng)大的工具包能夠自動(dòng)和AMD技術(shù)一起捋順哪怕最復(fù)雜的腳本依賴圖。
現(xiàn)在先來(lái)看一個(gè)用到Require.js 同名函數(shù)的簡(jiǎn)單腳本加載示例。
require(['moment'], function(moment) {
console.log(moment().format('dddd')); // 星期幾
});
require 函數(shù)接受一個(gè)由模塊名稱構(gòu)成的數(shù)組,然后并行地加載所有這些腳本模塊。與yepnope 不同,Require.js 不會(huì)保證按順序運(yùn)行目標(biāo)腳本,只是保證它們的運(yùn)行次序能滿足各自的依賴性要求,但前提是
這些腳本的定義遵守了AMD(Asynchronous Module Definition,異步模塊定義)規(guī)范。
案例一: 加載 JavaScript 文件
<script src="./js/require.js"></script>
<script>
require(["./js/a.js", "./js/b.js"], function() {
myFunctionA();
myFunctionB();
});
</script>
如案例一 所示,有兩個(gè) JavaScript 文件 a.js 和 b.js,里面各自定義了 myFunctionA 和 myFunctionB 兩個(gè)方法,通過(guò)下面這個(gè)方式可以用 RequireJS 來(lái)加載這兩個(gè)文件,在 function 部分的代碼可以引用這兩個(gè)文件里的方法。
require 方法里的這個(gè)字符串?dāng)?shù)組參數(shù)可以允許不同的值,當(dāng)字符串是以”.js”結(jié)尾,或者以”/”開頭,或者就是一個(gè) URL 時(shí),RequireJS 會(huì)認(rèn)為用戶是在直接加載一個(gè) JavaScript 文件,否則,當(dāng)字符串是類似”my/module”的時(shí)候,它會(huì)認(rèn)為這是一個(gè)模塊,并且會(huì)以用戶配置的 baseUrl 和 paths 來(lái)加載相應(yīng)的模塊所在的 JavaScript 文件。配置的部分會(huì)在稍后詳細(xì)介紹。
這里要指出的是,RequireJS 默認(rèn)情況下并沒有保證 myFunctionA 和 myFunctionB 一定是在頁(yè)面加載完成以后執(zhí)行的,在有需要保證頁(yè)面加載以后執(zhí)行腳本時(shí),RequireJS 提供了一個(gè)獨(dú)立的 domReady 模塊,需要去 RequireJS 官方網(wǎng)站下載這個(gè)模塊,它并沒有包含在 RequireJS 中。有了 domReady 模塊,案例一 的代碼稍做修改加上對(duì) domReady 的依賴就可以了。
案例二: 頁(yè)面加載后執(zhí)行 JavaScript
<script src="./js/require.js"></script>
<script>
require(["domReady!", "./js/a.js", "./js/b.js"], function() {
myFunctionA();
myFunctionB();
});
</script>
執(zhí)行案例二的代碼后,通過(guò) Firebug 可以看到 RequireJS 會(huì)在當(dāng)前的頁(yè)面上插入為 a.js 和 b.js 分別聲明了一個(gè) < script> 標(biāo)簽,用于異步方式下載 JavaScript 文件。async 屬性目前絕大部分瀏覽器已經(jīng)支持,它表明了這個(gè) < script> 標(biāo)簽中的 js 文件不會(huì)阻塞其他頁(yè)面內(nèi)容的下載。
案例三:RequireJS 插入的 < script>
<script type="text/javascript" charset="utf-8" async="" data-requirecontext="_" data-requiremodule="js/a.js" src="js/a.js"></script>
AMD推行一個(gè)由Require.js 負(fù)責(zé)提供的名叫define 的全局函數(shù),該函數(shù)有3 個(gè)參數(shù):
- 模塊名稱,
- 模塊依賴性列表,
- 在那些依賴性模塊加載結(jié)束時(shí)觸發(fā)的回調(diào)。
使用 RequireJS 來(lái)定義 JavaScript 模塊
這里的 JavaScript 模塊與傳統(tǒng)的 JavaScript 代碼不一樣的地方在于它無(wú)須訪問全局的變量。模塊化的設(shè)計(jì)使得 JavaScript 代碼在需要訪問”全局變量”的時(shí)候,都可以通過(guò)依賴關(guān)系,把這些”全局變量”作為參數(shù)傳遞到模塊的實(shí)現(xiàn)體里,在實(shí)現(xiàn)中就避免了訪問或者聲明全局的變量或者函數(shù),有效的避免大量而且復(fù)雜的命名空間管理。
如同 CommonJS 的 AMD 規(guī)范所述,定義 JavaScript 模塊是通過(guò) define 方法來(lái)實(shí)現(xiàn)的。
下面我們先來(lái)看一個(gè)簡(jiǎn)單的例子,這個(gè)例子通過(guò)定義一個(gè) student 模塊和一個(gè) class 模塊,在主程序中實(shí)現(xiàn)創(chuàng)建 student 對(duì)象并將 student 對(duì)象放到 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 模塊都是獨(dú)立的模塊,下面我們?cè)俣x一個(gè)新的模塊,這個(gè)模塊依賴 student 和 class 模塊,這樣主程序部分的邏輯也可以包裝進(jìn)去了。
案例七:依賴 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
});
通過(guò)上面的代碼示例,我們已經(jīng)清楚的了解了如何寫一個(gè)模塊,這個(gè)模塊如何被使用,模塊間的依賴關(guān)系如何定義。
其實(shí)要想讓自己的站點(diǎn)更快捷,可以異步加載那些暫時(shí)用不到的腳本。為此最簡(jiǎn)單的做法是審慎地使用defer 屬性和async 屬性。如果要求根據(jù)條件來(lái)加載腳本,請(qǐng)考慮像yepnope 這樣的腳本加載器。如果站點(diǎn)存在大量相互依賴的腳本,請(qǐng)考慮Require.js。選擇最適合任務(wù)的工具,然后使用它,享受它帶來(lái)的便捷。
以上就是關(guān)于javascript的異步腳本加載的全部?jī)?nèi)容,想對(duì)大家的學(xué)習(xí)有所幫助。
相關(guān)文章
JS利用?clip-path?實(shí)現(xiàn)動(dòng)態(tài)區(qū)域裁剪功能
這篇文章主要介紹了JS利用?clip-path?實(shí)現(xiàn)動(dòng)態(tài)區(qū)域裁剪功能,文中主要通過(guò)使用 box-shadow 實(shí)現(xiàn),代碼簡(jiǎn)單易懂,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12
JS利用cookies設(shè)置每隔24小時(shí)彈出框
這篇文章主要介紹了利用cookies設(shè)置每隔24小時(shí)彈出框的實(shí)例代碼,需要的朋友可以參考下2017-04-04
javascript計(jì)算星座屬相(十二生肖屬相)示例代碼
本文介紹了使用javascript計(jì)算星座和屬相的示例,這個(gè)可以用在用戶注冊(cè)的時(shí)候顯示出來(lái),大家參考使用吧2014-01-01
javascript中導(dǎo)出與導(dǎo)入實(shí)現(xiàn)模塊化管理教程
這篇文章主要給大家介紹了關(guān)于javascript中導(dǎo)出與導(dǎo)入實(shí)現(xiàn)模塊化管理的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
css3元素簡(jiǎn)單的閃爍效果實(shí)現(xiàn)(html5 jquery)
本篇文章主要介紹了css3元素簡(jiǎn)單的閃爍效果實(shí)現(xiàn)(html5 jquery) 需要的朋友可以過(guò)來(lái)參考下,希望對(duì)大家有所幫助2013-12-12
js寫一個(gè)彈出層并鎖屏效果實(shí)現(xiàn)代碼
js實(shí)現(xiàn)一個(gè)彈出層并鎖屏效果是每一網(wǎng)友所期望的效果,于是搜集整理一番,把代碼曬出來(lái)和大家分享2012-12-12

