throttleとdebounce イベント間引き処理

JavaScript

イベント間引き処理を実装する際に便利な throttle と debounce の使い方を紹介します。

# イベント間引きとは

特定のイベントをトリガーとして何かしらの処理をする際に、イベントが連続して発生する場合に負荷軽減のため、指定した時間が経過するまで次のイベント処理が発生しないように制御することを指します。
例えばテキストフォームに入力するとリアルタイムで API 通信して検索結果を表示するような機能を実装した場合、タイピングする度に API 通信が走ると負荷が掛かりすぎる為、イベント間引き処理を入れたりします。

# lodash のインストール

タイトルにある throttle と debounce を使うには lodash をインストールする必要があります。
Lodash (opens new window)

yarn add lodash

Vue.js で使用する場合は前述のインストール後、main.js に以下の記述を追加します。

window._ = require("lodash");

またグローバルでインポートせず、コンポーネントでインポートする場合は以下の記述になります。

<script>
import _ from "lodash";
export default {
  components: {},
}
</script>

# throttle

throttle は連続して繰り返されるイベントを一定感覚で間引きます。
わかりやすい説明があったので引用させて頂きます🙇

連続して大量に繰り返される処理を一定感覚で間引くものです。
よく使われるのは scroll イベントです。スクロールイベントをすべてハンドリングすると処理回数が多くなり、場合によってはスクロールがもっさりしてしまいますよね。それを防ぎます。

throttle と debounce | mille-feuille code (opens new window)

以下はテキストフォームの値を監視し、変更があった場合に throttle で 3 秒の間引き処理を入れて、入力値のアラートで表示するサンプルです。

Code
<template>
  <div class="throttle">
    <input v-model="text" class="input" />
  </div>
</template>

<script>
import _ from "lodash";
export default {
  data() {
    return {
      text: ""
    };
  },
  watch: {
    text: _.throttle(function(val) {
      alert(val);
    }, 3000)
  }
};
</script>

<style lang="stylus" scoped>
.throttle
  .input
    width 240px
    padding 8px
    border-radius 4px
    border 1px solid #cfcfcf
    box-sizing border-box
    &:focus-visible
      outline: none
</style>

# debounce

debounce は連続して大量に繰り返される処理が指定時間内に何度発生しても最後の 1 回だけ実行します。
こちらもとてもわかりやすい説明を引用させて頂きます🙏

連続して大量に繰り返される処理が指定時間内に何度発生しても最後の 1 回だけ実行するものです。 よく使われるのは resize イベントです。「ウインドウサイズが変わったときに要素のサイズを変える」みたいなケースってありますよね。その時、ウインドウの端っこをつまんでぐりぐりとドラッグするイベントを全て取得しても意味がありません。マウスから手を話し resize が完了したときのイベントをとれば十分です。

throttle と debounce | mille-feuille code (opens new window)

以下はテキストフォームの値を監視し、変更があった場合に debounce で 3 秒の間引き処理を入れて、入力値のアラートで表示するサンプルです。
前述した throttle との違いが分かるかと思います。

Code
<template>
  <div class="debounce">
    <input v-model="text" class="input" />
  </div>
</template>

<script>
import _ from "lodash";
export default {
  data() {
    return {
      text: ""
    };
  },
  watch: {
    text: _.debounce(function(val) {
      alert(val);
    }, 3000)
  }
};
</script>

<style lang="stylus" scoped>
.debounce
  .input
    width 240px
    padding 8px
    border-radius 4px
    border 1px solid #cfcfcf
    box-sizing border-box
    &:focus-visible
      outline: none
</style>

# サンプル

こちらは debounce を使った実用的(?)なサンプルコードになります。
テキストフォームがタイピングされる度に、検索処理を実行していますが、debounce で 1 秒の間引き処理をしています。
この配列のデータ量で filter するくらいなら、特に間引き処理を入れる必要はないかと思いますが、検索処理が API 通信でデータ取得するような処理だと効果を発揮する実装だと思います。

# コード

Code
<template>
  <div class="debounce-search">
    <div>
      <div class="search">
        <input
          v-model="search"
          @keydown="onKeydown"
          @keyup="onKeyup"
          class="search__input"
        />
        <vue-loading
          v-if="loading"
          type="spin"
          color="#9fcdff"
          :size="{ width: '24px', height: '24px' }"
          class="search__loading"
        />
      </div>
      <div class="data">
        <template v-if="searchList.length">
          <ul class="list">
            <li v-for="item in searchList" :key="`searchList${item}`">
              {{ item }}
            </li>
          </ul>
        </template>
        <template v-else>
          検索結果がありません。
        </template>
      </div>
    </div>
  </div>
</template>

<script>
import _ from "lodash";
import { VueLoading } from "vue-loading-template";
export default {
  components: {
    VueLoading
  },
  data() {
    return {
      search: "",
      list: [],
      searchList: [],
      loading: false
    };
  },
  created() {
    this.createList();
  },
  methods: {
    createList() {
      for (let i = 1; i <= 20; i++) {
        // 7桁のランダムな整数(0埋め)を生成
        this.list.push(
          ("0000000" + (Math.floor(Math.random() * 9999999) + 1)).slice(-7)
        );
      }
      this.searchList = _.cloneDeep(this.list);
    },
    onKeydown() {
      this.loading = true;
    },
    onKeyup: _.debounce(function() {
      const search = this.search;
      this.searchList = _.filter(this.list, item => {
        return _.includes(item, search);
      });
      this.loading = false;
    }, 1000)
  }
};
</script>

<style lang="stylus" scoped>
.debounce-search
  .search
    display flex
    align-items center
    &__input
      width 240px
      padding 8px
      border-radius 4px
      border 1px solid #cfcfcf
      box-sizing border-box
      &:focus-visible
        outline: none
    &__loading
      display inline-block
      margin-left 8px
  .data
    padding: 16px 0
  .list
    margin: 0
</style>

# プレビュー


Last Updated: 2021-9-2 12:36:29