簡單的算法-解決頁面腳本異步加載順序問題

這幾天稍微掃了一下CoffeeScript的部分源碼,發現了一條挺有意思的算法,它解決了頁面異步加載腳本時遇到的順序問題。只是當初都沒想過能夠這樣優雅地去處理這方面的問題。異步加載的腳本之間可能會有依賴關係,所以加載順序就異常重要了。javascript

一. 場景分析-同步與異步

1. 同步加載

假如瀏覽器須要引入多個JavaScript資源,咱們通常會在頁面上嵌入以下代碼html

<body>
  <script src="http://xxxxx.com/global.js"></script>
  <script> <!-- Global 全局變量是從global.js腳本中引入 --> window.Global.user_id = "x223345333445" </script>
  <script src="http://xxxxx.com/main.js"></script>
</body>
複製代碼

默認狀況下script標籤裏面的資源會自動加載,而且這個過程是同步的,咱們並不須要擔憂第一個script標籤請求完成以前瀏覽器就去執行第二個script標籤中的代碼(超時或者是加上了aync這些屬性的狀況要另當別論了)。這種場景我姑且稱之爲腳本的同步加載。java

2. 異步加載

上述場景中script標籤至關於自動加上了type="text/javascript"這樣一個屬性值,瀏覽器會自動識別這類資源並進行加載。可是若是加載的不是JavaScript資源呢?假設咱們要加載CoffeeScript資源,或許就會把代碼寫成這樣git

<body>
  <script type="text/coffeescript" src="http://xxxxx.com/global.coffee"></script>
  <script type="text/coffeescript"> <!-- Global 全局變量是從global.coffee腳本中引入 --> window.Global.user_id = "x223345333445" </script type="text/coffeescript"> <script src="http://xxxxx.com/main.coffee"></script>
</body>
複製代碼

這種狀況下,瀏覽器就不會自動加載script標籤裏面的資源了,畢竟瀏覽器沒法直接解析這種類型的腳本。github

若是要加載這類資源咱們則須要手動編寫代碼來遍歷全部包含屬性值type="text/coffeescript"script標籤,若是是帶有src屬性則異步請求資源,若是沒有src屬性則直接獲取標籤包裹的內容。經過特殊的腳原本執行加載好的CoffeeScript代碼。ajax

這種場景中第一和第三個標籤都須要經過發送請求來獲取資源,這就會致使一種現象,若是不加特殊處理第二個標籤裏面的代碼會比另外兩個腳本先執行,而這個時候變量Global尚未被定義,就會致使腳本出錯。這種就是異步加載腳本的場景,異步雖好,它不會堵塞頁面,不過要處理各個腳本之間的依賴關係也是個頭疼的問題。算法

二. 解決方案

在不考慮使用打包工具的狀況下我暫且提出這三個解決方案瀏覽器

1. 回調

回調無疑是最爲簡單粗暴的方式,以上的案例中只有3個JavaScript資源,構建一條完整的回調鏈彷佛沒什麼問題。不過仍是會使代碼變得難懂,且噁心。回調很簡單這裏就不貼代碼了。bash

若是我把script標籤增長到10個,而且其中包含幾個內嵌腳本的話你應該不會再想用回調來解決了吧?案例以下異步

<body>
  ...
  <script type="text/coffeescript" src="http://xxxxx.com/extern1.coffee"></script>
  <script type="text/coffeescript"><!-- 內嵌腳本 --></script>
  <script type="text/coffeescript" src="http://xxxxx.com/extern2.coffee"></script>
  <script type="text/coffeescript" src="http://xxxxx.com/extern3.coffee"></script>
  <script type="text/coffeescript"><!-- 內嵌腳本 --></script>
  <script type="text/coffeescript" src="http://xxxxx.com/extern4.coffee"></script>
  <script type="text/coffeescript" src="http://xxxxx.com/extern5.coffee"></script>
  <script type="text/coffeescript"><!-- 內嵌腳本 --></script>
  <script type="text/coffeescript" src="http://xxxxx.com/extern6.coffee"></script>
  <script type="text/coffeescript"><!-- 內嵌腳本 --></script>
