2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JSで一時停止機能付きのsetTimeoutを自作してみた

Last updated at Posted at 2024-05-18

初めに

JavaScriptには、「何ミリ秒後に実行する」という処理を行うことができるsetTimeoutというメソッドが標準で搭載されてあります。

また、clearTimeoutというものも存在し、これは既に開始されたsetTimeoutを中断することができます。

しかし、その次にまた同じものを開始させた場合、カウントは途中からではなくまた始めからになってしまいます。
もちろんそのような場合の方が便利なときもありますが、途中から再開できるものも欲しいときがあったので、
少し改良して一時停止機能を搭載した自作のsetTimeoutを作りました。

コード

実際に作ってみたものがこちらになります。

mySetTimeout.js
// 時間を管理するオブジェクトの宣言
let TimeLines = {
    ID: [],
    StopTimeList: [],
    isPauseList: []
};
let isPause = false;

// 自作のsetTimeout
function mySetTimeout(Function, Time) {

    let StartTime = Date.now();

    let TimerID = setTimeout(() => {
        Function();
        // setTimeoutが終了したらその情報を削除
        ClearTimeoutData(TimerID);
    }, Time);

    TimeLines.ID.push(TimerID);
    // [開始時間, 待機時間, 待機後に実行する関数]
    TimeLines.StopTimeList.push([StartTime, Time, Function]);
    TimeLines.isPauseList.push(isPause);
    
}

// 一時停止機能
function PauseTimeout() {
    isPause = true;

    // setTimeoutの一時停止
    TimeLines.ID.forEach((ID, index) => {

        // 既に一時停止している場合は終了
        if (TimeLines.isPauseList[index]) return;
        TimeLines.isPauseList[index] = true;

        let StopTime = Date.now();

        clearTimeout(ID);

        // それぞれの待機時間から、開始してから一時停止するまでの時間を引く(開始してからの経過した時間を取得)
        TimeLines.StopTimeList[index][1] -= (StopTime - TimeLines.StopTimeList[index][0]);

    });
    console.log("Pause");
}

// カウント再開
function ResumeTimeout() {

    isPause = false;

    //setTimeoutの再開
    TimeLines.ID.forEach((ID, index) => {

        // まだ一時停止していない場合は終了
        if (!TimeLines.isPauseList[index]) return;
        TimeLines.isPauseList[index] = false;

        let ResumeTime = Date.now();

        let TimerID = setTimeout(() => {
            TimeLines.StopTimeList[TimeLines.ID.indexOf(TimerID)][2]();
            // setTimeoutが終了したらその情報を削除
            ClearTimeoutData(TimerID);
        }, TimeLines.StopTimeList[index][1]);

        TimeLines.ID[index] = TimerID;
        TimeLines.StopTimeList[index][0] = ResumeTime;

    });
    console.log("Resume");
}

// setTimeoutの情報を削除する関数
function ClearTimeoutData(TimerID) {

    let index = TimeLines.ID.indexOf(TimerID);
    if (index != -1) {
        TimeLines.ID.splice(index, 1);
        TimeLines.StopTimeList.splice(index, 1);
        TimeLines.isPauseList.splice(index, 1);
    }

}

実用例

簡単なHTMLを作成し、試しに一時停止機能ができるカウントダウンを作ってみました。

HTML

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CountdownTimer</title>
</head>

<body>

    <button id="start">スタート</button>
    <button id="pause">ストップ</button>
    <button id="resume">再開</button>
    <div id="time"></div>

    <!--今回のjsファイル-->
    <script src="Timer.js"></script>

</body>
</html>

JavaScript

Timer.js

/**
先程記述したコードをここに書く
// 時間を管理するオブジェクトの宣言
let TimeLines = {
    ID: [],
    StopTimeList: []
};
・
・
・
}
*/

// 今回は10秒に設定
const TIMER = 10000;
const StartButton = document.body.querySelector("#start");
const PauseButton = document.body.querySelector("#pause");
const ResumeButton = document.body.querySelector("#resume");

function TimerFunction() {

    // ここにタイマーが終了したときに実行する処理を書く
    console.log("関数実行");
    
}

let PauseTimeList = [];

// タイマー表示の更新を10msごとに行う
setInterval(() => {

    if (isPause) return;

    const TimerList = document.body.querySelectorAll(".timetext");
    TimerList.forEach((el, index) => {
        const time = Math.max(PauseTimeList[index][0] + PauseTimeList[index][1] - Date.now(), 0);
        const sec = Math.floor(time / 1000);
        const ms = Math.floor((time % 1000) / 10);
        el.innerHTML = `${sec.toString().padStart(2,"0")}:${ms.toString().padStart(2,"0")}`;
    });

}, 10);

// スタートボタン
StartButton.addEventListener("click", () => {

    // 一時停止中はタイマーを開始させないようにする
    if (isPause) {
        return console.warn("一時停止中のためタイマーを追加できません。");
    }

    // setTimeoutと同じ書き方でOK
     mySetTimeout(TimerFunction, TIMER);

    document.body.querySelector("#time").insertAdjacentHTML("afterbegin", '<p class="timetext">10:00</p>');

    PauseTimeList.push([TIMER, Date.now()]);
});

