8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

setIntervalとsetTimeoutの使う際には注意を

Last updated at Posted at 2022-09-05

Javascriptで定期実行させたい場合、下記のように setInterval を利用することが多いと思います。

1秒おきにコンソールに文字列を出力
setInterval(() => {
  console.log("Hello!")
},1000)

こんな便利な setInterval ですが、非同期処理を実行する場合は注意が必要です。

下記のコードを見てください。

1秒おきにサーバへリクエスト
setInterval(async () => {
  const response = await fetch("<APIエンドポイント>")
  const data = await response.json()
  console.log("OK")
},1000)

1秒おきに「OK」が出力されるでしょうか?答えはNoです。リクエストからレスポンスまでの時間が1秒未満であれば、1秒おきにメッセージが出力されますが、それ以上になると出力されるタイミングはバラバラです。これは、setInterval の仕組みが、指定された関数の処理時間に関係なく指定されたインターバルで実行されるようになっているからです。これの何が問題かといいますと、サーバからのレスポンス待ちのときに次々とリクエストされてしまいサーバへの負荷に繋がってしまいます。では、遅延を考慮した定期実行を行う場合はどうしたらよいでしょう?

再帰呼び出し + setTimeout を利用しましょう。setTimeout は指定された時間経過後に関数を一度実行するものです。そのため、定期実行するためには再帰呼び出しの仕組みも必要になります。

先ほどのコードを書き換えてみます。

const polling = () => {
  setTimeout(async () => {
    const response = await fetch("<APIエンドポイント>")
    console.log("OK")
    polling() // 再帰呼び出し
  },1000)
}

polling()

これで問題なく遅延を考慮した定期実行ができます。ただし、SPAを利用している場合さらに注意が必要です。

画面Aから画面Bへ遷移させるケースを考えてみます。

※例としてVue3ベースで説明します。

画面A
<template>
  <div><router-link to="/画面B">画面B</router-link></div>
</template>

<script setup>
const polling = () => {
  setTimeout(async () => {
    const response = await fetch("<APIエンドポイント>")
    console.log(response.status)
    polling()
  },1000)
}

// コンポーネントがDOM要素にマウントされた際に呼ばれる
onMounted(() => polling())
</script>

router-link は画面遷移させる、またはコンポーネント差し替える際に利用するaタグです。

ここで画面に表示される画面Bへのリンクをクリックして画面遷移しても、定期実行処理は止まりません。
SPAの特性上、location.href とは違い、メモリは明示的な処理を記述しない限りクリアされません。そのため、画面Aの定期実行も継続されてしまいます。

では、画面遷移する際に clearTimeout を実行してみましょう。clearTimeoutsetTimeout を初期化(実行させないようにする)する関数です。

clearTimeoutによる初期化
<template>
  <div><router-link to="/画面B">画面B</router-link></div>
</template>

<script setup>
let timeout
const polling = () => {
  timeout = setTimeout(async () => {
    const response = await fetch("<APIエンドポイント>")
    console.log(response.status)
    polling()
  },1000)
}

onMounted(() => polling())

// コンポネントが破棄された際に呼ばれる
onUnmounted(() => if(timeout) clearTimeout(timeout)) // timeoutが設定されている場合のみclearTimeoutを実行
</script>

clearTimeoutsetTimeout に指定した関数が実行される前では有効ですが、実行中では初期化(停止)はできません。つまり、サーバへのリクエスト中に clearTimeout が呼ばれても、レスポンス後の polling() により setTimeout が設定されてしまい継続されてしまいます。

じゃあどうするの?

あくまでも現時点の暫定的な対応になると思いますが、VueRouterのcurrentRoute.pathが自画面でない場合に早期リターンを行うことで対応可能です。

自画面判定による早期リターン
<template>
  <div><router-link to="/画面B">画面B</router-link></div>
</template>

<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()

const polling = () => {
  setTimeout(async () => {
    // 現在表示されているコンポーネントが画面Aでない場合はreturn
    if(router.currentRoute.path !== '/画面A') return
    const response = await fetch("<APIエンドポイント>")
    console.log(response.status)
    polling()
  },1000)
}

onMounted(() => polling())
</script>

他にもいい案があれば、教えていただけると幸いです。

8
5
3

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
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?