</body>
複製代碼

固然,上面的只是示範代碼,正常狀況下咱們不可能這樣去寫代碼。這種狀況若是用回調去解決加載順序問題的話,估計是我的都會崩潰了。咱們須要尋找更好的解決方案。

2. 填充隊列,隊列滿了再執行

爲了異步加載CoffeeScript資源,我先把僞代碼寫成這樣

// 用於運行CoffeeScript代碼
function handleCoffeeScript(cfCodeString) {
 ...
}

// 用於異步請求資源,返回Promise
function ajax(url) {
}

document.querySelectorAll('[type="text/coffeescript"]').forEach((item) => {
  if (item.src) {
    ajax(item.src).then((content) => {
        handleCoffeeScript(content)
    })
  } else {
    handleCoffeeScript(item.innerHTML)
  }
})
複製代碼

這代碼咋一看彷佛沒什麼問題,尤爲是這10個script標籤所涵蓋的CoffeScript代碼的業務邏輯彼此間沒有任何依賴關係的時候,上訴代碼徹底能夠直接使用。然而,一旦它們之間有依賴關係,這樣去加載腳本就會報錯。我給10個腳本分別編號1-10,假設全部腳本都可以順利加載,那麼會出現下面的狀況

2, 5, 8, 10 // 同步的內嵌腳本先加載運行

1, 3, 4, 6, 7, 9 // 須要異步請求的腳本後運行
複製代碼

PS: 這只是一種狀況,咱們永遠沒法保證先發送的請求會先響應,畢竟每一個接口的響應時間都不同,假設編號1中的資源比較大響應時間較長,那麼執行順序可能會變成3, 4, 1, 6, 7, 9。

一般爲了解決這種問題咱們須要維護一個。初始化一個特定長度的隊列,初始值值都是undefined。因爲異步腳本都會在同步腳本以後才能被執行,爲此能夠在每次異步請求結束時都去檢測隊列是否已經滿了,若是滿了就證實全部腳本都已經加載完畢。接着依次執行隊列中的每一項所包含的CoffeeScript資源。

// 用於運行CoffeeScript代碼
function handleCoffeeScript(cfCodeString) {
 ...
}

// 用於異步請求資源,返回Promise
function ajax(url) {
 ...
}

const sources = document.querySelectorAll('[type="text/coffeescript"]')
// 初始化隊列
let queue = new Array(sources.length)

// 檢測隊列是否已經滿了
function checkQueueFull(queue) {
    for(let i = 0; i < sources.length; i ++) {
        if (queue[i] === undefined) return false
    }
    return true
}

sources.forEach((item, i) => {
  if (item.src) {
    ajax(item.src).then((content) => {
        // 隊列填充
        queue[i] = content
        // 隊列若是塞滿的話則依次運行全部腳本
        if (checkQueueFull(queue)) {
            queue.forEach(item => handleCoffeeScript(item))
        }
    })
  } else {
    // 隊列填充
    queue[i] = item.innerHTML
  }
})
複製代碼

這個腳本確實可以解決異步加載資源時遇到的順序問題了,可是它顯得有點笨拙,它必需要等到全部腳本加載完成後纔可以依次去執行全部腳本

假設編號爲5的資源並非那麼重要,並且加載時間會比較長,這種方式就會致使全部資源都須要等待編號5的資源加載完畢以後纔有機會執行,這會致使腳本層面的堵塞。接下來咱們進一步優化這個流程,看如何規避這種問題。

3. 填充隊列,讓腳本儘量早地去運行

