我正在参与「启航方案」

大家好,我是Lvzl, 一个三年工作经验的前端小菜鸡,在渠道分享一些 平时学习的感悟 & 实际项目场景 的文章。

前言

「查找」这个场景在各种业务的系统中都是是十分常见的,比方电商渠道的商品查找、百度查找、查找、google查找……,只要是有内容呈现给用户的,都少不了查找功用。

按照触发查找动作的不同可分为:

  1. 输完关键字后手动触发查找
  2. 输入字符主动查找

第一种是由用户去决定何时进行查找,没啥问题。

第二种是经过监听用户录入的事情主动查找,主动查找能在一定程度上提升用户体会(比方在用户输入时关键词联想提示),同时也可能会带来问题,因为input事情触发是十分频频的,比方下面这个示例:

翻开百度的查找,找到输入框的dom,然后绑定一个input事情,看看咱们录入一个telephone会触发多少次input事情:

temp1.addEventListener('oninput', (val) => console.log(val))

触发了10次:

Vue3指令——查找框输入防抖完成
那假如每触发一次都会调用后台接口查询,那当用户量足够大,就会给后台服务器带来很大的压力,导致接口呼应慢,甚至引起服务器宕机,也会给用户带来欠好的体会。

为了不引起过多恳求给服务端形成压力,咱们希望在用户在输入完以后再建议恳求,那怎么判别用户是否输入完了❓

能够认为当用户在一个固定的时刻内都没有录入字符是输入完成/暂时输入完成。这个是陈词滥调的常识点了————「防抖」和「节省」。有很多文章专门写这个的,笔者在此不过多描述,简略一笔带过说下笔者的观点。

防抖 & 节省

防抖

防抖的函数在调用后,假如在防抖时刻内没有再次触发,就会在过了 防抖时刻 后履行;假如在防抖时刻内再次触发,不管触发多少次,履行的时机会一向往后推迟,直到满足最后一次调用时刻 小于 当时时刻减去防抖时刻才会履行。

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

节省

节省函数在调用后,在设置的节省时刻后履行一次,在此期间,不管触发多少次,都直接return了。

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)
  }
}

再来看下加了防抖 & 节省 处理的input事情履行情况:

  • 防抖:

    Vue3指令——查找框输入防抖完成

  • 防抖 + 节省:

    Vue3指令——查找框输入防抖完成

从上面的两个图不难看出防抖 和节省的区别,节省的函数假如一向被触发,每隔一段时刻履行一次。而防抖则是一向推迟,最后履行一次。

输入防抖存在的问题

上面的防抖函数履行貌似没啥问题,前提是咱们输入的是英文,假如是中文输入就出问题了,看下图:

Vue3指令——查找框输入防抖完成

当中文输入的时候,即使现已防抖处理了,还可能会履行屡次。接下来咱们进入正题了,怎么完成输入防抖Vue3指令(并处理中文输入触发屡次的问题)。

指令完成

为什么要以指令的方法完成❓我能想到的有以下两点:

  1. 将防抖处理的逻辑封装在指令内部重用,更易用。
  2. 指令要支撑input输入框,也要支撑封装input的组件,也就是说需求进行底层 DOM 拜访的逻辑,而这一点恰好是指令提供的才能之一。

需求弥补Vue 指令相关常识的同学点这里自定义指令

希望运用方法:

<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>

即指令的绑定值为 履行逻辑,指令参数为 防抖时长。

指令完成:

import { debounce, isFunction } from '../../utils/index'
let inputFunction: (event: Event) => {}
// 因为要同时支撑 input 输入框和封装 input 的组件,因此需求去找到input这个元素。
function findInput(el: HTMLElement): HTMLElement | null {
  const quene: HTMLElement[] = []
  quene.push(el)
  while (quene.length > 0) {
    const current = quene.shift()
    if (current?.tagName === 'INPUT') {
      return current
    }
    if (current?.childNodes) {
      quene.push(...current.childNodes)
    }
  }
  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)
      }
    }
  },
  beforeUnmount(el: HTMLElement) {
    if (el._INPUT) {
      el._INPUT.removeEventListener('input', inputFunction)
      el._INPUT = null
    }
  }
}

接着处理中文输入可能会触发屡次的问题。可凭借compositionstart和compositionend来完成。

  • 输入法编辑器开端新的输入组成时会触发compositionstart 事情。例如,当用户运用拼音输入法开端输入汉字时,这个事情就会被触发。
  • 当文本阶段的组成完成或取消时,compositionend 事情将被触发 (具有特殊字符的触发,需求一系列键和其他输入,如语音识别或移动中的字词建议)。

在一开端拼音输入法时就设置composing为true,因此在compositionEnd没有调用之前都不会履行函数,等compositionEnd调用,经过dispatchEvent派发一个input事情出去,确保逻辑一定会履行。

function compositionStart(event: CompositionEvent) {
  event.target.composing = true
}
function compositionEnd(e: CompositionEvent) {
  e.target.composing = false
  const event = new Event('input', { bubbles: true })
  e.target?.dispatchEvent(event)
}
export default {
  mounted(el: HTMLElement, binding: any) {
    //...
      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)
    }
  }
}

防抖完成 & 判别是 function

export function debounce(input: (event: Event) => any, timeout: number): (this: HTMLElement, ev: Event) => any {
  let timer: string | number | NodeJS.Timeout | undefined
  return (event: Event) => {
    // @ts-ignore
    if (event.target.composing === true) {
      return
    }
    if (timer) {
      clearTimeout(timer)
      timer = undefined
    }
    timer = setTimeout(() => {
      input(event)
      clearTimeout(timer)
      timer = undefined
    }, timeout)
  }
}
export function isFunction(param: any): boolean {
  return Object.prototype.toString.call(param) === '[object Function]'
}

注册指令(全局注册):

createApp(App).directive('debounceInput', debounceInput).mount('#app')

写个事例测试一下:

<template>
  <div>防抖</div>
  <input v-model="val" v-debounceInput="onInput" />
  <div>普通</div>
  <input v-model="count" type="text" @input="onInput" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
const val = ref('')
const count = ref('')
function onInput(e: Event): void {
  console.log('调用了onInput')
}
</script>

看下最终的效果:

Vue3指令——查找框输入防抖完成

不同还是十分明显的。指令已发布npm仓库,有需求可自取。

总结

感谢阅读,经过本文你能够了解到以下内容:

  1. 防抖
  2. 节省
  3. Vue3指令开发
  4. compositionstart 事情 (需求留意阅读器兼容性
  5. compositionend 事情(需求留意阅读器兼容性)