Vue3防重复点击指令clickOnce使用方法

 更新时间:2026年03月21日 11:20:13   作者:码途进化论  
使用Vue 框架开发项目中,遇到个问题,就是在用户提交数据时,如果连续多次点击按钮,会触发多次,导致数据异常,这篇文章主要介绍了Vue3防重复点击指令clickOnce使用的相关资料,需要的朋友可以参考下

一、问题背景

在实际的 Web 应用开发中,我们经常会遇到以下问题:

  1. 用户快速多次点击提交按钮:导致重复提交表单,产生多条相同数据
  2. 异步请求未完成时再次点击:可能导致数据不一致或服务器压力增大
  3. 用户体验不佳:没有明确的加载状态反馈,用户不知道操作是否正在进行

这些问题在以下场景中尤为常见:

  • 表单提交(注册、登录、创建订单等)
  • 数据保存操作
  • 文件上传
  • 支付操作
  • API 调用

二、解决方案

clickOnce 指令通过以下机制解决上述问题:

1.节流机制

使用 @vueuse/coreuseThrottleFn,在 1.5 秒内只允许执行一次点击操作。

2.按钮禁用

点击后立即禁用按钮,防止用户再次点击。

3.视觉反馈

自动添加 Element Plus 的 Loading 图标,让用户明确知道操作正在进行中。

4.智能恢复

  • 如果绑定的函数返回 Promise(异步操作),则在 Promise 完成后自动恢复按钮状态
  • 如果是同步操作,则立即恢复

三、核心特性

自动防重复点击:1.5秒节流时间

自动 Loading 状态:无需手动管理 loading 变量

支持异步操作:自动检测 Promise 并在完成后恢复

优雅的清理机制:组件卸载时自动清理事件监听

类型安全:完整的 TypeScript 支持

四、技术实现

关键技术点

  1. Vue 3 自定义指令:使用 Directive 类型定义
  2. VueUse 节流useThrottleFn 提供稳定的节流功能
  3. 动态组件渲染:使用 createVNoderender 动态创建 Loading 图标
  4. Promise 检测:自动识别异步操作并在完成后恢复状态

工作流程

用户点击按钮
    ↓
节流检查(1.5秒内只执行一次)
    ↓
禁用按钮 + 添加 Loading 图标
    ↓
执行绑定的函数
    ↓
检测返回值是否为 Promise
    ↓
Promise 完成后(或同步函数执行完)
    ↓
移除 Loading + 恢复按钮状态

五、使用方法

1. 注册指令

// main.ts
import clickOnce from '@/directives/clickOnce'

app.directive('click-once', clickOnce)

2. 在组件中使用

<template>
  <!-- 异步操作示例 -->
  <el-button 
    type="primary" 
    v-click-once="handleSubmit">
    提交表单
  </el-button>

  <!-- 带参数的异步操作 -->
  <el-button 
    type="success" 
    v-click-once="() => handleSave(formData)">
    保存数据
  </el-button>
</template>

<script setup lang="ts">
const handleSubmit = async () => {
  // 模拟 API 调用
  await api.submitForm(formData)
  ElMessage.success('提交成功')
}

const handleSave = async (data: any) => {
  await api.saveData(data)
  ElMessage.success('保存成功')
}
</script>

六、优势对比

传统方式

<template>
  <el-button 
    type="primary" 
    :loading="loading"
    :disabled="loading"
    @click="handleSubmit">
    提交
  </el-button>
</template>

<script setup lang="ts">
const loading = ref(false)

const handleSubmit = async () => {
  if (loading.value) return
  
  loading.value = true
  try {
    await api.submit()
  } finally {
    loading.value = false
  }
}
</script>

问题

  • 需要手动管理 loading 状态
  • 每个按钮都要写重复代码
  • 容易遗漏 finally 清理逻辑

使用 clickOnce 指令

<template>
  <el-button 
    type="primary" 
    v-click-once="handleSubmit">
    提交
  </el-button>
</template>

<script setup lang="ts">
const handleSubmit = async () => {
  await api.submit()
}
</script>

优势

  • 代码简洁,无需管理状态
  • 自动处理 loading 和禁用
  • 统一的用户体验

七、注意事项

  1. 仅用于异步操作:该指令主要为异步操作设计,同步操作会立即恢复
  2. 绑定函数必须返回 Promise:对于异步操作,确保函数返回 Promise
  3. 节流时间固定:当前节流时间为 1.5 秒,可根据需求调整 THROTTLE_TIME 常量
  4. 依赖 Element Plus:使用了 Element Plus 的 Loading 图标和样式

八、适用场景

适合使用

  • 表单提交按钮
  • 数据保存按钮
  • 文件上传按钮
  • API 调用按钮
  • 支付确认按钮

不适合使用

  • 普通导航按钮
  • 切换/开关按钮
  • 需要快速连续点击的场景(如计数器)

九、指令源码

import type { Directive } from 'vue'
import { createVNode, render } from 'vue'
import { useThrottleFn } from '@vueuse/core'
import { Loading } from '@element-plus/icons-vue'

const THROTTLE_TIME = 1500

