Javascriptで定期実行させたい場合、下記のように setInterval
を利用することが多いと思います。
setInterval(() => {
console.log("Hello!")
},1000)
こんな便利な setInterval
ですが、非同期処理を実行する場合は注意が必要です。
下記のコードを見てください。
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ベースで説明します。
<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
を実行してみましょう。clearTimeout
は setTimeout
を初期化(実行させないようにする)する関数です。
<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>
clearTimeout
は setTimeout
に指定した関数が実行される前では有効ですが、実行中では初期化(停止)はできません。つまり、サーバへのリクエスト中に 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>
他にもいい案があれば、教えていただけると幸いです。