vue前端面试题——记录一次面试当中遇到的题(5)

目录

1.请求响应慢,怎么处理?

一、诊断分析阶段

二、前端层面优化

三、网络层面优化

四、服务端协作优化

五、用户体验优化

六、监控与告警

实际项目案例

面试回答要点总结

2.怎么防止用户重复点击?

一、UI层面防护(最直观的用户反馈)

二、请求层面拦截(核心防护)

三、业务层面防护(精细化控制)

四、高级防护方案

五、实际项目中的综合方案

面试回答要点总结


1.请求响应慢,怎么处理?

请求响应慢是一个综合性的性能问题,我会从前端、网络、服务端三个层面进行系统化分析和优化。首先需要通过监控工具定位瓶颈,然后针对性地实施优化措施。

一、诊断分析阶段

1. 性能监控与问题定位

**使用浏览器开发者工具分析

  • Network 面板:查看请求耗时、排队时间、TTFB
  • Performance 面板:分析主线程活动和长任务
  • Lighthouse:获取性能评分和建议**
javascript 复制代码
// 1. 使用浏览器开发者工具分析
// - Network 面板:查看请求耗时、排队时间、TTFB
// - Performance 面板:分析主线程活动和长任务
// - Lighthouse:获取性能评分和建议

// 2. 自定义性能监控
const requestStartTime = Date.now();

fetch('/api/data')
  .then(response => {
    const timings = {
      total: Date.now() - requestStartTime,
      dns: 0, // 可通过 Performance API 获取详细时序
      tcp: 0,
      ttfb: 0
    };
    
    // 上报性能数据
    this.reportPerformance(timings);
  });

// 3. 关键性能指标
const performanceMetrics = {
  TTFB: 'Time to First Byte',     // 首字节时间
  FCP: 'First Contentful Paint',  // 首次内容绘制
  LCP: 'Largest Contentful Paint' // 最大内容绘制
};

2. 问题分类诊断

javascript 复制代码
// 判断问题类型
function diagnoseSlowRequest(metrics) {
  if (metrics.TTFB > 1000) {
    return '服务端处理慢或网络延迟高';
  }
  
  if (metrics.downloadTime > 3000) {
    return '响应数据过大或网络带宽低';
  }
  
  if (metrics.queueingTime > 500) {
    return '浏览器请求队列阻塞';
  }
  
  return '需要进一步分析';
}
二、前端层面优化

1. 请求合并与减少

javascript 复制代码
// 1.1 请求合并
class RequestBatcher {
  constructor() {
    this.batch = new Map();
    this.timer = null;
  }
  
  addRequest(key, request) {
    this.batch.set(key, request);
    
    if (!this.timer) {
      this.timer = setTimeout(() => this.flush(), 50);
    }
  }
  
  async flush() {
    const requests = Array.from(this.batch.values());
    
    // 合并多个请求为一个批量请求
    const batchResponse = await fetch('/api/batch', {
      method: 'POST',
      body: JSON.stringify({ requests })
    });
    
    this.batch.clear();
    this.timer = null;
  }
}

// 1.2 避免重复请求
const requestCache = new Map();

async function cachedRequest(url, options) {
  const cacheKey = JSON.stringify({ url, options });
  
  if (requestCache.has(cacheKey)) {
    return requestCache.get(cacheKey);
  }
  
  const promise = fetch(url, options).then(response => {
    // 缓存成功响应
    if (response.ok) {
      requestCache.set(cacheKey, response.clone());
    }
    return response;
  });
  
  requestCache.set(cacheKey, promise);
  return promise;
}

2. 请求优化策略

javascript 复制代码
// 2.1 请求优先级管理
class RequestScheduler {
  constructor(maxConcurrent = 6) {
    this.maxConcurrent = maxConcurrent;
    this.activeCount = 0;
    this.queue = [];
  }
  
