JS指令碼非同步載入淺析

MiyaWang發表於2018-08-17

前言:

在梳理知識點的時候,發現作為瀏覽器渲染中的機制之一——非同步載入機制,當使用者訪問站點,需要下載各種資源,例如JS指令碼,CSS,圖片,iframe等,它是實現現代網站進行載入頁面時一種必不可少的手段。查資料加上老師擴充課程均對於非同步載入機制還有很多方法可以說,故抽出來單獨進行一個知識點的梳理。


瞭解js指令碼非同步載入前,我們有必要先了解一下瀏覽器在頁面樣式和js的作用下出現的兩種頁面常見場景:白屏和fouc(無樣式內容閃爍)。

一、白屏和FOUC

1、即指影響瀏覽器頁面載入順序的兩種場景

-白屏:特指一種場景,開啟頁面是一片白色,突然頁面出現,樣式正確。那麼一片白色的時間,則稱之為白屏。 -FOUC (Flash of UnstyledContent):無樣式內容閃爍,網速情況差,開啟頁面時仍有樣式,之後樣式時有時無,甚至一開始並無出現樣式,突然樣式恢復。(常出現在firefox瀏覽器)

此類現象,在不同瀏覽器進行的資源載入和頁面渲染時,所採用的不同的處理方式,並不是bug。

2、寫一個server,驗證白屏和fouc效果

在樣式檔案index.html中

//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.cssb.cssb.css引用了c.ss@import"./c.css?t=5";b.css中加入了一個10s的延時檔案(<link rel="stylesheet"href="b.css?t=10">),載入這個10s的css樣式檔案,瀏覽器是如何完成載入工作,有兩種方式:

第1種: html解析完成,此時10s延時的css檔案先不管,先展示<body>裡所展示的內容,等css檔案全載入後再去計算樣式,再去重新渲染一次

第2種: 即使html的dom樹已經解析、渲染都完成,對未載入完成的樣式都必須等待,即css樣式要全部載入、獲取,img資源載入完成,此時底部JS立刻執行,才一次性展示出頁面。例子中展示這種方法,即為白屏很久的原因。

(2)不同瀏覽器的不同處理機制所出現的場景不同

A、白屏場景(常出現在chrome): 開啟一個國外網站,使用國外伺服器,嵌在css的字型使用的是谷歌字型,執行特別慢,等了好久突然出現頁面樣式效果。這是因為頁面需要等待css樣式載入所有完成,甚至出現404載入失敗,最後才展示出頁面。那麼那段載入時間,等待了幾秒左右的白色一片的頁面,就是白屏

B、Fouc場景(常出現在Firefox): 一開始的時候,先讓你看見樣式,如字的小號樣式,樣式載入完後看到所規定字號的大字。對使用者來說,同樣的樣式,突然從小變大,則這個場景就是Fouc(無樣式內容閃爍)。

總結: 不管是css樣式,還是js檔案,只要加長延時,都會造成白屏

(3)CSS 和 JS 最佳放置順序

  • 使用 link 標籤將樣式表放在頂部
  • 將JS放在底部

(3.1)場景:假設JS檔案頁面頂部:

  • JS指令碼會阻塞後面內容的呈現
  • JS指令碼會阻塞其後元件(如圖片)的下載
  • JS載入時間過長,css需等待,則會出現一段時間白屏

場景說明: 引入一個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指令碼,然後再渲染。

  • (放頂部時)JS載入時機過晚導致一系列問題,指令碼會阻塞後面內容的呈現、指令碼會阻塞其後元件的下載(主要指img資源下載)、白屏等。
  • (放底部)則可以先讓其他先載入完成,JS立刻執行的特點可以“掃尾”最後的頁面效果

C、JS指令碼操作頁面上的html+css元素,(放頂部時)JS先執行,元素都未載入到(即不存在),未出現在文件流中【載入,這裡指資源載入和資源是否出現在文件流中】,所以也不能操作相應JS功能,此時後臺將會報錯。

