對於 Vue、React 等框架開發的單頁應用,在某些頁面開發特殊功能時經常需要依賴第三方 JS 檔案,如果在全局引入 CDN 資源可能會加載冗余檔案,此時最好使用動態加載方式。
動態加載 JS 腳本指僅在某些特殊頁面引入依賴檔案,而非全局引入,這樣可以避免在這些頁面並未打開時造成加載無用的資源,提高頁面加載速度的同時,也讓整個專案更加模塊化。
文件物件模型(DOM)允許使用 JavaScript 動態創建 HTML。<script>
元素也是如此,它與頁面其他元素沒有什麼不同,所以可以手動創建 <script>
來加載 JS 檔案。
defer 與 async#
<script>
元素有兩個屬性 defer
與 async
分別代表兩種 JS 腳本的加載執行模式。
defer:此布林屬性被設置為向瀏覽器指示腳本在文件被解析後執行。
async:設置此布林屬性,以指示瀏覽器如果可能的話,應異步執行腳本。
對於 defer
,可以認為是將外鏈的 js 放在了頁面底部。js 的加載不會阻塞頁面的渲染和資源的加載。defer
會按照原本的 js 的順序執行。
對於 async
,它的作用是能夠異步的加載和執行腳本,同樣不會阻塞頁面的渲染和資源的加載,一旦加載到就會立刻執行。在有 async
的情況下,js 一旦下載好了就會執行,所以很有可能不是按照原本的順序來執行的。如果多個腳本檔案前後具有相互依賴性,用 async
就很有可能出錯。
所以通俗來講,瀏覽器首先會請求 HTML 文件,然後對其中的各種資源調用相應的資源加載器進行異步網路請求,同時進行 DOM 渲染,直到遇 <script>
到標籤的時候,主進程才會停止渲染等待此資源加載完畢然後執行,繼而繼續進行 DOM 解析。如果加了 async
屬性就相當於單獨開了一個進程去獨立加載和執行,而 defer
是和將 <script>
放到 <body>
底部一樣的效果。
defer 與 async
上圖藍色線代表網路讀取,紅色線代表執行時間,綠色線代表 HTML 解析。
const loadJS = (url, defer) => {
const recaptchaScript = document.createElement('script')
recaptchaScript.setAttribute('src', url)
if (defer) {
recaptchaScript.defer = true
} else {
recaptchaScript.async = true
}
recaptchaScript.onload = () => {
console.log('加載完成', url)
}
document.head.appendChild(recaptchaScript)
}
下面舉個例子,這裡加載五個 JS 腳本,其中 jquery-ui
與 fullcalendar
都依賴 jquery
,而 locale
依賴 fullcalendar
,這種情況需要讓 JS 檔案按照一定的依賴關係按次序加載資源。
const assets = [
'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.0/jquery.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.9.0/moment.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.0/fullcalendar.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/fullcalendar/3.10.0/locale/zh-cn.js',
]
assets.forEach((url) => loadJS(url, true))
現實很骨感#
然而在現實環境當中,瀏覽器對於延遲腳本並不一定會按照順序執行,也不一定會在 DOMContentLoaded 事件觸發前執行,因此僅使用 defer
來控制腳本檔案的執行順序有很大的風險,但可以通過監聽 onload
事件來判斷檔案是否加載完成,配合 Promise
等待上一個腳本檔案加載完成後再加載下一個檔案,從而實現按次序加載執行腳本。
const loadJS = (url) => {
return new Promise((resolve) => {
const recaptchaScript = document.createElement('script')
recaptchaScript.setAttribute('src', url)
recaptchaScript.defer = true
recaptchaScript.onload = () => {
resolve()
}
document.head.appendChild(recaptchaScript)
}).catch(console.error)
}
// 按次序加載 JS 檔案
const loadAssets = async () => {
for (const url of assets) {
await loadJS(url, true)
}
}
Just enjoy it ฅ●ω●ฅ