  async schedule(request, priority = 'normal') {
    return new Promise((resolve, reject) => {
      const task = { request, resolve, reject, priority };
      
      // 按优先级插入队列
      this.insertByPriority(task);
      
      this.processQueue();
    });
  }
  
  insertByPriority(task) {
    const priorities = { high: 0, normal: 1, low: 2 };
    const taskPriority = priorities[task.priority];
    
    let index = this.queue.findIndex(item => 
      priorities[item.priority] > taskPriority
    );
    
    if (index === -1) index = this.queue.length;
    this.queue.splice(index, 0, task);
  }
  
  async processQueue() {
    if (this.activeCount >= this.maxConcurrent || this.queue.length === 0) {
      return;
    }
    
    this.activeCount++;
    const task = this.queue.shift();
    
    try {
      const result = await task.request();
      task.resolve(result);
    } catch (error) {
      task.reject(error);
    } finally {
      this.activeCount--;
      this.processQueue();
    }
  }
}

// 2.2 使用 Web Workers 处理复杂计算
// main.js
const worker = new Worker('request-worker.js');

worker.postMessage({
  type: 'API_REQUEST',
  payload: { url: '/api/complex-data' }
});

worker.onmessage = (event) => {
  if (event.data.type === 'RESPONSE') {
    this.processData(event.data.payload);
  }
};

// request-worker.js
self.onmessage = async (event) => {
  if (event.data.type === 'API_REQUEST') {
    const response = await fetch(event.data.payload.url);
    const data = await response.json();
    
    // 在 Worker 中进行数据处理,不阻塞主线程
    const processedData = processDataInWorker(data);
    
    self.postMessage({
      type: 'RESPONSE',
      payload: processedData
    });
  }
};

3. 缓存策略优化

javascript 复制代码
// 3.1 多级缓存策略
class CacheManager {
  constructor() {
    this.memoryCache = new Map();
    this.localStorageKey = 'api-cache';
  }
  
  async getWithCache(url, options = {}) {
    const { ttl = 300000 } = options; // 默认5分钟
    
    // 1. 内存缓存(最快)
    const memoryKey = this.generateKey(url, options);
    if (this.memoryCache.has(memoryKey)) {
      const cached = this.memoryCache.get(memoryKey);
      if (Date.now() - cached.timestamp < ttl) {
        return cached.data;
      }
    }
    
    // 2. localStorage 缓存
    const storageCache = this.getStorageCache();
    const storageKey = this.generateKey(url, options);
    if (storageCache[storageKey] && 
        Date.now() - storageCache[storageKey].timestamp < ttl) {
      
      // 回填内存缓存
      this.memoryCache.set(memoryKey, storageCache[storageKey]);
      return storageCache[storageKey].data;
    }
    
    // 3. 网络请求
    try {
      const response = await fetch(url, options);
      const data = await response.json();
      
      const cacheItem = {
        data,
        timestamp: Date.now()
      };
      
      // 更新缓存
      this.memoryCache.set(memoryKey, cacheItem);
      this.updateStorageCache(storageKey, cacheItem);
      
      return data;
    } catch (error) {
      // 返回过期的缓存数据(如果有)
      if (storageCache[storageKey]) {
        return storageCache[storageKey].data;
      }
      throw error;
    }
  }
  
  generateKey(url, options) {
    return btoa(JSON.stringify({ url, options }));
  }
}
三、网络层面优化

1. HTTP/2 与连接优化

javascript 复制代码
// 1.1 HTTP/2 多路复用
// 服务器配置 HTTP/2,前端无需特殊处理
// 但可以优化资源加载顺序

// 1.2 DNS 预解析
const dnsPrefetchLinks = [
  '//api.example.com',
  '//cdn.example.com'
];

dnsPrefetchLinks.forEach(domain => {
  const link = document.createElement('link');
  link.rel = 'dns-prefetch';
  link.href = domain;
  document.head.appendChild(link);
});

// 1.3 预连接
const preconnectLinks = [
  'https://api.example.com'
];

