這篇適合技能停在 jQuery 及 WebForm + AJAX 時代的老人。

依我從小學到的傳統概念,要存取 DOM 元素必須把程式寫進 $(function() ) 或 window.onload 事件(二者差別在於前者發生在載入 DOM 後,後者需等圖檔等資源載入完成)以確保程式執行時 DOM 已經載入。(延伸閱讀 使用 jQuery(document).ready() 與 window.onload 注意事項 by 保哥)。到了 AJAX 時代則流行另一種做法,將 Script 放在網頁末端如 </body> 的前方,執行至此 DOM 元素已解析完畢可直接存取。ASP.NET WebForm 為此提供了一個 API:RegisterStartuoScript 可將 Script 區塊放在 </form> 前,也是相同概念。

用 jQuery 包裝或掛載 DOMContentLoaded、window.onload 事件會讓程式巢狀化,結構變得複雜;限定 script 放到網頁尾端則限制了彈性,不利將 js 集中在 head 統一管理。但如果不這麼做,在 DOM 還沒解析完成前執行,存取對象會不存在。

這裡用個範例展示。demo.js 如下,目標是要讀取 <div id="d"> 文字,測試四種方式:直接讀取、window.onload 事件、jQuery(document).read()、jQuery(function() ) (快捷寫法,與 jQuery(document).read() 作用相同)。

function readDomElement(label) {
    console.log('%c' + label, 'color: cyan');
    if (!document.body)
        console.log('  No document.body');
    else 
        console.log('  #d textContent = ' + document.getElementById('d').textContent);
}

readDomElement('direct access');
window.addEventListener('load', function() {
    readDomElement('window.onload');
});
$(document).ready(() => readDomElement('jQuery document ready()'));
$(function() {
    readDomElement('jQuery(function() ...)');
});

第一次測試將 <script src="demo.js"></script> 放在 <head> 區:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <script src="demo.js"></script>
    </head>
    <body>
        <div id="d">DEMO</div>
    </body>
</html>

結果如預期,直接讀取會撲空(因為瀏覽器還沒讀到 <body> 內容),其餘三者成功,而 windows.onload 事件發生在最後:

<script> 移到網頁最後,便能直接讀取。

事實上,早在 HTML4 時代 <script> 便已支援 defer Attribute,連 IE 10+ 都能用,更別說其他真正的瀏覽器。加入 defer 後,不管 <script> 所在位置,瀏覽器會先平行載入外部 js 但不執行,直到 HTML 解析好要觸發 DOMContentLoaded 事件前再執行,與放在 </body> 前方效果相似。

我們在 <script src="demo.js"></script> 加上 defer,並加掛 DOMContentLoaded 事件,證明 defer 的執行時機在其之前。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <script src="demo.js" defer></script>
        <script>
            document.addEventListener("DOMContentLoaded", function(event) {
                console.log('%c DOMContentLoaded', 'background: #222; color: #bada55');
            });
        </script>
    </head>
    <body>
        <div id="d">DEMO</div>
    </body>
</html>

如此,即使 <script src="demo.js" defer></script> 放在 <head> 區,直接存取 DOM 也沒問題,而發生時間則在 DOMContentLoaded 事件前,印證了理論。

除了延遲執行,defer 跟另一個 async Attribute 還有個效果是,可在解析 DOM 過程平行載入,讓瀏覽器不會卡在大型 js 下載,早點顯示頁面。defer 與 async 的差別在於 defer 會等到 DOMContentLoaded 事件前才執行,async 則是解析完就立即執行。

理論是理論,用實驗來見證吧!

我用以下 PowerShell 程式產生特肥的 .js 檔:

param (
    [Parameter(Mandatory=$true)][string]$name,
    [Parameter(Mandatory=$true)][int]$sizeInMB
)
$file = "$name.js"
'/*' | Out-File $file -Encoding utf8
(1..1024*4*$sizeInMB) | ForEach-Object {
    "0123456789ABCDEF" * 16 | Out-File $file -Append -Encoding utf8
} 
'*/' | Out-File $file -Append -Encoding utf8
"console.log('$name', document.getElementById('d')?.textContent);" | Out-File $file -Append -Encoding utf8

總共做了 2 個 10MB、2 個 1MB,準備分別用 defer 跟 async 載入。

先試試 defer:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script src="10mb-defer.js" defer></script>
        <script src="1mb-defer.js" defer></script>
        <script>
            document.addEventListener('DOMContentLoaded', function() {
                console.log('%c DOMContentLoaded', 'background: #222; color: #bada55');
            });
        </script>
    </head>
    <body>
        <div id="d">DEMO</div>
    </body>
</html>

defer 保證執行順序,10MB 載入較久,但仍先執行:

換成 async 試試:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <script>
        document.addEventListener('DOMContentLoaded', function () {
            console.log('%c DOMContentLoaded', 'background: #222; color: #bada55');
        });
    </script>
    <script src="10mb-async.js" async></script>
    <script src="1mb-async.js" async></script>
</head>

<body>
    <div id="d">DEMO</div>
</body>
</html>

async 是載入完成就執行,10MB 排前面,但 1MB 體積小下載快,反而先執行。照理 async 若在 DOM 解析過程完成,可中斷 DOM 解析先執行,但也可能在 DOMContentLoaded 之後才跑,這個案例由於 .js 偏肥,都發生在 DOMContentLoaded 事件後。

若要見證在 async 中斷解析過程執行,要將 DOM 弄複雜一點,塞入三萬顆按鈕。如下圖所示,後載入的 0kb-async.js 在 DOMContentLoaded 之前執行,10mb-async 在 DOMContentLoaded 之後,得證。依據 async 此一特性,它較適合與 DOM 元素無關的 JavaScript 作業,應用時要假設載入順序相反及 DOM 尚未就緒的可能性。

學會 defer、async Attriubte,也透過實測觀察其行為,開發前端時可善用它們改善載入效率及增加 Script 安排彈性。

Introduction to Script defer and async attributes.


Comments

# by 大人

ASP.NET WebForm 為此提供了一個 API:RegisterStartuoScript 可將 Script 區塊放在 .... 错别字RegisterStartupScript

Post a comment