Frontend Development 9 min read

Implementing Input Debounce and Throttle in Vue 3 Directives with Chinese Input Support

This article explains how to create a reusable Vue 3 directive that applies debounce and throttle to input events, addresses the extra triggers caused by Chinese IME composition, and provides complete TypeScript code examples and usage instructions.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing Input Debounce and Throttle in Vue 3 Directives with Chinese Input Support

The author, a front‑end developer with three years of experience, introduces the common need for search input handling in many applications and points out that frequent input events can overload back‑end services.

To reduce unnecessary requests, the article discusses two classic techniques: debounce, which delays execution until a pause in events, and throttle, which limits execution to at most once per interval.

Debounce implementation example:

function debounce(fn, time) {
  let timer;
  return function (...argu) {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    timer = setTimeout(() => {
      fn(...argu);
      clearTimeout(timer);
      timer = null;
    }, time);
  }
}

Throttle implementation example:

function throttle(fn, time) {
  let flag = true;
  return function (...argu) {
    if (!flag) {
      return;
    }
    flag = false;
    let timer = setTimeout(() => {
      fn(...argu);
      flag = true;
      clearTimeout(timer);
      timer = null;
    }, time);
  }
}

Visual comparisons (GIFs) show that plain debounce delays the final call, while debounce + throttle limits calls to a fixed interval.

The article then highlights a problem: when users type Chinese characters, the IME generates multiple input events even with debounce, because composition events are not considered.

To solve this, the author explains the compositionstart and compositionend events, which indicate the start and end of a composition session. By setting a composing flag on the target element, the debounce logic can ignore intermediate events.

Composition handlers:

function compositionStart(event) {
  event.target.composing = true;
}
function compositionEnd(e) {
  e.target.composing = false;
  const event = new Event('input', { bubbles: true });
  e.target?.dispatchEvent(event);
}

Full Vue directive implementation (TypeScript) that finds the underlying input element, applies the debounced function, and registers the composition listeners:

import { debounce, isFunction } from '../../utils/index'

let inputFunction: (event: Event) => void = () => {}

function findInput(el: HTMLElement): HTMLElement | null {
  const queue: HTMLElement[] = []
  queue.push(el)
  while (queue.length > 0) {
    const current = queue.shift()
    if (current?.tagName === 'INPUT') {
      return current
    }
    if (current?.childNodes) {
      queue.push(...Array.from(current.childNodes) as HTMLElement[])
    }
  }
  return null
}

export default {
  mounted(el: HTMLElement, binding: any) {
    const { value, arg } = binding
    if (value && isFunction(value)) {
      let timeout = 600
      if (arg && !Number.isNaN(arg)) {
        timeout = Number(arg)
      }
      inputFunction = debounce(value, timeout)
      const input = findInput(el)
      el._INPUT = input
      if (input) {
        input.addEventListener('input', inputFunction)
        input.addEventListener('compositionstart', compositionStart)
        input.addEventListener('compositionend', compositionEnd)
      }
    }
  },
  beforeUnmount(el: HTMLElement) {
    if (el._INPUT) {
      el._INPUT.removeEventListener('input', inputFunction)
      el._INPUT.removeEventListener('compositionstart', compositionStart)
      el._INPUT.removeEventListener('compositionend', compositionEnd)
      el._INPUT = null
    }
  }
}

Usage example in a Vue component:

<template>
  <input v-model="val" v-debounceInput:600="onInput" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
const val = ref('')
function onInput(e: Event): void {
  console.log(e)
}
</script>

The directive is registered globally with createApp(App).directive('debounceInput', debounceInput).mount('#app') , and a test case demonstrates the clear difference between normal input and debounced input (shown via GIF). The article concludes with a summary of the covered topics.

Summary of key takeaways:

Debounce

Throttle

Vue 3 directive development

Handling compositionstart event (browser compatibility note)

Handling compositionend event (browser compatibility note)

frontendJavaScriptVueDebouncethrottleDirective
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.