D、(放頂部時)其他JS若作為一種框架語言,則能提前形成一個初步的框架有效構成頁面結構。

二、JS指令碼的非同步載入

1、一個問題?

即一個放在的js檔案,如下: <script src="script.js"></script>

原本放在頂部的這個js檔案,會提前載入,如何使它在頂部仍然稍後載入呢?

2、解決方法: asyncdefer

(1)作用: 沒有 deferasync,瀏覽器會立即載入並執行指定的指令碼,“立即”指的是在渲染該<script>標籤之下的文件元素之前,也就是說不等待後續載入的文件元素,讀到就載入並執行。也就是說,使用deferasync後能夠改變這種載入、執行的時機。

常應用在引用了廣告和統計的頁面中,不會影響、堵塞,更不會影響到到頁面其他元素

(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不可能不做事情
複製程式碼

JS指令碼非同步載入淺析

同步的 sleep
//非同步的 sleep
function sleep(seconds, fn){
    setTimeout(fn, seconds * 1000)
}
console.log(1)
sleep(3, ()=> console.log('wake up'))
console.log(2)
複製程式碼

非同步的 sleep

畫一張同步&非同步工作的示意圖:

JS指令碼非同步載入淺析
可以看出,用了非同步之後,JS 的空閒時間多了許多。

但是注意,在 JS 空閒的這段時間,實際上是瀏覽器中的計時器在工作(很有可能是每過一段時間檢查是否時間到了,具體要看 Chrome 程式碼)

四、遇到非同步例項

1、前端經常遇到的非同步:圖片載入是需要時間的

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指令碼非同步載入淺析
由此可知,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事件,此為回撥函式。

2、面試題中的非同步

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指令碼非同步載入淺析
這裡,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指令碼非同步載入淺析

可以看出,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)
    }
}
複製程式碼

執行如下:

JS指令碼非同步載入淺析
為何let能一一列印出結果呢?即let不會被提升到外面,let作用域即處於for迴圈函式裡,即每一次迴圈,liList[i]都有一個新的 i 值。let會在每一次進入迴圈時,產生一個分身i1-i6.

畫一個執行圖:【缺】

3、AJAX 中的非同步(必須)

//同步的Ajax
let request = $.ajax({
  url: '.',   //1、獲取當前 url
  async: false
})//2、此時,該函式會等待請求完成才執行下一步
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,如圖:

JS指令碼非同步載入淺析
首先ajax函式會發一個請求,繼續執行第二句console.log,這就是ajax中的非同步。在這裡,先不管ajax裡的請求成功或失敗,直接執行第二句程式碼。不等,即為非同步;而等則是一定要拿到結果才進行下一步。時間不到,非同步絕對拿不到結果。

畫一下圖:

JS指令碼非同步載入淺析
如果我們把它改為同步:async:false,並模擬一個很慢的網速:Network——add,引數設定如下:
JS指令碼非同步載入淺析
同步之後,程式碼執行演示如下:
JS指令碼非同步載入淺析

五、非同步的形式

從上面的例子中:可以通過繫結onload事件獲取寬度大小,或者ajax中的success函式。一般,有兩種方式拿到非同步結果

1、傻逼方法:輪詢

2、正規方法:回撥

回撥的形式

  • Node.js 的 error-first 形式
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( ()=> {})
複製程式碼
  • Prosmise 的 then 形式
$.ajax({
     url:'/xxx',
 }).then( ()=>{}, ()=>{} ).then( ()=>{})
複製程式碼

六、如何處理異常?

  • 如何使用多個 success 函式?
  • 在有多個成功回撥的情況下,如何處理異常?

自己返回 Promise

function ajax(){
    return new Promise((resolve, reject)=>{
        做事
        如果成功就呼叫 resolve
        如果失敗就呼叫 reject
    })
}

var promise = ajax()
promise.then(successFn, errorFn)
複製程式碼
  • Promise 深入閱讀:
  • Promise/A+ 規範:

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)
複製程式碼

相關文章