13

この記事は最終更新日から3年以上が経過しています。

投稿日

更新日

キャンセル可能でPromiseなsetTimeout()を作る

async/awaitを使ってきちんと非同期処理の実行順序を制御するためには、async関数内で使用するsetTimeout()のようなコールバック関数をPromise化する必要があります。

で、普通にPromise化する分にはいいんですが、clearTimeout()を呼んだときのように、外から実行を中止できるようなPromise関数を作らなければいけなかったのでメモ。

ほぼ以下のパクリですが、いくつか改変しているので、メモっておきます

最終的なコードは以下です

asyncSetTimeout.js

function asyncSetTimeout(msec, func = () => {}){
    let timeoutId
    let r
    const exec = () => new Promise((res) => {
            r = res
            timeoutId = setTimeout(async () => {
                timeoutId = null
                await func()
                res()
            },msec)
        })
    return {
        exec,
        cancel: () => {
            if (timeoutId) {
              clearTimeout(timeoutId)
              timeoutId = null
              r()
            }
        }
    }
}

使い方は以下のような感じです。処理を実行するときはexec()を呼び出します。

実行するとき.js

    const a = asyncSetTimeout(1000,asyncFunc)
    await a.exec() // ここで設定した時間分処理を待ったあとasyncFuncを実行する

もしくはもっと単純に以下のように使うこともできます

実行するとき.js
    const a = asyncSetTimeout(1000)
    await a.exec() // ここで設定した時間分処理を待ったあとasyncFuncを実行する
    await asyncFunc()

中断したいときはcancel()を呼び出します。

処理を中止するとき.js
let cancel

(async ()=>{
    const a = asyncSetTimeout(1000,asyncFunc)
    cancel = a.cancel
    await a.exec() // ここで設定した時間分処理を待ったあとasyncFuncを実行する
    dosomething()
})()

cancel() // cancel()を呼び出すと、setTimeout処理を中断し、asyncFuncを実行せずに次の処理(doSomething)を呼び出してくれる

また、以下のように使うこともできますが、この場合は、cancel()を実行してもasyncFunc()の実行は中止されません。あくまでsetTimeout()の処理が中止されるだけです。

処理を中止するとき.js
let cancel

(async ()=>{
    const a = asyncSetTimeout(1000)
    cancel = a.cancel
    await a.exec() // ここで設定した時間分処理を待ったあとasyncFuncを実行する
    await asyncFunc()
    dosomething()
})()

cancel() // cancel()を呼び出すと、setTimeout処理を中断し、次の処理(asyncFunc)を呼び出してくれる

以上。

以下はこのasyncSetTiomeout()を作るときの注意点です。使う分には関係ないので読まなくて良いです

引数にasync関数がやってくることを想定する

以下のようなコードを書くと、引数に指定したasyncFuncの中でawaitしてくれません。
今回に限らず、引数に関数を取る関数を作るときは、非同期関数がやってくることを想定して作りましょう

間違ったコード.js
function asyncSetTimeout(msec, func = () => {}){
    let timeoutId
    let r
    const exec = () => new Promise((res) => {
            r = res
            timeoutId = setTimeout(() => {
                func() // awaitをつけていない
                res()
            },msec)
        })
    return {
        exec,
        cancel: () => {
            clearTimeout(timeoutId)
            r()
        }
    }
}

clearTimeout()を呼び出すと、Promiseがresolve()しなくなる

例えば以下のようなコードは、cancel()を呼び出したときに、Promiseが永遠に解決しない関数になってしまいます。

間違ったコード2.js
function asyncSetTimeout(msec, func = () => {}){
    let timeoutId
    let r
    const exec = () => new Promise((res) => {
            r = res
            timeoutId = setTimeout(async () => {
                await func()
                res()
            },msec)
        })
    return {
        exec,
        cancel: () => {
            clearTimeout(timeoutId) // clearTimeout後にexec()のresolve()を呼び出していない
        }
    }
}

こうなってしまうと、cancel()したときに、永遠に次のコード(dosomething())が走らなくなってしまいます。気をつけましょう。

サンプルコード

新規登録して、もっと便利にQiitaを使ってみよう

  1. あなたにマッチした記事をお届けします
  2. 便利な情報をあとで効率的に読み返せます
ログインすると使える機能について
13