【Javascript】setInterval()で要素が現れるまで待つ


.ready()もonload()もsetTimeout()もなぜかうまくいかない

Chromeの拡張で結構ネックになるのがある要素にオリジナルなボタンを追加したいときに、その要素がアニメーションなどで最初は表示されておらず、loading後に表示されるという時。
色々なサイトを見ると以下のようなjQueryを使った.ready()や、ピュアなJSのonload()setTimeout()が紹介されているがうまく動作しなかったりとなんだか全部しっくり来ない。

ということで今回は要素が確実に表示されるまで待ち、かつ、見つかったらそこで処理をやめるという地球に優しい方法を伝えたいと思う。

$(document).ready( function(){
// ページ読み込み時に実行したい処理 => なぜかうまくいかない
});

window.onload = function(){
// ページ読み込み時に実行したい処理 => なぜかうまくいかない
}

setTimeout(findElement関数,2000); // 2秒間待機 => 2秒以内に見つからなかったらどうするの、というかちょっとキモい

やりたかったこと

プルリクを出してBitbucket上でコードレビューをする際に、ライブラリや特にコメントをする必要がないファイルがあると、スクロールに手間がかかるのでファイルを非表示にして見たいファイルだけを残したい。結果からいうと以下のような”隠す”ボタンを設置した。

この”隠す”ボタンを設置するところの要素がloading後にしか表示されないため、今回はこれを元に説明していく。

方法

ピュアなJSのsetInterval()を使う。setTimeout()との違いは以下の通り。
setTimeout() :一定時間後に1度だけ処理をする
setInterval():一定時間ごとに処理を繰り返す

ということで要素を見つける処理をsetInterval()に渡して見つかったらsetInterval()を止めて、やりたい処理をするという流れにすれば良さそうだ。

setInterval()の基本的な使い方

var set_interval_id = setInterval(findTargetElement, 1000);

function findTargetElement() {
 // 要素を見つける処理

 if(要素が見つかったら) {
   clearInterval(set_interval_id);
 }
}

まずsetInterval()を変数に格納する形で宣言する。これで一定時間ごとに処理を繰り返すようになる。setInterval()はインターバルを一意に識別するインターバルIDを返すため、このIDを使用してclearInterval()でインターバルを削除することができる。(clearInterval()をしなかったら永久に処理を繰り返し続けるので気をつけて。)

リトライ処理を加える

今回はもし要素が見つからなかった場合に、ずっと要素を見つけにいかせるのは嫌だったのでリトライの上限回数(秒数)を設定する。
以下の処理では1秒に一回要素を見つけにいって、10秒経っても見つからなかったらインターバルを削除するという処理をしている。

const MAX_RETRY_COUNT_FIND_DIFF_CONTAINER = 10;
var retry_counter = 0;
var set_interval_id = setInterval(findTargetElement, 1000);

/**
 * diff-container要素を取得
 */
function findTargetElement() {
    retry_counter++;
    // 要素がMAXリトライ値になっても見つからない場合、インターバルを削除
    if(retry_counter > MAX_RETRY_COUNT_FIND_DIFF_CONTAINER) {
        clearInterval(set_interval_id);
    }
    var diff_container_elements = document.getElementsByClassName('diff-container');
    if(diff_container_elements.length > 0) {
        clearInterval(set_interval_id);
        // やりたい処理を呼ぶ(”隠す”ボタンを設置する関数)
        addHideBtn(diff_container_elements);
    }       
}

/**
 * ”隠す”ボタンを追加
 *
 * @param array diff_container_elements ”隠す”ボタンを追加するdiff-container要素が格納された配列
 */
function addHideBtn(diff_container_elements) {
    //以下略...
}

var diff_container_elements = document.getElementsByClassName('diff-container');で要素を取得し、取得できたら要素が配列に格納されるので、diff_container_elements.lengthで判定して、見つかったらインターバル削除してやりたい処理を呼ぶ、見つからなかったら何もしない→インターバルでまたこの関数が呼ばれるってのをリトライの上限回数分繰り返している。

結果

インターバルを止めたあとその変数がメモリ上に残っているのが気持ち悪かったのでメモリ解放したり、後々インターバル関係なしに要素取得処理を呼びたい等あると思い結局以下のコードになった。地球に優しいかはわからんが要件は満たせたので満足。(別にグローバル変数にするぐらいならset_interval_idメモリ解放しなくていいんじゃねっていうのはなしにしましょうね)

main.js

const MAX_RETRY_COUNT_FIND_DIFF_CONTAINER = 10;
var retry_counter = 0;
set_interval_id = setInterval(getDiffContainerElements, 1000);

/**
 * diff-container要素を取得
 */
function getDiffContainerElements() {
    retry_counter++;
    // 要素がMAXリトライ値になっても見つからない場合、処理を停止する
    if(retry_counter > MAX_RETRY_COUNT_FIND_DIFF_CONTAINER) {
        clearInterval(set_interval_id);
        delete set_interval_id;
    }
    var diff_container_elements = document.getElementsByClassName('diff-container');
    if(diff_container_elements.length > 0) {
        if(typeof(set_interval_id) != 'undefined') { // setIntervalをセットしている状態(初回起動)
            clearInterval(set_interval_id);
            delete set_interval_id; //ここで変数削除しないと単体でこの関数が呼べなくなる
            addHideBtn(diff_container_elements);
        }else { // インターバル以外で関数を呼ぶとき(単純にdiff-container要素を取得したい場合に使用)
            return diff_container_elements;
        }
    }
}

/**
 * ”隠す”ボタンを追加
 *
 * @param array diff_container_elements ”隠す”ボタンを追加するdiff-container要素が格納された配列
 */
function addHideBtn(diff_container_elements) {
    const hide_btn_mode_text = '隠す';
    const show_btn_mode_text = '現す';

    var hide_btn = '<div class="aui-buttons"><button class="hide-file-btn aui-button aui-button-light" data-module="components/tooltip" original-title="ファイルを非表示" resolved="">隠す</button></div>';

    for(var i = 0; i < diff_container_elements.length; i++) {
        var diff_container_header = $(diff_container_elements[i]).children('.heading').children('.diff-actions' + '.secondary');
        diff_container_header.prepend(hide_btn);
    }

    $('.hide-file-btn').on('click', function(e) {
        var diff_container_description = $(this).parents().children('.diff-content-container' + '.refract-container');
        if($(this).text() == hide_btn_mode_text) {
            diff_container_description.css('display','none');
            $(this).text(show_btn_mode_text);
        }else {
            diff_container_description.css('display','block');
            $(this).text(hide_btn_mode_text);
        }
    });
}

余談

JSで変数を初期化するときにinterval_id = nullのようにnullセットしただけではメモリ上にはnullという値で存在している。なので完全にメモリ上から削除したい時はdelete interval_idとする必要がある。しかしdeleteではvarで宣言されたものを削除できないから注意。こういう細かいところも調べて見ると面白いし知って置くべきだなって思うよ。