在梳理知識點的時候,發現做爲瀏覽器渲染中的機制之一——異步加載機制,當用戶訪問站點,須要下載各類資源,例如JS腳本,CSS,圖片,iframe等,它是實現現代網站進行加載頁面時一種必不可少的手段。查資料加上老師拓展課程均對於異步加載機制還有不少方法能夠說,故抽出來單獨進行一個知識點的梳理。javascript
瞭解js腳本異步加載前,咱們有必要先了解一下瀏覽器在頁面樣式和js的做用下出現的兩種頁面常見場景:白屏和fouc(無樣式內容閃爍)。css
-白屏:特指一種場景,打開頁面是一片白色,忽然頁面出現,樣式正確。那麼一片白色的時間,則稱之爲白屏。 -FOUC (Flash of UnstyledContent):無樣式內容閃爍,網速狀況差,打開頁面時仍有樣式,以後樣式時有時無,甚至一開始並沒有出現樣式,忽然樣式恢復。(常出如今firefox瀏覽器)html
此類現象,在不一樣瀏覽器進行的資源加載和頁面渲染時,所採用的不一樣的處理方式,並非bug。前端
在樣式文件index.html中java
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>fouc & 白屏</title>
<!--在下面模擬一個延時裝置-->
<link rel="stylesheet" href="b.css?t=10"> //設置這個工具,當請求該文件時,服務器會延遲請求10s再去加載這個資源,以此能夠模擬一個網速特別慢的狀況
<link rel="stylesheet" href="a.css?t=3">
</head>
<body>
<p>hello</p>
<p>飢人谷</p>
<!-- <script src="A.js?t=5"></script>
-->
<img src="https://user-gold-cdn.xitu.io/2018/8/15/1653c442f35af77c?w=211&h=200&f=png&s=8004" alt="">
<!-- <link rel="stylesheet" href="c.css?t=6"> -->
<!-- <script src="http://a.jrg.com:8080/B.js?t=4" ></script>
<script src="http://b.jrg.com:8080/A.js?t=8" ></script> -->
</body>
</html>
複製代碼
(1)關於白屏,須要注意的是,瀏覽器對於樣式和js的處理,即CSS 和 JS 放置順序。推薦:將樣式放在<head>
裏面,將JS放在<body>
內部下方。面試
如上面代碼所示,html頁面裏引入了兩個css:a.css
和b.css
。b.css
引用了c.ss
(@import"./c.css?t=5";
)b.css
中加入了一個10s的延時文件(<link rel="stylesheet"href="b.css?t=10">
),加載這個10s的css樣式文件,瀏覽器是如何完成加載工做,有兩種方式:ajax
第1種: html解析完成,此時10s延時的css文件先無論,先展現<body>
裏所展現的內容,等css文件全加載後再去計算樣式,再去從新渲染一次chrome
第2種: 即便html的dom樹已經解析、渲染都完成,對未加載完成的樣式都必須等待,即css樣式要所有加載、獲取,img資源加載完成,此時底部JS馬上執行,才一次性展現出頁面。例子中展現這種方法,即爲白屏好久的緣由。segmentfault
(2)不一樣瀏覽器的不一樣處理機制所出現的場景不一樣promise
A、白屏場景(常出如今chrome): 打開一個國外網站,使用國外服務器,嵌在css的字體使用的是谷歌字體,運行特別慢,等了很久忽然出現頁面樣式效果。這是由於頁面須要等待css樣式加載全部完成,甚至出現404加載失敗,最後才展現出頁面。那麼那段加載時間,等待了幾秒左右的白色一片的頁面,就是白屏
B、Fouc場景(常出如今Firefox): 一開始的時候,先讓你看見樣式,如字的小號樣式,樣式加載完後看到所規定字號的大字。對用戶來講,一樣的樣式,忽然從小變大,則這個場景就是Fouc(無樣式內容閃爍)。
總結: 無論是css樣式,仍是js文件,只要加長延時,都會形成白屏
(3)CSS 和 JS 最佳放置順序
(3.1)場景:假設JS文件頁面頂部:
場景說明: 引入一個JS文件在頂部,設置一個延時時間。
加載順序: css—js—img—所有獲取到展示頁面效果
此時,img和css加載時會併發加載,即如一個域名下同時加載兩個文件(併發是有限度的),加載在頂部的js時,會禁用併發img和css,並阻止其餘內容下載和渲染。
js並不影響css加載,可是會影響css樣式的一個計算。當js加載時,css已經獲取到(不過此時頁面仍是一片空白),直到js獲取當即執行後,圖片馬上出現,頁面才展現效果。因此js文件放入頁面頂部<head>
裏,也會致使白屏現象出現
(3.2)JS加載特色總結
A、優先加載js文件,加載後js馬上去執行,展現頁面(CSS樣式則是所有加載完,而後一次性展現出頁面)
注: css放前面,優先加載;若放後面,其餘資源則會阻礙css加載,那麼時機就太晚。
B、因爲渲染線程和js腳本線程是互斥的,白屏是渲染進程被阻塞的緣由,當碰到script標籤的時候,會先執行js腳本,而後再渲染。
C、JS腳本操做頁面上的html+css元素,(放頂部時)JS先執行,元素都未加載到(即不存在),未出如今文檔流中【加載,這裏指資源加載和資源是否出如今文檔流中】,因此也不能操做相應JS功能,此時後臺將會報錯。
D、(放頂部時)其餘JS若做爲一種框架語言,則能提早造成一個初步的框架有效構成頁面結構。
即一個放在的js文件,以下: <script src="script.js"></script>
本來放在頂部的這個js文件,會提早加載,如何使它在頂部仍然稍後加載呢?
async
和defer
(1)做用: 沒有 defer
或async
,瀏覽器會當即加載並執行指定的腳本,「當即」指的是在渲染該<script>
標籤之下的文檔元素以前,也就是說不等待後續載入的文檔元素,讀到就加載並執行。也就是說,使用defer
或async
後可以改變這種加載、執行的時機。
常應用在引用了廣告和統計的頁面中,不會影響、堵塞,更不會影響到到頁面其餘元素
(2)async
HTML5裏爲script
標籤裏新增了async屬性,用於異步加載腳本: 不保證順序(獨立的個體)
<script async src="script.js"></script>
/*或*/
<script type="text/javascript" src="alert.js" async="async"></script>
複製代碼
瀏覽器解析到HTML裏的該行script標籤,發現指定爲async
,會異步下載解析執行腳本(即加載後續文檔元素的過程將和script.js的加載並行進行)。
頁面的DOM結構裏假設<script>
在img以前,若是你的瀏覽器支持async
的話,就會異步加載腳本。此時DOM裏已經有img了,因此腳本里能順利取到img的src並彈框。
(3)defer
<script>
標籤裏能夠設置defer
,表示延遲加載腳本:腳本先不執行,延遲到文檔解析和顯示後執行,有順序
<script defer src="script.js"></script>
/*或*/
<script type="text/javascript" src="alert.js" defer="defer"></script>
複製代碼
瀏覽器解析到HTML裏該行<script>
標籤,發現指定爲defer
,會暫緩下載解析執行腳本,等到頁面文檔解析並加載執行完畢後,纔會加載該腳本(更精確地說,是在DOM樹構建完成後,在DOMContentLoaded
事件觸發前,加載defer
的腳本)。
頁面的DOM結構裏假設script在img圖片以前,若是你的瀏覽器支持defer的話,就會延遲到頁面加載完後才下載腳本。此時DOM裏已經有img元素了,因此腳本里能順利取到img的src並彈框。
總結: JS實質採用一種能夠更自由地選擇加載時機和任何位置,讓處於頂部的js文件可以像在底部時,在頁面必要元素加載完成時進行「異步」加載。
注意,異步經常伴隨回調一塊兒出現,可是異步不是回調,回調也不必定是異步。
// 同步的 sleep
function sleep(seconds){
var start = new Date()
while(new Date() - start < seconds * 1000){
}
return
}
console.log(1)
sleep(3) //3秒內要不斷重複作一些無心義的工做才能保證js運行按順序
console.log('wake up')
console.log(2)
//執行結果的順序是:打印1——停3s——醒來——打印2,但事實上js環境內,停3s不可能不作事情
複製代碼
同步的 sleep
//異步的 sleep
function sleep(seconds, fn){
setTimeout(fn, seconds * 1000)
}
console.log(1)
sleep(3, ()=> console.log('wake up'))
console.log(2)
複製代碼
畫一張同步&異步工做的示意圖:
能夠看出,用了異步以後,JS 的空閒時間多了許多。可是注意,在 JS 空閒的這段時間,其實是瀏覽器中的計時器在工做(頗有多是每過一段時間檢查是否時間到了,具體要看 Chrome 代碼)
document.getElementsByTagNames('img')[0].width // 寬度爲 0
console.log('done')
複製代碼
剛開始是直接獲取寬度
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<img src="https://user-gold-cdn.xitu.io/2018/8/17/16546d713fd568f0?w=1200&h=799&f=jpeg&s=121670" alt="">
</body>
</html>
var w = document.getElementsByTagNames('img')[0].width
console.log(w)
複製代碼
先畫一個示意圖:
由此可知,js在img網絡請求還沒執行完的時候緊隨執行,可知爲異步//先獲取網絡請求前img信息,爲空對象
var img = document.getElementsByTagName('img')[0]
複製代碼
img等待網絡請求完成後,獲取完整圖片信息後,便會觸發一個onload事件:
//等待完成以後執行的內容:img若是加載成功,就會觸發一個onload的事件,獲取它的寬度並打印出寬度
img.onload = function(){
var w =img.width
console.log(w)
}
複製代碼
✨完整代碼:
var img = document.getElementsByTagName('img')[0]
//異步不等繼續執行,異步回調函數:等待到網絡請求完成後觸發onload事件
img.onload = function(){
var w =img.width
console.log(w)
}
console.log(img.width)
/*或*/
document.getElementsByTagNames('img')[0].onload = function(){
console.log(this.width) // 寬度不爲 0
console.log('real done')
}
console.log('done')
複製代碼
總結: 異步想拿到一個結果,常採用監聽一個事件,而後告知(這個事件的完成時間不肯定,不可預測),那就能夠掛一個函數在onload上,等你請求完成,調用一下onload事件,此爲回調函數。
let liList = document.querySelectorAll('li')
for(var i=0; i<liList.length; i++){
liList[i].onclick = function(){
console.log(i)
}
}
//獲取dom結構的全部li元素,獲取li的長度去遍歷,每個點擊後都能打印出東西
複製代碼
把 var i 改爲 let 就能夠破解:zhuanlan.zhihu.com/p/28140450
先讓我運行上面的js代碼:
這裏,js代碼運行,還要注意一個技巧:變量提高,即var i = 0
【關鍵點】變量提高爲:
var i
i =0
複製代碼
那麼,代碼以下:
let liList = document.querySelectorAll('li')
var i //i是貫穿6次循環的一個變量(沒有多個)
for(i=0; i<liList.length; i++){
liList[i].onclick = function(){
console.log(i)
}
}
複製代碼
畫一個時序圖:
能夠看出,js執行代碼時,當i=5,i++
結果爲6的時候,並不小於liList.length
,那麼就跳出該循環,最後輸出結果:i=6。js代碼執行完,用戶開始操做他的鼠標,假設等待3ms後,執行click li
,當你最早click的時候(i=0,liList[0]
,此時js已經執行完代碼,輸出i = 6 ),而不是在綁定事件的時候打印出幾,就是幾。
在這裏,咱們有必要知道,異步函數如下綁定事件爲:
XXXX.onclick function(){
console.log(i)
}
複製代碼
瀏覽器並未等該異步執行,直接進入for循環,直接將i=6輸出,而後第一個click
纔出現,瀏覽器不會等click出現纔去打印i
值 如何解決?——使用let
假設你已經知道let(不懂看這篇文章): 方應杭:我用了兩個月的時間才理解let
將代碼var i改成let:
let liList = document.querySelectorAll('li')
for(let i=0; i<liList.length; i++){
liList[i].onclick = function(){
console.log(i)
}
}
複製代碼
運行以下:
爲什麼let能一一打印出結果呢?即let不會被提高到外面,let做用域即處於for循環函數裏,即每一次循環,liList[i]都有一個新的 i 值。let會在每一次進入循環時,產生一個分身i1-i6.畫一個運行圖:【缺】
//同步的Ajax
let request = $.ajax({
url: '.', //一、獲取當前 url
async: false
})//二、此時,該函數會等待請求完成才執行下一步
console.log(request.responseText)//打印出這個請求的響應文本,即當前html頁面
//responseText:響應文本
複製代碼
至關於同步,js在該函數中什麼都沒作,但就是停了幾十ms,如同一個呆滯的人白白浪費了一段空閒時間。
而Ajax的異步如何作?——async:true
$.ajax({
url: '.',
async: true,
success: function(responseText){
console.log(responseText)
}//表示:若是請求返回回來,麻煩調用如下success這個函數,而後把得出的結果打印出來
})
console.log('請求發送完畢')
複製代碼
在控制檯上,模擬一個網速很慢的操做:Network——slow 3G,如圖:
首先ajax函數會發一個請求,繼續執行第二句console.log,這就是ajax中的異步。在這裏,先無論ajax裏的請求成功或失敗,直接執行第二句代碼。不等,即爲異步;而等則是必定要拿到結果才進行下一步。時間不到,異步絕對拿不到結果。畫一下圖:
若是咱們把它改成同步:async:false
,並模擬一個很慢的網速:Network——add,參數設置以下:
同步以後,代碼運行演示以下:
從上面的例子中:能夠經過綁定onload事件獲取寬度大小,或者ajax中的success函數。通常,有兩種方式拿到異步結果
回調的形式
fs.readFile('./1.txt', (error, content)=>{
if(error){
// 失敗
}else{
// 成功
}
})
複製代碼
-jQuery 的 success / error 形式
$.ajax({
url:'/xxx',
success:()=>{},
error: ()=>{}
})
複製代碼
-jQuery 的 done / fail / always 形式
$.ajax({
url:'/xxx',
}).done( ()=>{} ).fail( ()=>{} ).always( ()=> {})
複製代碼
$.ajax({
url:'/xxx',
}).then( ()=>{}, ()=>{} ).then( ()=>{})
複製代碼
本身返回 Promise
function ajax(){
return new Promise((resolve, reject)=>{
作事
若是成功就調用 resolve
若是失敗就調用 reject
})
}
var promise = ajax()
promise.then(successFn, errorFn)
複製代碼
async / await
function buyFruit(){
return new Promise((resolve, reject)=>{
作事
若是成功就調用 resolve
若是失敗就調用 reject
})
}
var promise = await ajax()
複製代碼
async functon fn(){
var result = await buyFruit()
return result
}
var r = await fn()
console.log(r)
複製代碼