防止按钮重复点击发送请求

11 阅读3分钟

一、核心思路:控制按钮点击的有效时间窗口

通过“禁用按钮+防抖/节流”双重机制,确保短时间内只触发一次请求。

二、具体实现方案

方案1:基础版 - 按钮禁用与状态管理

核心逻辑:点击后立即禁用按钮,请求完成后恢复。

<template>
  <button 
    @click="fetchData" 
    :disabled="isLoading"
    class="btn {{ isLoading ? 'btn-loading' : '' }}"
  >
    {{ isLoading ? '加载中...' : '获取数据' }}
  </button>
</template>

<script>
export default {
  data() {
    return {
      isLoading: false
    }
  },
  methods: {
    async fetchData() {
      // 若已在加载中,直接返回
      if (this.isLoading) return;
      
      this.isLoading = true;
      try {
        // 发送请求
        const response = await fetch('/api/data');
        const data = await response.json();
        // 处理数据...
      } catch (error) {
        console.error('请求失败', error);
      } finally {
        // 请求完成后恢复按钮状态
        this.isLoading = false;
      }
    }
  }
}
</script>

优势:简单直观,适合大多数场景;
注意:需在finally中恢复状态,避免请求报错导致按钮一直禁用。

方案2:进阶版 - 函数防抖(debounce)

核心逻辑:设置时间阈值,短时间内多次点击仅触发最后一次。

<template>
  <button @click="debouncedFetch">获取数据</button>
</template>

<script>
import { debounce } from 'lodash'; // 或自行实现防抖函数

export default {
  methods: {
    // 原始请求方法
    async fetchData() {
      try {
        const response = await fetch('/api/data');
        // 处理数据...
      } catch (error) {
        console.error(error);
      }
    },
    // 防抖处理后的方法
    debouncedFetch: debounce(function() {
      this.fetchData();
    }, 500) // 500ms内重复点击只触发一次
  }
}
</script>

自行实现防抖函数

function debounce(func, wait) {
  let timeout;
  return function(...args) {
    if (timeout) clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(this, args);
    }, wait);
  };
}

适用场景:搜索框联想、按钮防重复提交(需等待用户操作结束)。

方案3:高阶版 - 函数节流(throttle)

核心逻辑:设置时间窗口,保证一定时间内仅触发一次请求。

<template>
  <button @click="throttledFetch">获取数据</button>
</template>

<script>
import { throttle } from 'lodash'; // 或自行实现节流函数

export default {
  methods: {
    async fetchData() {
      // ...请求逻辑同上
    },
    // 节流处理:1秒内最多触发一次
    throttledFetch: throttle(function() {
      this.fetchData();
    }, 1000)
  }
}
</script>

自行实现节流函数(时间戳版)

function throttle(func, wait) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= wait) {
      func.apply(this, args);
      lastTime = now;
    }
  };
}

适用场景:频繁点击按钮(如点赞、刷新),需限制触发频率。

方案4:装饰器模式(Vue3组合式API)

核心逻辑:通过自定义指令或装饰器封装防重复点击逻辑。

<template>
  <button v-debounce.click="fetchData">获取数据</button>
</template>

<script setup>
import { ref } from 'vue';

// 自定义防抖指令
const vDebounce = {
  mounted(el, binding) {
    let timeout;
    el.addEventListener('click', () => {
      if (timeout) clearTimeout(timeout);
      timeout = setTimeout(() => {
        binding.value(); // 执行绑定的函数
      }, 500);
    });
  }
};

const fetchData = async () => {
  // ...请求逻辑
};
</script>

优势:解耦业务逻辑,可复用在多个按钮上。

三、问题

1. 问:防抖和节流的区别是什么?
    • 防抖(debounce):等待用户停止操作后触发,如“输入完成后搜索”;
    • 节流(throttle):控制操作频率,如“每秒最多点击一次”。
2. 问:如何处理请求超时后按钮恢复的情况?
    • 结合Promise.race设置超时时间:
    async fetchData() {
      this.isLoading = true;
      try {
        // 3秒超时
        const response = await Promise.race([
          fetch('/api/data'),
          new Promise((_, reject) => 
            setTimeout(() => reject(new Error('请求超时')), 3000)
          )
        ]);
        // ...处理数据
      } catch (error) {
        console.error(error);
      } finally {
        this.isLoading = false;
      }
    }
    
3. 问:移动端如何优化按钮点击体验?
    • 点击时添加微动画(如缩放、颜色变化),提供视觉反馈;
    • 结合touchstart事件提前禁用按钮,防止滑动时误触。

四、总结

“防止按钮重复点击的核心是控制事件触发频率:对于普通表单提交,可通过isLoading状态禁用按钮并显示加载中提示;若用户可能快速点击(如刷新按钮),推荐使用防抖(等待操作结束)或节流(限制触发频率)。在Vue中,可结合自定义指令或lodash的工具函数封装通用逻辑,避免重复代码。同时,需注意请求异常时的状态恢复,以及通过加载动画提升用户体验。”