preconnectLinks.forEach(domain => {
  const link = document.createElement('link');
  link.rel = 'preconnect';
  link.href = domain;
  document.head.appendChild(link);
});

2. 数据压缩与传输优化

javascript 复制代码
// 2.1 请求数据压缩
async function sendCompressedRequest(url, data) {
  // 压缩请求体
  const compressedData = pako.gzip(JSON.stringify(data));
  
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Content-Encoding': 'gzip'
    },
    body: compressedData
  });
  
  return response;
}

// 2.2 响应数据流式处理
async function streamLargeResponse(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  
  while (true) {
    const { done, value } = await reader.read();
    
    if (done) break;
    
    // 分批处理数据,避免阻塞
    const chunk = decoder.decode(value, { stream: true });
    this.processChunk(chunk);
  }
}

// 2.3 数据分页与懒加载
class PaginatedLoader {
  constructor(pageSize = 20) {
    this.pageSize = pageSize;
    this.currentPage = 0;
    this.hasMore = true;
  }
  
  async loadNextPage() {
    if (!this.hasMore) return;
    
    const response = await fetch(`/api/data?page=${this.currentPage}&size=${this.pageSize}`);
    const data = await response.json();
    
    this.currentPage++;
    this.hasMore = data.hasMore;
    
    return data.items;
  }
}
四、服务端协作优化

1. API 设计优化建议

javascript 复制代码
// 1.1 GraphQL 精确获取数据
const query = `
  query GetUserData($id: ID!) {
    user(id: $id) {
      name
      email
      posts(limit: 5) {
        title
        createdAt
      }
    }
  }
`;

// 1.2 批量接口设计
const batchRequest = {
  requests: [
    { path: '/users/1', method: 'GET' },
    { path: '/posts/recent', method: 'GET' },
    { path: '/notifications', method: 'GET' }
  ]
};

// 1.3 增量更新接口
const incrementalUpdate = {
  since: '2024-01-01T00:00:00Z',
  types: ['users', 'posts', 'comments']
};

2. 缓存头优化

javascript 复制代码
// 建议服务端设置合适的缓存头
const optimalCacheHeaders = {
  // 静态资源:长期缓存
  'Cache-Control': 'public, max-age=31536000, immutable',
  
  // 动态数据:短期缓存
  'Cache-Control': 'private, max-age=300', // 5分钟
  
  // 实时数据:不缓存
  'Cache-Control': 'no-cache, no-store'
};

// 前端检查缓存状态
function checkCacheStatus(response) {
  const cacheHeader = response.headers.get('Cache-Control');
  
  if (cacheHeader && cacheHeader.includes('max-age')) {
    const maxAge = parseInt(cacheHeader.match(/max-age=(\d+)/)[1]);
    console.log(`数据缓存时间: ${maxAge}秒`);
  }
}
五、用户体验优化

1. 加载状态管理

html 复制代码
<template>
  <div>
    <!-- 骨架屏 -->
    <div v-if="loading" class="skeleton-container">
      <div class="skeleton-item" v-for="n in 5" :key="n"></div>
    </div>
    
    <!-- 实际内容 -->
    <div v-else>
      <div v-for="item in data" :key="item.id" class="data-item">
        {{ item.name }}
      </div>
    </div>
    
    <!-- 加载更多 -->
    <button 
      v-if="hasMore && !loading" 
      @click="loadMore"
      :disabled="loadingMore"
    >
      {{ loadingMore ? '加载中...' : '加载更多' }}
    </button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      loading: false,
      loadingMore: false,
      data: [],
      hasMore: true
    }
  },
  
  methods: {
    async loadData() {
      this.loading = true;
      
      try {
        // 设置超时
        const timeoutPromise = new Promise((_, reject) => 
          setTimeout(() => reject(new Error('请求超时')), 10000)
        );
        
        const dataPromise = this.fetchData();
        this.data = await Promise.race([dataPromise, timeoutPromise]);
      } catch (error) {
        this.handleError(error);
      } finally {
        this.loading = false;
      }
    },
    
    handleError(error) {
      if (error.message === '请求超时') {
        this.showToast('请求超时,请重试');
      } else {
        this.showToast('加载失败');
      }
      
      // 重试逻辑
      if (this.retryCount < 3) {
        setTimeout(() => this.loadData(), 2000);
        this.retryCount++;
      }
    }
  }
}
</script>