const clickOnce: Directive<HTMLButtonElement, () => Promise<unknown> | void> = {
  mounted(el, binding) {
    const handleClick = useThrottleFn(
      () => {
        // 如果元素已禁用,直接返回(双重保险)
        if (el.disabled) return

        // 禁用按钮
        el.disabled = true
        // 添加 loading 状态
        el.classList.add('is-loading')

        // 创建 loading 图标容器
        const loadingIconContainer = document.createElement('i')
        loadingIconContainer.className = 'el-icon is-loading'

        // 使用 Vue 的 createVNode 和 render 来渲染 Loading 组件
        const vnode = createVNode(Loading)
        render(vnode, loadingIconContainer)

        // 将 loading 图标插入到按钮开头
        el.insertBefore(loadingIconContainer, el.firstChild)

        // 将 loading 图标存储到元素上,以便后续移除
        ;(el as any)._loadingIcon = loadingIconContainer
        ;(el as any)._loadingVNode = vnode

        // 执行绑定的函数(应返回 Promise 或普通函数)
        const result = binding.value?.()

        const removeLoading = () => {
          el.disabled = false
          // 移除 loading 状态
          el.classList.remove('is-loading')
          const icon = (el as any)._loadingIcon
          if (icon && icon.parentNode === el) {
            // 卸载 Vue 组件
            render(null, icon)
            el.removeChild(icon)
            delete (el as any)._loadingIcon
            delete (el as any)._loadingVNode
          }
        }

        // 如果返回的是 Promise,则在完成时恢复;否则立即恢复
        if (result instanceof Promise) {
          result.finally(removeLoading)
        } else {
          // 非异步操作,立即恢复(或根据需求决定是否恢复)
          // 通常建议只用于异步操作,所以这里也可以不处理,或给出警告
          removeLoading()
        }
      },
      THROTTLE_TIME,
    )

    // 将 throttled 函数存储到元素上,以便在 unmount 时移除
    ;(el as any)._throttledClick = handleClick
    el.addEventListener('click', handleClick)
  },

  beforeUnmount(el) {
    const handleClick = (el as any)._throttledClick
    if (handleClick) {
      el.removeEventListener('click', handleClick)
      // 取消可能还在等待的 throttle
      handleClick.cancel?.()
      delete (el as any)._throttledClick
    }
  },
}

export default clickOnce

十、总结

clickOnce 指令通过封装防重复点击逻辑,提供了一个开箱即用的解决方案,让开发者可以专注于业务逻辑,而不用担心重复点击的问题。它结合了节流、状态管理和视觉反馈,为用户提供了更好的交互体验。

到此这篇关于Vue3防重复点击指令clickOnce使用方法的文章就介绍到这了,更多相关Vue3防重复点击指令clickOnce内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Vue使用Cropper实现图片裁剪功能

    Vue使用Cropper实现图片裁剪功能

    图片裁剪功能无论是用户头像的裁剪,还是图片内容的精确调整,都成为了提升用户体验的关键一环,本文将详细介绍如何在Vue.js项目中集成并使用Cropper.js实现一个强大的图片裁剪组件,需要的可以参考下
    2024-11-11
  • Vue.js axios响应拦截如何获取返回状态码

    Vue.js axios响应拦截如何获取返回状态码

    这篇文章主要介绍了Vue.js axios响应拦截如何获取返回状态码问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-01-01
  • vue如何动态修改meta的title

    vue如何动态修改meta的title

    这篇文章主要介绍了vue如何动态修改meta的title,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-06-06
  • Vue多层数据结构响应式失效,视图更新失败问题

    Vue多层数据结构响应式失效,视图更新失败问题

    这篇文章主要介绍了Vue多层数据结构响应式失效,视图更新失败问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-02-02
  • vue3项目如何配置按需自动导入API组件unplugin-auto-import

    vue3项目如何配置按需自动导入API组件unplugin-auto-import

    这篇文章主要介绍了vue3项目如何配置按需自动导入API组件unplugin-auto-import问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-03-03
  • 前端Vue自定义地址展示地址选择地址管理组件的示例详解

    前端Vue自定义地址展示地址选择地址管理组件的示例详解

    这篇文章主要介绍了前端Vue自定义地址展示地址选择地址管理组件的示例详解,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-06-06
  • vue限制输入数字或者保留两位小数实现

    vue限制输入数字或者保留两位小数实现

    这篇文章主要为大家介绍了vue限制输入数字或者保留两位小数实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-07-07
  • Vue+Router+Element实现简易导航栏

    Vue+Router+Element实现简易导航栏

    这篇文章主要为大家详细介绍了Vue+Router+Element实现简易导航栏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-09-09
  • Vue实现点击箭头上下移动效果

    Vue实现点击箭头上下移动效果

    这篇文章主要介绍了Vue实现点击箭头上下移动效果,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-06-06
  • Vue3进行路由管理的示例代码

    Vue3进行路由管理的示例代码

    在现代 Web 开发中,前端路由管理是构建单页面应用(SPA)的关键组成部分,随着 Vue3 的发布,Vue Router 也得到了相应的更新,在这篇博文中,我们将详细探讨 Vue3 中的路由管理,需要的朋友可以参考下
    2025-01-01

最新评论