defer和async的原理與區別
上一篇剛轉載了一篇有關於網站效能優化的文章,其中提及到了頁面的載入和渲染的過程,提到了defer和async的相關區別,但是本人在此之前並沒有深究其中的區別。
defer和async是script標籤的兩個屬性,用於在不阻塞頁面文件解析的前提下,控制指令碼的下載和執行。
在介紹他們之前,我們有必要先了解一下頁面的載入和渲染過程:
- 列表內容
- 瀏覽器通過HTTP協議請求伺服器,獲取HMTL文件並開始從上到下解析,構建DOM;
- 在構建DOM過程中,如果遇到外聯的樣式宣告和指令碼宣告,則暫停文件解析,建立新的網路連線,並開始下載樣式檔案和指令碼檔案;
- 樣式檔案下載完成後,構建CSSDOM;指令碼檔案下載完成後,解釋並執行,然後繼續解析文件構建DOM
- 完成文件解析後,將DOM和CSSDOM進行關聯和對映,最後將檢視渲染到瀏覽器視窗
在這個過程中,指令碼檔案的下載和執行是與文件解析同步進行,也就是說,它會阻塞文件的解析,如果控制得不好,在使用者體驗上就會造成一定程度的影響。
所以我們需要清楚的瞭解和使用defer和async來控制外部指令碼的執行。
先來簡明概要的介紹一下下面三者的區別:
<script src="script.js"></script>
沒有 defer 或 async,瀏覽器會立即載入並執行指定的指令碼,“立即”指的是在渲染該 script 標籤之下的文件元素之前,也就是說不等待後續載入的文件元素,讀到就載入並執行。<script async src="script.js"></script>
有 async,載入和渲染後續文件元素的過程將和 script.js 的載入與執行並行進行(非同步)<script defer src="myscript.js"></script>
有 defer,載入後續文件元素的過程將和 script.js 的載入並行進行(非同步),但是 script.js 的執行要在所有元素解析完成之後,DOMContentLoaded 事件觸發之前完成。
接著,我們來看一張圖:
藍色線代表網路讀取,紅色線代表執行時間,這倆都是針對指令碼的;綠色線代表 HTML 解析。
此圖告訴我們以下幾個要點:
- defer 和 async 在網路讀取(下載)這塊兒是一樣的,都是非同步的(相較於 HTML 解析)
- 它倆的差別在於指令碼下載完之後何時執行,顯然 defer 是最接近我們對於應用指令碼載入和執行的要求的
- 關於 defer,此圖未盡之處在於它是按照載入順序執行指令碼的,這一點要善加利用
- async 則是一個亂序執行的主,反正對它來說指令碼的載入和執行是緊緊挨著的,所以不管你宣告的順序如何,只要它載入完了就會立刻執行
- 仔細想想,async 對於應用指令碼的用處不大,因為它完全不考慮依賴(哪怕是最低階的順序執行),不過它對於那些可以不依賴任何指令碼或不被任何指令碼依賴的指令碼來說卻是非常合適的,最典型的例子:Google Analytics
為了演示指令碼的執行情況,進而介紹這兩個屬性的作用,我們先來搭建一個簡單的伺服器:
建立了一個簡易的Node伺服器server.js,其程式碼如下:
var http = require('http');
var fs = require('fs');
var typeMapping = {
'html': 'text/html',
'js' : 'text/javascript',
'css' : 'text/css',
'ico' : 'image/x-icon'
};
var getResourceExtension = function(req) {
var url = req.url;
var lastIndexOfDot = url.lastIndexOf('.');
if (lastIndexOfDot === -1) return 'text/plain';
return url.substring(lastIndexOfDot + 1);
};
var respondResourceToClient = function(req, res) {
//read the reource and respond via 'pipe'
fs.createReadStream(req.url.replace(/^\//, '')).pipe(res);
};
var server = http.createServer(function(req, res) {
console.log('requesting url: ', req.url);
var extension = getResourceExtension(req);
res.writeHead(200, {'Content-Type': typeMapping[extension]});
var delay = function(time) {
setTimeout(function() {
respondResourceToClient(req, res);
}, time || 0);
};
if (extension === 'html' || extension === 'css') {
delay(0);
} else if (extension === 'js') {
delay(1000);
} else {
res.end('');
}
});
server.listen(3000);
console.log('listening at port 3000...');
從上面的程式碼我們可以看出,當伺服器接收到請求之後,會判斷請求資源是否為JS,如果是則延遲1s後返回對應的資源。
啟動這個服務很簡單,只需執行node server.js即可,然後就可以在瀏覽器中輸入http://localhost:3000/app/index.html訪問主頁了
現在我們來看看index.html中的內容:
<!DOCTYPE html>
<html>
<head>
<title>defer & async</title>
<link rel="stylesheet" type="text/css" href="css/main.css">
<script type="text/javascript" src="js/1.js"></script>
</head>
<body>
<div class="text">Hello World</div>
<script type="text/javascript" src="js/2.js"></script>
</body>
</html>
在這個HTML文件中,我們先在head中引用了一個外部的指令碼檔案js/1.js,然後在我們要顯示的Hello World後面又引用了一個js/2.js,它們的內容都很簡單,就是彈出對應的標示資訊:
// js/1.js
alert(1);
// js/2.js
alert(2);
下面我們就來訪問主頁,看看會發生些什麼:
從圖中可以看到,渲染的過程的確是自上而下,同步進行的,也就是說遇到外部的指令碼,就得暫停文件的解析,下載並且解釋執行,這種方式是阻塞的,會造成網頁空白的現象。
現在稍微修改一下程式碼,將head中的script標籤加上defer屬性,然後也稍微改動一下兩個JS檔案:
<!DOCTYPE html>
<html>
<head>
<title>defer & async</title>
<link rel="stylesheet" type="text/css" href="css/main.css">
<!-- adding a 'defer' attribute, by default, the value will be 'true' -->
<script type="text/javascript" src="js/1.js" defer></script>
</head>
<body>
<div class="text">Hello World</div>
<script type="text/javascript" src="js/2.js"></script>
</body>
</html>
// js/1.js
console.log(1);
// js/2.js
console.log(2);
再次訪問index.html,我們會在控制檯中看到下面的執行順序:
顯而易見,1.js被延後致至文件解析完成後執行了,它的執行順序比body中的
<!DOCTYPE html>
<html>
<head>
<title>defer & async</title>
<link rel="stylesheet" type="text/css" href="css/main.css">
<!-- adding a 'defer' attribute, by default, the value will be 'true' -->
<script type="text/javascript" src="js/1.js" defer></script>
<script type="text/javascript" src="js/2.js" defer></script>
<script type="text/javascript" defer>
console.log(3);
</script>
</head>
<body>
<div class="text">Hello World</div>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
console.log('dom content loaded, ready state:', this.readyState);
}, false);
window.addEventListener('load', function() {
console.log('window loaded, dom ready state:', document.readyState);
}, false);
</script>
</body>
</html>
可以看到,外聯的指令碼是按照宣告順序執行的,內聯指令碼並沒有遵守這個規則,另外,DOMContentLoaded和load事件依次被捕獲,DOM的狀態分別變為interactive和complete
接下來我們介紹一下async屬性,為了能夠很好的演示執行順序,我們還需要一些修改:
<!DOCTYPE html>
<html>
<head>
<title>defer & async</title>
<link rel="stylesheet" type="text/css" href="css/main.css">
<!-- adding a 'async' attribute, by default, the value is 'true' as well -->
<script type="text/javascript" src="js/1.js" async></script>
<script type="text/javascript" src="js/2.js" async></script>
<script type="text/javascript" src="js/3.js" async></script>
</head>
<body>
<div class="text">Hello World</div>
</body>
</html>
JS檔案內如下:
// js/1.js
console.log(1);
// js/2.js
console.log(2);
// js/3.js
console.log(3);
再次訪問index.html,會發現控制檯列印如下:
我們發現,3個指令碼的執行是沒有順序的,我們也無法預測每個指令碼的下載和執行的時間和順序。async和defer一樣,不會阻塞當前文件的解析,它會非同步地下載指令碼,但和defer不同的是,async會在指令碼下載完成後立即執行,如果專案中指令碼之間存在依賴關係,不推薦使用async。
關於async,也需要注意以下幾點:
1. 只適用於外聯指令碼,這一點和defer一致
2. 如果有多個聲明瞭async的指令碼,其下載和執行也是非同步的,不能確保彼此的先後順序
3. async會在load事件之前執行,但並不能確保與DOMContentLoaded的執行先後順序
以上就是defer和async的介紹,瞭解和掌握這兩個屬性的作用,不僅對JS程式碼執行的控制更加熟練,也會對頁面渲染多一分了解。
參考文章: