秒学会!在vue中实现防抖的自定义指令!

78 阅读3分钟

什么是防抖、节流!

防抖(Debouncing)和节流(Throttling)是两种用于控制函数触发频率的常见前端开发技巧,它们的主要目的是减少函数被频繁触发的情况,以提高性能和用户体验。

防抖:

防抖是指在一段时间内,多次触发同一事件后,只执行一次相应的函数。具体来说,如果在规定的时间间隔内多次触发事件,那么防抖会将这些事件合并为一次,并在延迟结束后执行相应的函数。防抖常用于处理用户输入,如搜索框自动补全或搜索建议,以减少不必要的请求和响应。

举个例子:当用户在搜索框中输入文字时,防抖可以确保只在用户停止输入一段时间后才触发搜索请求,而不是在每次按键时都触发请求。

 function debounce(fn, time) {
      let timer = null
      return function (...args) {
        if (timer) {
          clearTimeout(timer)
          timer = null
        }

        timer = setTimeout(() => {
          fn(...args)
          clearTimeout(timer)
          timer = null
        }, time)
      }
    }

    const input =  document.querySelector('#kw')

    const onInput = debounce(function (e) {
      console.log(e, 'debounce')
    }, 1000)

    input.addEventListener('input', onInput)

节流:

节流是指在一段时间内,将多次触发的事件均匀分布执行相应的函数,而不会重复执行。具体来说,事件触发后,函数会执行,然后等待一段时间,在这段时间内不管事件触发多少次,都不会再次执行,只有等到这段时间结束后才会再次执行函数。节流常用于控制事件的触发频率,以避免事件处理过于频繁。

举个例子:当用户滚动页面时,节流可以确保在每隔一定时间才触发页面滚动事件的处理函数,而不是每次滚动都触发。

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

const input =  document.querySelector('#kw')

const onInput = throttle(function (e) {
  console.log(e, 'throttle')
}, 3000)

input.addEventListener('input', onInput)

如何在vue中通过自定义指令方式使用防抖呢?

  • 1、在directive目录下新建inputDirective.ts,代码如下
const InputTagName = ['INPUT', 'TEXTAREA']

function compositionStart(event: CompositionEvent & { target: { composing: boolean } }) {
  event.target.composing = true
}

function compositionEnd(event: CompositionEvent & { target: { composing: boolean } }) {
  event.target.composing = false
  // 发送一个事件出去,确保当输入法组合事件结束时,文本框内的内容能及时更新
  const e = new Event('input', { bubbles: true })
  event.target.dispatchEvent(e)
}

// 找到input元素:兼容当指令绑定到组件上时
function findInput(el: HTMLElement): HTMLElement | null {
  const queue: HTMLElement[] = []
  queue.push(el)
  while(queue.length > 0) {
    const cur = queue.shift()
    if(InputTagName.includes(cur.tagName)) {
      return cur
    }

    if(cur?.childNodes) {
      // @ts-ignore
      queue.push(...cur.childNodes)
    }
  }
  return null
}

function isFuncton(value: any) {
  return Object.prototype.toString.call(value) === '[object Function]'
}

function debounce(input: (event: Event & { target: { composing: boolean } } ) => any, time: number): (this: HTMLElement, event: Event) => any {
  // @ts-ignore
  let timer: string | number | NodeJS.Timeout | undefined
  return function(event: Event & { target: { composing: boolean } }) {

    if(event.target.composing) {
      return
    }

    if(timer) {
      clearTimeout(timer)
      timer = undefined
    }

    timer = setTimeout(() => {
      input(event)
      clearTimeout(timer)
      timer = undefined
    }, time)
  }
}


let functionDebounce : (this: HTMLElement, event: Event) => any

export default {
  mounted(el: HTMLElement & { _INPUT:  HTMLElement | null}, binding: any) {
    const { value, arg } = binding
    if(value && isFuncton(value)) {
      let timeout = 600
      if(arg && Number.isNaN(arg)) {
        timeout = Number(arg)
      }

      functionDebounce = debounce(value, timeout)
      const input = findInput(el)
      el._INPUT = input

      if(input) {
        input.addEventListener('input', functionDebounce)
        input.addEventListener('compositionstart', compositionStart)
        input.addEventListener('compositionend', compositionEnd)
      }

    }
  },
  beforeUnmount(el: HTMLElement & { _INPUT:  HTMLElement | null}) {
    if(el._INPUT) {
      el._INPUT.removeEventListener('input', functionDebounce)
      el._INPUT.removeEventListener('compositionstart', compositionStart)
      el._INPUT.removeEventListener('compositionend', compositionEnd)
      el._INPUT = null
    }
  },
}
  • 2、把自定义指令进行全局注册,在main.ts中修改如下:
const app = createApp(App)

// 全局注册
app.directive('inputDebouce', inputDebouce)

app.mount('#app')
  • 3、在组件中使用:
<template>
	    <el-input
            type="textarea"
            v-model="internalValue"
            v-inputDebouce:1000="inputDebouce"
          />
</template>

<script setup lang="ts">
  const inputDebouce = (e: Event) => {
			console.log(e)
  }
</script>