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

# 初めに
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3627021/21bc35c7-f24f-82d5-5850-89d3042f0a50.gif)
しっかりと動作してくれているのが分かります。

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

```js
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オブジェクトに格納しています。
```js
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オブジェクトから削除しています。
```js
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で一時停止したところから再開できます。

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

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