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锁

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

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

相关推荐
weixin_ab4 小时前
【HTML分离术】
前端·html
文心快码BaiduComate4 小时前
新手该如何选择AI编程工具?文心快码Comate全方位体验
前端·后端·程序员
夫唯不争,故无尤也4 小时前
Tomcat 内嵌启动时找不到 Web 应用的路径
java·前端·tomcat
lichong9514 小时前
【Xcode】Macos p12 证书过期时间查看
前端·ide·macos·证书·xcode·大前端·大前端++
oh,huoyuyan4 小时前
如何在火语言中指定启动 Chrome 特定用户配置文件
前端·javascript·chrome
前端大聪明20024 小时前
single-spa原理解析
前端·javascript
一枚前端小能手4 小时前
📦 从npm到yarn到pnpm的演进之路 - 包管理器实现原理深度解析
前端·javascript·npm
影i4 小时前
CSS Transform 和父元素撑开问题
前端
@大迁世界5 小时前
Promise.all 与 Promise.allSettled:一次取数的小差别,救了我的接口
开发语言·前端·javascript·ecmascript