redux-sagaのthrottleとdebounceを試してみる

## はじめに
[redux-sagaのAPI reference](https://redux-saga.js.org/docs/api/)を眺めていて、読んだだけでは動きがイメージしにくかったthrottleとdebounceについて、実際に動かしてイメージを掴んでみました。~~(英語難しい)~~
実際の開発で使用したわけではないので、サンプルを作って動かしてみただけになります。サンプルのソースは[こちら](https://github.com/r-nouchi/redux-saga-example)からどうぞ。

使用したredux-sagaのバージョン: 1.0.5

## TL;DR
基本的にはどちらのAPIもtakeと同様にactionのdispatchを待ち受けてタスクを起動します。同じactionが短時間内に複数dispatchされた際の挙動が異なります。

- throttle: actionがdispatchされたらタスクを起動します。指定時間内に同じactionがdispatchされた場合はタスクを起動せずに最新のactionを1個だけ保持しておき、指定時間経過後にタスクを起動します。
- debounce: actionがdispatchがされたら、actionを保持して指定時間待ってからタスクを起動します。待っている間に同じactionがdispatchされた場合は、新しいactionを保持してまた指定時間待ちます。

## サンプルコード
文字だけだと「なるほど、わからん」状態になるので、実際に動かしてみました。

サンプルは[前回の記事](https://qiita.com/r-nouchi/items/8a0bbb078114959c3a02)で使用したものにthrottleとdebounceを追加したものを使用しました。throttleとdecounceのボタンをクリックしたらそれぞれ`onClickThrottleButton`、`onClickDebounceButton`が実行されます。

![スクリーンショット 2019-09-29 12.02.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/df4e02af-c9e4-8aab-9ee7-c024ee7366f7.png)

```sampleContainer.js
  // 省略

  onClickThrottleButton: () => {
    let count = 0
    const interval = setInterval(() => {
    dispatch(throttleSampleStart(count))
      count++
      if(count >= 6) {
        clearInterval(interval)
      }
    }, 500)
  },
  onClickDebounceButton: () => {
    let count = 0
    const interval = setInterval(() => {
    dispatch(debounceSampleStart(count))
      count++
      if(count >= 6) {
        clearInterval(interval)
      }
    }, 500)
  },

  // 省略
```

```sampleSaga.js
// 省略

function* handleThrottleSampleStart() {
  yield throttle(1800, THROTTLE_SAMPLE_START, runThrottleSampleStart)
}

function* runThrottleSampleStart(action) {
  console.log(`take action ${JSON.stringify(action)}`)
  yield call(sleepAsync, action.payload.count)
  yield put(throttleSampleSuccess())
}

function* handleDebounceSampleStart() {
  yield debounce(1200, DEBOUNCE_SAMPLE_START, runDebounceSampleStart)
}

function* runDebounceSampleStart(action) {
  console.log(`take action ${JSON.stringify(action)}`)
  yield call(sleepAsync, action.payload.count)
  yield put(debounceSampleSuccess())
}

const sleepAsync = async (count) => {
  await new Promise(r => setTimeout(r, 5000))
}

// 省略
```




## 動作確認
### throttle
実行結果は以下になります。まずdispatchされたaction(payloadのcountが0)でタスクが起動し、1800ミリ秒待ちます。その間dispathされたaction(payloadのcountが1、2、3)ではタスクは起動しません。
1800ミリ秒経過後、保持していた最新のaction(payloadのcountが3)で再度タスクを起動します。

![スクリーンショット 2019-09-29 12.13.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/32e793f1-f61e-5827-8a7f-c646a4cdca25.png)

画像ではcount4のactionではタスクが起動されていません。これは、指定時間経過後にタスク(count3のaction)が起動されていますが、このタスクが起動してからも指定時間が経過するまで同じactionを保持する状態になっているからです。（ややこしい‥）
count3のタスクが起動してから1800ミリ秒以内にcount4と5のactionがdispatchされているので、最新のaction(countが5)が保持されて1800ミリ秒経過後にタスクが起動されているようです。

### debounce
実行結果は以下になります。throttleと比較していくらかシンプルですね。同じactionがdispatchされる度に最新のactionを保持して新たに指定秒数待ちます。待っている間に同じactionが来なかった場合にタスクを起動しています。

![スクリーンショット 2019-09-29 12.35.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/306956/338d84d8-297f-4195-a154-6d72794638cc.png)


## まとめ
throttleはtakeEveryの代わりに使えそうだと感じました。takeEveryはdispatchされたactionをすべて拾うので、負荷の面でちょっと心配がありますが、throttleならある程度コントロールができます。そのため、actionを取りこぼしたくない、かといって負荷もあまりかけたくない、といった場面では出番がありそうですね。
debounceは‥ちょっと思いつきませんでした。開発を進める中で有効に活用できる場面が出てきたらまた紹介したいと思います。