目录
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
};
面试回答要点总结
"总结请求响应慢的优化方案:
-
诊断先行:使用专业工具定位性能瓶颈
-
前端优化:请求合并、缓存、优先级调度、Web Workers
-
网络优化:HTTP/2、预连接、数据压缩、CDN
-
服务端协作:API设计优化、缓存策略、数据库优化
-
用户体验:骨架屏、智能重试、服务降级
-
监控告警:性能数据收集、慢请求监控
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
}
}
}
}
面试回答要点总结
"总结防止用户重复点击的解决方案:
分层防护策略:
-
UI层面:
-
按钮loading状态
-
视觉反馈和禁用状态
-
防抖函数控制点击频率
-
-
请求层面:
-
请求锁机制,拦截重复请求
-
Promise锁,保证同一操作只执行一次
-
请求队列管理
-
-
业务层面:
-
页面级加载状态
-
路由导航防护
-
操作冷却时间控制
-
-
高级方案:
-
自定义指令封装
-
组合式API(Vue3)
-
完整的提交管理器
-
技术选型建议:
-
简单场景:按钮loading状态 + 防抖
-
中等复杂度:请求拦截器 + Promise锁
-
复杂系统:完整的提交管理 + 指令封装
在实际项目中,我通常会根据业务重要性采用组合策略,比如关键支付操作使用多重防护,普通表单使用基础防护。"