2. 智能重试与降级

javascript 复制代码
// 2.1 指数退避重试
class RetryableRequest {
  constructor(maxRetries = 3, baseDelay = 1000) {
    this.maxRetries = maxRetries;
    this.baseDelay = baseDelay;
  }
  
  async execute(requestFn) {
    let lastError;
    
    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        return await requestFn();
      } catch (error) {
        lastError = error;
        
        // 非重试错误
        if (error.status && [400, 401, 403, 404].includes(error.status)) {
          throw error;
        }
        
        if (attempt === this.maxRetries) break;
        
        // 指数退避
        const delay = this.baseDelay * Math.pow(2, attempt);
        await this.sleep(delay + Math.random() * 1000);
      }
    }
    
    throw lastError;
  }
  
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// 2.2 服务降级策略
class FallbackStrategy {
  constructor() {
    this.fallbacks = new Map();
  }
  
  register(service, fallbackFn) {
    this.fallbacks.set(service, fallbackFn);
  }
  
  async executeWithFallback(service, mainFn) {
    try {
      return await mainFn();
    } catch (error) {
      console.warn(`服务 ${service} 失败,使用降级方案`);
      
      const fallbackFn = this.fallbacks.get(service);
      if (fallbackFn) {
        return await fallbackFn();
      }
      
      throw error;
    }
  }
}

// 使用示例
const fallback = new FallbackStrategy();

// 注册降级方案
fallback.register('user-api', async () => {
  // 返回本地缓存或默认数据
  return localStorage.getItem('cached-users') || [];
});

// 执行请求
const userData = await fallback.executeWithFallback(
  'user-api',
  () => fetch('/api/users')
);
六、监控与告警

1. 性能数据收集

javascript 复制代码
// 性能监控SDK
class PerformanceMonitor {
  constructor() {
    this.metrics = [];
    this.reportUrl = '/api/performance';
  }
  
  recordRequest(url, startTime, endTime, success) {
    const duration = endTime - startTime;
    
    this.metrics.push({
      url,
      duration,
      success,
      timestamp: new Date().toISOString()
    });
    
    // 批量上报
    if (this.metrics.length >= 10) {
      this.report();
    }
  }
  
  async report() {
    if (this.metrics.length === 0) return;
    
    try {
      await fetch(this.reportUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ metrics: this.metrics })
      });
      
      this.metrics = [];
    } catch (error) {
      console.warn('性能数据上报失败:', error);
    }
  }
  
  // 慢请求告警
  checkSlowRequests() {
    const slowThreshold = 3000; // 3秒
    
    this.metrics.forEach(metric => {
      if (metric.duration > slowThreshold) {
        this.alertSlowRequest(metric);
      }
    });
  }
}
实际项目案例

"在我负责的电商项目中,我们遇到了商品列表API响应慢的问题:

问题分析:

  • TTFB平均2.8秒,95分位值达到5秒

  • 并发请求过多导致浏览器队列阻塞

  • 服务端数据库查询未优化

优化措施:

javascript 复制代码
// 1. 前端请求优化
const optimizedStrategy = {
  // 请求合并
  batchRequests: true,
  // 数据分页
  pagination: { pageSize: 20 },
  // 缓存策略
  cache: { ttl: 60000 },
  // 优先级调度
  priority: { visible: 'high', background: 'low' }
};