// ストップボタン
PauseButton.addEventListener("click", () => {
    if (isPause) return;

    const TimerList = document.body.querySelectorAll(".timetext");
    TimerList.forEach((el, index) => {
        PauseTimeList[index][0] -= Date.now() - PauseTimeList[index][1];
    });

    // mySetTimeoutを一時停止させたいときにこの関数を記述する
    PauseTimeout();
});

// 再開ボタン
ResumeButton.addEventListener("click", () => {
    if (!isPause) return;

    const TimerList = document.body.querySelectorAll(".timetext");
    TimerList.forEach((el, index) => {
        PauseTimeList[index][1] = Date.now();
    });

    // mySetTimeoutを再開させたいときにこの関数を記述する
    ResumeTimeout();
});

実行結果

このHTMLを開くとこのようになっております。
(Edgeで実行したものです)
Timer.gif
しっかりと動作してくれているのが分かります。

解説

やっていることは、PauseSetTmeoutが実行されたタイミングでclearTimeoutで一度中断し、ResumeTimeoutで開始してから中断するまでの時間の差を引いた時間、
つまり残り時間から再びsetTimeoutをするという感じです。

function PauseTimeout() {
    isPause = true;

    // setTimeoutの一時停止
    TimeLines.ID.forEach((ID, index) => {

        // 既に一時停止している場合は終了
        if (TimeLines.isPauseList[index]) return;
        TimeLines.isPauseList[index] = true;

        let StopTime = Date.now();

        // ----------ここでclearTimeoutをしている----------
        clearTimeout(ID);

        // それぞれの待機時間から、開始してから一時停止するまでの時間を引く(開始してからの経過した時間を取得)
        TimeLines.StopTimeList[index][1] -= (StopTime - TimeLines.StopTimeList[index][0]);

    });
    console.log("Pause");
}

function ResumeTimeout() {

    isPause = false;

    //setTimeoutの再開
    TimeLines.ID.forEach((ID, index) => {

        // まだ一時停止していない場合は終了
        if (!TimeLines.isPauseList[index]) return;
        TimeLines.isPauseList[index] = false;

        let ResumeTime = Date.now();

        // ----------ここで新たにsetTimeoutをしている----------
        let TimerID = setTimeout(() => {
            // 実行する関数
            TimeLines.StopTimeList[TimeLines.ID.indexOf(TimerID)][2]();
            // setTimeoutが終了したらその情報を削除
            ClearTimeoutData(TimerID);
        }, TimeLines.StopTimeList[index][1]);

        TimeLines.ID[index] = TimerID;
        TimeLines.StopTimeList[index][0] = ResumeTime;

    });
    console.log("Resume");
}

それらに使う数値やsetTimeoutのIDの情報をTimeLinesオブジェクトに格納しています。

TimeLines.ID[i]
// --> i番目のsetTimeoutのIDが格納される配列

TimeLines.StopTimeList[i][]
// --> i番目のsetTimeoutのIDの開始した時間や残り時間、待機したあとに実行する関数を格納している二重配列

TimeLines.StopTimeList[i][0]
// --> setTimeoutが開始されたときの現在時刻(Date.now()での取得)[ms]

TimeLines.StopTimeList[i][1]
// --> setTimeoutの待機時間[ms]

TimeLines.StopTimeList[i][2]
// --> setTimeoutで待機したあとに実行される関数

また、ClearTimeoutDataは、完了したsetTimeoutの情報をTimeLinesオブジェクトから削除しています。

function ClearTimeoutData(TimerID) {

    // 削除したいsetTimeoutのIDがどこにあるかを探す
    let index = TimeLines.ID.indexOf(TimerID);
    // 存在していれば削除
    if (index != -1) {
        TimeLines.ID.splice(index, 1);
        TimeLines.StopTimeList.splice(index, 1);
        TimeLines.isPauseList.splice(index, 1);
    }

}

応用(Promiseに利用する)

これをPromiseに応用することで
「3秒後に関数1を実行し、その5秒後に関数2を実行する」
みたいなこともできます。

PromiseTimeout.js
// ここに先程のmySetTimeout.jsを記述しておく

const Sleep = ms => new Promise(func => mySetTimeout(func, ms));
async function PromiseTimeout() {

    console.log("開始");

    // 3秒待機
    await Sleep(3000);

    // ここに3秒後に実行したい処理を記述する
    console.log("関数1");

    // 5秒待機
    await Sleep(5000);

    // ここに5秒後(開始してからだと8秒後)に実行したい処理を記述する
    console.log("関数2");
    
}

// 実行
PromiseTimeout();

もちろんこれも先程のPauseTimeoutで一時停止、ResumeTimeoutで一時停止したところから再開できます。

最後に

今回は私の「こんな機能あったらいいな~」という思いから作ったものです。
この記事が誰かの役に立てたならとても嬉しいです!

もしもっといい案があれば是非教えてください!
ご閲覧ありがとうございました。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?