読者です 読者をやめる 読者になる 読者になる

KDE.BLOG

web制作で学んだことを記していきます

【JavaScript基礎】JavaScriptの実行順序について

<目次>

ブラウザでのJavaScriptの処理の流れ

サーバにリクエストし、htmlの情報がブラウザに届いてから表示されるまで、JavaScriptはどのように処理されるか。

  1. ブラウザがhtmlを読むと最初にWindowオブジェクトが生成されます。
    windowオブジェクトは各ページまたはタブごとに生成されます。
  2. windowオブジェクトのプロパティとしてDocumentオブジェクトが生成され、htmlの中身を解釈してDOMツリーを構築しようとします。
    Documentオブジェクトには、文書の読み込み状況を示す文字列を返すreadyStateプロパティがあり、このときreadyStateプロパティの値は最初の「loading」になっています。
  3. htmlは記述順にしたがって構文解析(パース)されて、Documentオブジェクトに要素オブジェクト(<head><div>などの要素)やテキストノード(ブラウザ画面上に配置して表示できる文字列、改行、空白など)が追加されていきます。
  4. この時script要素があるとそのコードをパースし、エラーがなければそこでコードを同期実行します。
    つまりJavaScriptが実行されるまでhtmlのパースは一時停止します。
  5. htmlの中身がすべてパースされてDOMツリーの構築が完了するとdocument.readyStateプロパティ値は「interactive」になります。
  6. ブラウザはDocumentオブジェクトに対してDOMツリーの構築完了を告げる「DOMContentLoaded」イベントを発生させます。
  7. img要素などの外部リソース(サブリソース)を読み込みます。
  8. すべての読み込みが完了した時点でdocument.readyStateプロパティ値は「complate」になります。
  9. 最後にブラウザはWindowオブジェクトに対してloadイベントを発生させます。
  10. ここからユーザー定義イベントなど様々なイベントを受けつけて、イベントが発生するとイベントハンドラ非同期で実行されます。

document.readyStateプロパティの移り変わりは、試しにページ上で下記コードを実行してみるとよくわかるかと思います。

alert(document.readyState); // -> 初回 'loading'
document.addEventListener('readystatechange', function () {   
    alert(document.readyState); // -> 2回目 'interactive'、 3回目 'complate'
});

イベント登録のタイミング

DOMの構築完了前にJavaScriptでDOM操作などの処理を書いても対象となる要素がないため実行できません。
そのためwindowオブジェクトのloadイベントハンドラに処理を登録しておけば読み込み完了後、各要素にイベントが登録されますがページの操作ができるまでに時間がかかります。 そこでjQuery$(function(){ });のようにDOMが作られた時点で実行されるようにしておけば操作ができるまでの時間が短縮できます。
そのための記述は下記です。

document.addEventListener('DOMContentLoaded', function() {
    // 処理を書く
});

async属性とdefer属性

asyncとdeferは、JavaScriptの非同期の読み込みと実行のタイミングを制御する論理属性です。
defer自体はもともと古くから存在していましたがHTML5で発展した形で実装されました。
通常JavaScriptは上記の流れで見たように、htmlのパースの時に<script>タグを見つけた時点でhtmlのパース、DOMの構築の処理を中断し、JavaScriptのパース・実行を同期的に実行します。
このような挙動から、<script>タグは</body>タグの直前に記述し、DOMツリーの構築が終わってから読み込ませるのがベターとされていました。
しかしHTML5でこのasyncとdeferの出現により非同期の読み込みができるようになりました。

※インラインスクリプトには使用できません。src属性を持ったscriptタグに使用できます。

<script async src="js/hoge.js">
<script defer src="js/fuga.js">

async属性:非同期で読み込み開始し、読み込み完了後に実行

async属性を付けた場合、その<script>タグを見つけた時点で非同期で読み込みを開始し、script内部の読み込みが完了次第、実行します。
そのためscript要素の実行順が保証されないため、依存関係のあるscript要素は注意が必要です。

defer属性:非同期で読み込みDOM構築完了後に実行

defer属性を付けた場合、その<script>タグを見つけた時点で非同期で読み込みを開始し、script内部の読み込みが完了してもすぐには実行せず、DOMツリーの構築が完了次第、実行します。
つまりDOMContentLoadedの代わりとして使えます。

注意

実際にテストしてみると、下記のような場合は想定通りの動きをします。

<!DOCTYPE html>
<html lang="ja">
<head>
    // 略
    <script defer src="defer1.js"></script> // 3番目に実行
    <script defer src="defer2.js"></script> // 4番目に実行
    <script async src="async1.js"></script> // 1番目に実行
    <script async src="async2.js"></script> // 2番目に実行
</head>
// 略
</html>

しかし、head要素内とbody要素内に混在させて何度も実行してみると想定した挙動にならず、しかも安定しませんでした。

<!DOCTYPE html>
<html lang="ja">
<head>
    // 略
    <script defer src="defer1.js"></script> // 3番目に実行?
    <script async src="async1.js"></script> // 1番目に実行?
</head>
// 略
<body>
    <script async src="async2.js"></script> // 2番目に実行?
    <script defer src="defer2.js"></script> // 4番目に実行?
</body>
</html>

つまりhead要素とbody要素にスクリプトタグがあり、相互にasync/defer属性があるときは依存関係に気をつけて、何度もテストしてみた方がよいでしょう。
というよりはasync/defer属性を使用する場合はhead要素内でscriptタグを記述することを心掛けた方が良さそうです。

またasync/defer属性が指定されたscrit要素がdocument.write()メソッドを含んでいる場合、DOMの構築との不具合と思われますが、属性の指定は無効になり通常のscript要素として扱われ同期実行となります。
またasync/defer属性の対応していないブラウザ上でも同じように属性指定は無効になります。

参考

async/defer属性については下記の記事の図がとても分かりやすいです。

下記ページも、古いですが分かりやすくまとまっています。

その他参考