// 2. 与服务端协作
const apiOptimizations = {
  // 添加查询索引
  addedIndexes: ['category', 'price', 'created_at'],
  // 引入Redis缓存
  cacheLayer: 'redis',
  // 数据库读写分离
  readReplicas: true
};
面试回答要点总结

"总结请求响应慢的优化方案:

  1. 诊断先行:使用专业工具定位性能瓶颈

  2. 前端优化:请求合并、缓存、优先级调度、Web Workers

  3. 网络优化:HTTP/2、预连接、数据压缩、CDN

  4. 服务端协作:API设计优化、缓存策略、数据库优化

  5. 用户体验:骨架屏、智能重试、服务降级

  6. 监控告警:性能数据收集、慢请求监控

2.怎么防止用户重复点击?

防止用户重复点击是一个常见的用户体验优化需求,我会根据不同的业务场景采用分层级的解决方案,主要从UI层防抖、请求层拦截、业务层防护三个层面来设计

一、UI层面防护(最直观的用户反馈)

1. 按钮状态控制

javascript 复制代码
<template>
  <button 
    :class="['submit-btn', { 'loading': loading, 'disabled': disabled }]"
    :disabled="loading || disabled"
    @click="handleSubmit"
  >
    <span v-if="loading">
      <i class="loading-spinner"></i>
      提交中...
    </span>
    <span v-else>
      {{ buttonText }}
    </span>
  </button>
</template>

<script>
export default {
  data() {
    return {
      loading: false,
      disabled: false
    }
  },
  
  methods: {
    async handleSubmit() {
      if (this.loading) return
      
      this.loading = true
      try {
        await this.submitForm()
        // 提交成功后可以暂时禁用按钮,防止连续提交
        this.disabled = true
        setTimeout(() => {
          this.disabled = false
        }, 2000) // 2秒后恢复
      } catch (error) {
        console.error('提交失败:', error)
      } finally {
        this.loading = false
      }
    },
    
    async submitForm() {
      // 模拟API请求
      return new Promise(resolve => {
        setTimeout(resolve, 1000)
      })
    }
  }
}
</script>

<style scoped>
.submit-btn {
  padding: 12px 24px;
  border: none;
  border-radius: 6px;
  background: #1890ff;
  color: white;
  cursor: pointer;
  transition: all 0.3s;
}

.submit-btn:hover:not(.disabled) {
  background: #40a9ff;
}

.submit-btn.loading {
  background: #91d5ff;
  cursor: not-allowed;
}

.submit-btn.disabled {
  background: #d9d9d9;
  cursor: not-allowed;
}