爲了優化這個過程,**除了上述的隊列咱們還須要另外維護一個索引,每次異步請求完成以後檢測當前索引所在位置的資源,若是這個資源已經加載好了,則執行當前位置的腳本,索引自增,再檢測下一個索引所對應的資源是否可以執行,以此類推,直到遇到某個不可用的資源則中止執行。當再次發生異步請求的候重複上述過程,會根據索引值從以前中止的地方從新開啓檢測。**這一切能夠以遞歸的方式實現,僞代碼大概以下

// 用於運行CoffeeScript代碼
function handleCoffeeScript(cfCodeString) {
 ...
}

// 用於異步請求資源,返回Promise
function ajax(url) {
}

const sources = document.querySelectorAll('[type="text/coffeescript"]')

// 建立一個等長的隊列
let queue = new Array(sources.length)

// 腳本執行索引
let index = 0

// 執行函數,採用遞歸的方式,檢測隊列中當前索引的資源是否可用,若是可用則調用`handleCoffeeScript`方法來處理相關的內容,遞增索引,並調用自身
function execute() {
    param = queue[index]
    if(param !== undefined) {
        handleCoffeeScript(content)
        index ++
        execute()
    }
}

sources.forEach((item, i) => {
  if (item.src) {
    ajax(item.src).then((content) => {
        queue[i] = content
        // 每次腳本加載完成都觸發執行腳本,具體是否須要執行須要執行腳原本判斷
        execute()
    })
  } else {
    queue[i] = item.innerHTML
  }
})
複製代碼

咱們來幻想一個比較極端的場景,假設1號和9號的異步請求都是慢請求,9號腳本的耗時比1號腳本長許多(假設是5s),那麼加載程序運行起來會有如下表現

PS:簡單起見,我暫時用腳本的狀態來對隊列中的每一項進行佔位。

  • 同步腳本率先被加載進隊列,但先不執行
[undefined, "Available", undefined, undefined, "Available", undefined, undefined, "Available", undefined, "Available"]
複製代碼
  • 除了1號,9號腳本以外,其餘異步腳本都加載完成並塞進隊列中
[undefined, "Available", "Available", "Available", "Available", "Available", "Available", "Available", undefined, "Available"]
複製代碼
  • 1號腳本加載完成
// 1號腳本加載完畢
["Available", "Available", "Available", "Available", "Available", "Available", "Available", "Available", undefined, "Available"]
複製代碼

後續腳本會依次運行,但因爲9號腳本加載時間太長,因此在對應的位置會中止執行,並等待

// 而後依次執行
["Executed", "Available", "Available", "Available", "Available", "Available", "Available", "Available", undefined, "Available"]

["Executed", "Executed", "Available", "Available", "Available", "Available", "Available", "Available", undefined, "Available"]

.....

["Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", undefined, "Available"]
複製代碼
  • 等9號腳本加載完畢,繼續執行餘下腳本
["Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Available", "Available"]

["Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Available"]

["Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed", "Executed"]
複製代碼

這個腳本的性能會比以前的腳本好上一些了,起碼它不會等到全部資源都加載完畢以後纔去執行。

一方面,2-10號的資源都須要依賴1號資源,它保證了1號資源加載並執行完畢以前不會執行任何其餘的腳本。另外一方面,加載9號腳本須要比較長的時間,而咱們並不須要等到它加載完了纔去運行其餘腳本,而是會讓在它以前的可以執行的腳本先行執行。只有10號腳本會等待9號腳本。

總結

這篇文章簡單地對異步加載腳本可能遇到的問題以及相關的解決方案作了個簡單的闡述,雖然說真實環境可能不再會遇到這種問題了,不過了解一下算法仍是有好處的,說不定哪天遇到相似的場景就派上用場了。

本文只是用JavaScript寫了些僞代碼,算法流程也只是用文原本簡單闡述,可能會致使有些地方表達不夠到位。若是想更全面地瞭解這個加載腳本我建議直接看Coffeescript裏面的源代碼。

相關文章
相關標籤/搜索