DOM

【JavaScript】DOMが構築されてから実行する方法(async・defer属性対応)

更新日:2023/02/10

JavaScriptでDOM要素を操作する時は、DOM要素が構築されるのを待つ必要があります。
しかし外部スクリプトを読み込む場合、属性によっては実行されるタイミングがズレた結果、上手く動作しないことがあります。

そこでasync・defer属性を含めて、DOM要素の構築後に処理を開始できるような仕組みをお伝えします。

 

DOM構築前に操作するとエラー

次のコードは、DOM要素にテキストを設定しています。

<script>
 document.getElementById("id1").innerText = "Hello";
</script>

このコードを実行すると、次のようなエラーがデベロッパーツールのコンソールに表示されます。

TypeError: Cannot set properties of null (setting 'innerText')

DOMが構築されていないためid属性に"id1"を持つ要素を取得できないため、document.getElementById("id1") は nullです。

nullに対して、innerText プロパティを参照しようとしたためにエラーが表示されたのです。

 

DOMの構築を待つ方法

正常にDOM操作を行うには、DOM構築が終わるのを待つ必要があります。
async・deferを考慮しないケースと、するケースに分けて紹介します。

async・deferを考慮しない場合

次のように、インラインスクリプトやasyncまたはdefer属性を使用しない外部スクリプト読み込みについて、考えてみます。

<script>
 // インラインスクリプト
</script>

<script src="https://xxxx.xx"></script>

このケースは、DOMContentLoadedイベントで処理を行います。

document.addEventListener("DOMContentLoaded",()=>{
    document.getElementById("id1").innerText = "Hello";
}); 

コードが短い時は、外部ファイルよりもインラインの方が効率がいいです。
そのため、上記のようなコードを使用するケースが多いです。

async・deferを考慮する場合

次は、asyncdeferを考慮する場合です。
状況としてはライブラリとして配布するときなどですね。

同じ外部スクリプトを、次のように3種類の方法で読み込み可能にします。


<script src="https://xxxx.xx"></script>
<script async src="https://xxxx.xx"></script>
<script defer src="https://xxxx.xx"></script>

通常はscriptタグがあると、DOM構築を中断して外部ファイルを読み込み実行後に、DOM構築を再開します。
asyncdeferは、DOM構築と並行して読み込みます。
そして、次のようなタイミングで実行します。

async: 読み込みが終わったら実行される。DOM構築の前後、どちらでも実行される可能性がある。
defer: DOM構築後、読み込み完了を待ちDOMContentLoadedの前に実行される。

DOMの構築状況を確認して、構築前ならDOMContentLoadedイベントの登録を、構築された後ならそのままDOM操作を行えそうですね。

DOMの構築状況は、document.readyStateで確認できます。

document.readyStateの値意味
"loading"DOM構築中
"interactive"DOM構築完了。ただし画像等は読み込み中
"complete"全て読み込み完了

document.readyStateが"loading"かどうかを確認すればよさそうです。

if( document.readyState === "loading" ){
    document.addEventListener("DOMContentLoaded",()=>{
        document.getElementById("id1").innerText = "Hello";
    });
}else{
    document.getElementById("id1").innerText = "Hello";
}

関数化してみます。

window.testFunc = (( func )=>{
    const paramQueue = [];
    let EnabledDOMContentLoaded = false;

    const setDOMContentLoaded = ()=>{
        if( !EnabledDOMContentLoaded ) {
            document.addEventListener("DOMContentLoaded",()=>{
                    paramQueue.forEach( e=>func( ...e ) );
                }); 
        }
        EnabledDOMContentLoaded = true;
    };

    return (...arg)=>{
        if( document.readyState === 'loading' ){
            paramQueue.push(arg);
            setDOMContentLoaded();
        }else{
            func( ...arg );
        }
    };
})( // DOM操作を行う関数
    (id,text)=>{
        document.getElementById(id).innerText = text;
    }
);

少しだけ汎用性を考えて、DOM操作をおこなう関数を即時関数の引数で渡しています。

また、次のように複数回呼び出されるケースを想定しています。

testFunc( "id1" , "こんにちは" );
testFunc( "id2" , "こんばんは" );
testFunc( "id3" , "おはよう" );

更新日:2023/02/10

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。