.loading-spinner {
  display: inline-block;
  width: 12px;
  height: 12px;
  border: 2px solid transparent;
  border-top: 2px solid white;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin-right: 8px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
</style>

2. 防抖函数封装

javascript 复制代码
// utils/debounce.js
export function debounce(func, wait, immediate = false) {
  let timeout, result
  
  const debounced = function(...args) {
    const context = this
    
    if (timeout) clearTimeout(timeout)
    
    if (immediate) {
      // 立即执行版本
      const callNow = !timeout
      timeout = setTimeout(() => {
        timeout = null
      }, wait)
      if (callNow) result = func.apply(context, args)
    } else {
      // 延迟执行版本
      timeout = setTimeout(() => {
        func.apply(context, args)
      }, wait)
    }
    
    return result
  }
  
  debounced.cancel = function() {
    clearTimeout(timeout)
    timeout = null
  }
  
  return debounced
}

// 在组件中使用
import { debounce } from '@/utils/debounce'

export default {
  methods: {
    // 防抖处理,500ms内只能点击一次
    handleClick: debounce(function() {
      this.submitData()
    }, 500),
    
    // 立即执行版本,先执行然后冷却
    handleInstantClick: debounce(function() {
      this.submitData()
    }, 1000, true)
  }
}
二、请求层面拦截(核心防护)

1. 请求锁机制

javascript 复制代码
// utils/request-lock.js
class RequestLock {
  constructor() {
    this.pendingRequests = new Map()
  }
  
  // 生成请求唯一标识
  generateKey(config) {
    const { method, url, data, params } = config
    return JSON.stringify({ method, url, data, params })
  }
  
  // 添加请求到等待队列
  add(config) {
    const key = this.generateKey(config)
    if (this.pendingRequests.has(key)) {
      return false // 请求已存在,拒绝重复请求
    }
    
    this.pendingRequests.set(key, true)
    return true
  }
  
  // 移除请求
  remove(config) {
    const key = this.generateKey(config)
    this.pendingRequests.delete(key)
  }
  
  // 清空所有请求
  clear() {
    this.pendingRequests.clear()
  }
}

export const requestLock = new RequestLock()

// 在请求拦截器中使用
import axios from 'axios'
import { requestLock } from '@/utils/request-lock'

// 请求拦截器
axios.interceptors.request.use(config => {
  // 检查是否是重复请求
  if (!requestLock.add(config)) {
    // 如果是重复请求,取消本次请求
    return Promise.reject(new Error('重复请求已被取消'))
  }
  
  return config
})

// 响应拦截器
axios.interceptors.response.use(
  response => {
    // 请求完成,从等待队列移除
    requestLock.remove(response.config)
    return response
  },
  error => {
    // 请求失败,也从等待队列移除
    if (error.config) {
      requestLock.remove(error.config)
    }
    return Promise.reject(error)
  }
)

2. 基于 Promise 的请求锁

javascript 复制代码
// utils/promise-lock.js
class PromiseLock {
  constructor() {
    this.locks = new Map()
  }
  
  async acquire(key) {
    // 如果已经有相同的请求在进行,返回它的 Promise
    if (this.locks.has(key)) {
      return this.locks.get(key)
    }
    
    // 创建新的 Promise 并存储
    let resolveLock
    const lockPromise = new Promise(resolve => {
      resolveLock = resolve
    })
    
    this.locks.set(key, lockPromise)
    
    // 返回一个释放锁的函数
    return () => {
      resolveLock()
      this.locks.delete(key)
    }
  }
  
  // 直接执行带锁的函数
  async execute(key, asyncFunction) {
    const release = await this.acquire(key)
    
    try {
      const result = await asyncFunction()
      return result
    } finally {
      release()
    }
  }
}

export const promiseLock = new PromiseLock()

// 在业务代码中使用
import { promiseLock } from '@/utils/promise-lock'

export default {
  methods: {
    async submitOrder() {
      try {
        const result = await promiseLock.execute(
          'submit-order', // 锁的key
          () => this.api.submitOrder(this.orderData)
        )
        this.$message.success('下单成功')
        return result
      } catch (error) {
        if (error.message.includes('锁已存在')) {
          this.$message.warning('请求正在处理中,请勿重复点击')
        } else {
          this.$message.error('下单失败')
        }
        throw error
      }
    }
  }
}
三、业务层面防护(精细化控制)

1. 页面级防护

javascript 复制代码
<template>
  <div class="page-container">
    <!-- 全局加载遮罩 -->
    <div v-if="pageLoading" class="global-loading">
      <div class="loading-content">
        <i class="loading-icon"></i>
        <p>处理中,请稍候...</p>
      </div>
    </div>
    
    <form @submit.prevent="handleGlobalSubmit">
      <!-- 表单内容 -->
      <button type="submit">提交</button>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      pageLoading: false,
      lastSubmitTime: 0,
      submitCooldown: 3000 // 3秒冷却
    }
  },
  
  methods: {
    async handleGlobalSubmit() {
      const now = Date.now()
      
      // 检查冷却时间
      if (now - this.lastSubmitTime < this.submitCooldown) {
        this.$message.warning(`操作过于频繁,请${Math.ceil((this.submitCooldown - (now - this.lastSubmitTime)) / 1000)}秒后再试`)
        return
      }
      
      this.pageLoading = true
      this.lastSubmitTime = now
      
      try {
        await this.submitAllData()
        this.$message.success('提交成功')
      } catch (error) {
        console.error('提交失败:', error)
        this.lastSubmitTime = 0 // 失败时重置冷却时间
      } finally {
        this.pageLoading = false
      }
    }
  }
}
</script>

2. 路由级防护

javascript 复制代码
// utils/navigation-guard.js
import { requestLock } from './request-lock'

let isNavigating = false

export const navigationGuard = {
  install(Vue, options) {
    // 路由跳转前检查
    Vue.mixin({
      beforeRouteLeave(to, from, next) {
        // 如果有请求正在进行,提示用户
        if (requestLock.pendingRequests.size > 0) {
          if (confirm('当前有请求正在处理,确定要离开吗?')) {
            requestLock.clear() // 清空所有请求
            next()
          } else {
            next(false)
          }
        } else {
          next()
        }
      }
    })
    
    // 防止重复导航
    const originalPush = VueRouter.prototype.push
    VueRouter.prototype.push = function push(location) {
      if (isNavigating) {
        return Promise.reject(new Error('导航进行中'))
      }
      
      isNavigating = true
      return originalPush.call(this, location).finally(() => {
        isNavigating = false
      })
    }
  }
}

// 在main.js中使用
import { navigationGuard } from '@/utils/navigation-guard'
Vue.use(navigationGuard)
四、高级防护方案

1. 指令式防护

javascript 复制代码
// directives/click-once.js
export const clickOnce = {
  bind(el, binding, vnode) {
    let executed = false
    let loading = false
    
    const handler = async function(event) {
      if (executed || loading) {
        event.preventDefault()
        event.stopPropagation()
        return
      }
      
      const { value } = binding
      
      if (typeof value === 'function') {
        loading = true
        el.classList.add('click-once-loading')
        
        try {
          await value.call(vnode.context, event)
          executed = true
          el.classList.add('click-once-executed')
        } catch (error) {
          console.error('点击执行失败:', error)
        } finally {
          loading = false
          el.classList.remove('click-once-loading')
        }
      }
    }
    
    el._clickOnceHandler = handler
    el.addEventListener('click', handler)
  },
  
  unbind(el) {
    if (el._clickOnceHandler) {
      el.removeEventListener('click', el._clickOnceHandler)
    }
  }
}

// 全局注册
import Vue from 'vue'
import { clickOnce } from '@/directives/click-once'
Vue.directive('click-once', clickOnce)

// 在模板中使用
<template>
  <button v-click-once="handlePayment">支付</button>
</template>

2. 组合式API防护(Vue3)

javascript 复制代码
// composables/usePreventDuplicateClick.js
import { ref, onUnmounted } from 'vue'

export function usePreventDuplicateClick(options = {}) {
  const {
    cooldown = 1000,
    onDuplicate,
    onCooldown
  } = options
  
  const isSubmitting = ref(false)
  const lastSubmitTime = ref(0)
  const timers = []
  
  const execute = async (asyncFunction) => {
    const now = Date.now()
    
    // 检查冷却时间
    if (now - lastSubmitTime.value < cooldown) {
      onCooldown?.(Math.ceil((cooldown - (now - lastSubmitTime.value)) / 1000))
      return
    }
    
    // 检查是否正在提交
    if (isSubmitting.value) {
      onDuplicate?.()
      return
    }
    
    isSubmitting.value = true
    lastSubmitTime.value = now
    
    try {
      const result = await asyncFunction()
      return result
    } finally {
      isSubmitting.value = false
    }
  }
  
  const withLoading = async (asyncFunction) => {
    return execute(asyncFunction)
  }
  
  // 清理定时器
  onUnmounted(() => {
    timers.forEach(timer => clearTimeout(timer))
  })
  
  return {
    isSubmitting,
    execute,
    withLoading
  }
}

// 在Vue3组件中使用
<script setup>
import { usePreventDuplicateClick } from '@/composables/usePreventDuplicateClick'

const { isSubmitting, execute } = usePreventDuplicateClick({
  cooldown: 2000,
  onDuplicate: () => {
    console.log('请勿重复点击')
  },
  onCooldown: (seconds) => {
    console.log(`请${seconds}秒后再试`)
  }
})

const handleSubmit = async () => {
  const result = await execute(() => api.submit(data))
  if (result) {
    console.log('提交成功')
  }
}
</script>

<template>
  <button :disabled="isSubmitting" @click="handleSubmit">
    {{ isSubmitting ? '提交中...' : '提交' }}
  </button>
</template>
五、实际项目中的综合方案

1. 完整的防护体系

javascript 复制代码
// utils/submission-manager.js
class SubmissionManager {
  constructor() {
    this.submissions = new Map()
    this.defaultCooldown = 3000
  }
  
  // 开始提交
  start(key, cooldown = this.defaultCooldown) {
    const now = Date.now()
    const submission = this.submissions.get(key)
    
    if (submission) {
      const timeSinceLast = now - submission.lastSubmit
      if (timeSinceLast < cooldown) {
        return {
          allowed: false,
          waitTime: cooldown - timeSinceLast
        }
      }
    }
    
    this.submissions.set(key, {
      lastSubmit: now,
      cooldown,
      inProgress: true
    })
    
    return { allowed: true }
  }
  
  // 结束提交
  finish(key, success = true) {
    const submission = this.submissions.get(key)
    if (submission) {
      submission.inProgress = false
      if (!success) {
        // 失败时重置,允许立即重试
        submission.lastSubmit = 0
      }
    }
  }
  
  // 检查是否允许提交
  canSubmit(key) {
    const submission = this.submissions.get(key)
    if (!submission) return true
    
    const now = Date.now()
    return !submission.inProgress && 
           (now - submission.lastSubmit >= submission.cooldown)
  }
  
  // 获取等待时间
  getWaitTime(key) {
    const submission = this.submissions.get(key)
    if (!submission) return 0
    
    const now = Date.now()
    return Math.max(0, submission.cooldown - (now - submission.lastSubmit))
  }
}

export const submissionManager = new SubmissionManager()

// 在业务组件中使用
export default {
  methods: {
    async submitForm() {
      const formKey = `form-${this.formId}`
      const checkResult = submissionManager.start(formKey)
      
      if (!checkResult.allowed) {
        this.$message.warning(`请${Math.ceil(checkResult.waitTime / 1000)}秒后再试`)
        return
      }
      
      try {
        const result = await this.api.submit(this.formData)
        submissionManager.finish(formKey, true)
        this.$message.success('提交成功')
        return result
      } catch (error) {
        submissionManager.finish(formKey, false)
        this.$message.error('提交失败')
        throw error
      }
    }
  }
}
面试回答要点总结

"总结防止用户重复点击的解决方案:

分层防护策略:

  1. UI层面

    • 按钮loading状态

    • 视觉反馈和禁用状态

    • 防抖函数控制点击频率

  2. 请求层面

    • 请求锁机制,拦截重复请求

    • Promise锁,保证同一操作只执行一次

    • 请求队列管理

  3. 业务层面

    • 页面级加载状态

    • 路由导航防护

    • 操作冷却时间控制

  4. 高级方案

    • 自定义指令封装

    • 组合式API(Vue3)

    • 完整的提交管理器

技术选型建议:

  • 简单场景:按钮loading状态 + 防抖

  • 中等复杂度:请求拦截器 + Promise锁

  • 复杂系统:完整的提交管理 + 指令封装

在实际项目中,我通常会根据业务重要性采用组合策略,比如关键支付操作使用多重防护,普通表单使用基础防护。"

相关推荐
掘了36 分钟前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅39 分钟前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
NEXT063 小时前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法