uni-app——uni-app 防重复请求导致Promise悬挂页面卡死问题的解决方案
uni-app——uni-app 防重复请求导致Promise悬挂页面卡死问题的解决方案
·
问题描述
小程序使用过程中,突然出现页面卡顿的问题:
- 点击任何模块都没有反应
- 只能通过微信自带的返回或右上角图标操作
- 重新进入小程序后恢复正常
复现场景:用户在网络较慢时快速多次点击按钮,触发相同的接口请求。
问题原因
在 HTTP 请求封装中实现了"防重复请求"功能,但实现方式有缺陷:
```javascript
// 有问题的代码
const map = new Set();
const http = (config) => {
return new Promise((resolve, reject) => {
const key = ${config.url}${JSON.stringify(config.data)};
// 问题:检测到重复请求后直接 return
if (map.has(key)) {
console.log('检测到重复请求,已拦截:', key);
return; // ❌ Promise 永远 pending,不会 resolve/reject
}
map.add(key);
uni.request({
// ...
complete() {
map.delete(key);
},
});
});
};
**问题分析**:
1. 检测到重复请求时,函数直接 `return;`
2. 这只是从 Promise executor 回调中返回,**并不会 resolve 或 reject Promise**
3. 调用方得到一个**永远处于 pending 状态**的 Promise
4. 页面的 loading/mask 依赖 Promise 完成来关闭,但永远等不到
5. 用户界面被"锁定",表现为"卡顿"
Promise 状态流转对比:
正常请求: pending → resolved/rejected ✅
被拦截的重复请求: pending → (永远 pending) ❌
↑
loading 永不消失
页面无法响应
## 解决方案
将"直接拦截返回"改为"复用同一个 Promise":
```javascript
// 修复后的代码
const pendingRequests = new Map(); // 改用 Map 存储 Promise
/**
* 生成请求唯一标识
*/
const generateRequestKey = (config) => {
const dataStr = config.data ? JSON.stringify(config.data) : '';
return `${config.method || 'GET'}_${config.url}_${dataStr}`;
};
const http = (config) => {
const key = generateRequestKey(config);
// ✅ 如果存在相同的请求正在进行中,复用该 Promise
if (pendingRequests.has(key)) {
console.log('检测到重复请求,复用已有Promise:', key);
return pendingRequests.get(key); // 返回已有的 Promise
}
// 创建新的 Promise
const promise = new Promise((resolve, reject) => {
uni.request({
method: 'GET',
...config,
success(response) {
resolve(response.data);
},
fail(err) {
reject(err);
},
complete() {
// 请求完成后从缓存中移除
pendingRequests.delete(key);
},
});
});
// 在返回前将 Promise 存入缓存
pendingRequests.set(key, promise);
return promise;
};
完整 Demo 示例
// http.js - 完整的请求封装
/**
* 请求缓存 Map
* 结构: { requestKey: Promise }
*/
const pendingRequests = new Map();
/**
* 生成请求唯一标识
* @param {Object} config 请求配置
* @returns {string} 唯一标识
*/
const generateRequestKey = (config) => {
const method = (config.method || 'GET').toUpperCase();
const url = config.url || '';
const dataStr = config.data ? JSON.stringify(config.data) : '';
return `${method}_${url}_${dataStr}`;
};
/**
* 封装的 HTTP 请求方法
* @param {Object} config 请求配置
* @returns {Promise} 请求 Promise
*/
const http = (config) => {
const key = generateRequestKey(config);
// 检查是否有相同的请求正在进行
if (pendingRequests.has(key)) {
console.log('[HTTP] 复用已有请求:', key);
// 核心修复:返回已有的 Promise,而非 return;
return pendingRequests.get(key);
}
// 创建新的请求 Promise
const promise = new Promise((resolve, reject) => {
// 默认配置
const defaultConfig = {
method: 'GET',
timeout: 30000,
header: {
'Content-Type': 'application/json',
},
};
// 合并配置
const finalConfig = {
...defaultConfig,
...config,
header: {
...defaultConfig.header,
...config.header,
},
};
console.log('[HTTP] 发送请求:', key);
// 发送请求(uni-app 环境)
uni.request({
...finalConfig,
success(response) {
const { statusCode, data } = response;
if (statusCode >= 200 && statusCode < 300) {
// 业务层状态码判断
if (data.code === 0 || data.code === 200) {
resolve(data.data || data);
} else {
reject(new Error(data.message || '请求失败'));
}
} else {
reject(new Error(`HTTP Error: ${statusCode}`));
}
},
fail(err) {
console.error('[HTTP] 请求失败:', err);
reject(new Error(err.errMsg || '网络请求失败'));
},
complete() {
// 关键:请求完成后清理缓存
pendingRequests.delete(key);
console.log('[HTTP] 请求完成,清理缓存:', key);
},
});
});
// 将 Promise 存入缓存
pendingRequests.set(key, promise);
return promise;
};
/**
* GET 请求
*/
const get = (url, params = {}, config = {}) => {
return http({
method: 'GET',
url,
data: params,
...config,
});
};
/**
* POST 请求
*/
const post = (url, data = {}, config = {}) => {
return http({
method: 'POST',
url,
data,
...config,
});
};
/**
* 取消所有正在进行的请求
* 用于页面卸载等场景
*/
const cancelAllRequests = () => {
pendingRequests.clear();
console.log('[HTTP] 已清理所有待处理请求');
};
/**
* 获取当前待处理请求数量
*/
const getPendingCount = () => {
return pendingRequests.size;
};
export default http;
export { get, post, cancelAllRequests, getPendingCount };
使用示例
<template>
<div class="page">
<button @click="fetchData" :disabled="loading">
{{ loading ? '加载中...' : '获取数据' }}
</button>
<div v-if="data">{{ data }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { get } from '@/api/http';
const loading = ref(false);
const data = ref(null);
const fetchData = async () => {
loading.value = true;
try {
// 即使用户快速点击多次,也只会发送一个请求
// 后续点击复用第一个请求的 Promise
const result = await get('/api/user/info', { id: 123 });
data.value = result;
} catch (error) {
console.error('请求失败:', error.message);
} finally {
// Promise 正常完成,loading 正确关闭
loading.value = false;
}
};
</script>
核心机制详解
1. 请求流程对比
修复前(问题):
┌────────────────────────────────────────────────────┐
│ 第1次点击: http(config) → Promise1(pending) │
│ └─ uni.request 发送,等待响应 │
│ │
│ 第2次点击: http(config) → Promise2(pending) │
│ └─ 检测到重复,return; │
│ └─ Promise2 永远 pending ❌ │
│ │
│ 响应返回: Promise1 → resolved │
│ Promise2 → 仍然 pending ❌ │
│ │
│ 结果: 第2次调用的 .finally() 永远不会执行 │
│ loading 不关闭,页面卡死 │
└────────────────────────────────────────────────────┘
修复后(正确):
┌────────────────────────────────────────────────────┐
│ 第1次点击: http(config) → Promise1(pending) │
│ └─ uni.request 发送,等待响应 │
│ └─ pendingRequests.set(key, Promise1) │
│ │
│ 第2次点击: http(config) → Promise1(pending) │
│ └─ 检测到重复,return pendingRequests.get(key) │
│ └─ 返回同一个 Promise1 ✅ │
│ │
│ 响应返回: Promise1 → resolved │
│ └─ 第1次和第2次调用同时得到响应 ✅ │
│ └─ complete() 清理缓存 │
│ │
│ 结果: 两个调用的 .finally() 都正常执行 │
│ loading 正确关闭,页面响应正常 │
└────────────────────────────────────────────────────┘
2. 为什么用 Map 而非 Set
// Set 只能存储键,无法存储 Promise
const set = new Set();
set.add(key);
// 无法获取对应的 Promise
// Map 可以存储键值对
const map = new Map();
map.set(key, promise);
const cachedPromise = map.get(key); // 可以获取 Promise
3. 请求标识的生成
// 不完善:相同 URL 不同方法会冲突
const key = url + JSON.stringify(data);
// 完善:包含请求方法
const key = `${method}_${url}_${JSON.stringify(data)}`;
// 示例:
// GET_/api/user_{}
// POST_/api/user_{"name":"test"}
// 这两个是不同的请求
4. 缓存清理的时机
uni.request({
success() {
// 成功回调先执行
resolve(data);
},
fail() {
// 失败回调先执行
reject(error);
},
complete() {
// complete 总是最后执行
// 此时 Promise 已经 settled
// 可以安全地清理缓存
pendingRequests.delete(key);
},
});
经验总结
1. Promise executor 中 return 的陷阱
// 错误理解
new Promise((resolve, reject) => {
if (condition) {
return; // ❌ 这只是从函数返回,Promise 仍然 pending
}
resolve(data);
});
// 正确理解
new Promise((resolve, reject) => {
if (condition) {
resolve(null); // ✅ 明确 resolve
return;
}
// 或者
if (condition) {
reject(new Error('条件不满足')); // ✅ 明确 reject
return;
}
resolve(data);
});
2. 防重复请求的正确姿势
// 方案1: 复用 Promise(本文方案)
const cache = new Map();
const request = (config) => {
const key = generateKey(config);
if (cache.has(key)) {
return cache.get(key); // 复用已有 Promise
}
const promise = doRequest(config);
cache.set(key, promise);
promise.finally(() => cache.delete(key));
return promise;
};
// 方案2: 取消之前的请求
const abortControllers = new Map();
const request = (config) => {
const key = generateKey(config);
// 取消之前的请求
if (abortControllers.has(key)) {
abortControllers.get(key).abort();
}
const controller = new AbortController();
abortControllers.set(key, controller);
return fetch(config.url, {
signal: controller.signal,
}).finally(() => {
abortControllers.delete(key);
});
};
3. 使用 finally 确保清理
// 不推荐:分别在 success 和 fail 中清理
success() {
pendingRequests.delete(key); // 需要写两遍
},
fail() {
pendingRequests.delete(key); // 容易遗漏
}
// 推荐:使用 complete/finally
complete() {
pendingRequests.delete(key); // 只写一次,保证执行
}
// 或者 Promise.finally
promise.finally(() => {
cache.delete(key);
});
4. 页面卸载时的清理
// 页面组件中
import { cancelAllRequests } from '@/api/http';
onUnload(() => {
// 清理所有待处理请求,防止页面已销毁但回调仍在执行
cancelAllRequests();
});
调试技巧
1. 模拟慢网络
// 在请求封装中添加延迟,方便调试重复请求问题
const http = (config) => {
return new Promise((resolve) => {
setTimeout(() => {
// 实际请求逻辑
}, 3000); // 模拟 3 秒延迟
});
};
2. 日志输出
const http = (config) => {
const key = generateRequestKey(config);
console.log('[HTTP] 请求开始:', key);
console.log('[HTTP] 当前缓存数量:', pendingRequests.size);
console.log('[HTTP] 缓存内容:', [...pendingRequests.keys()]);
if (pendingRequests.has(key)) {
console.log('[HTTP] 命中缓存,复用 Promise');
}
// ...
};
3. 检测 Promise 状态
// 辅助函数:检测 Promise 是否已完成
const isPromiseSettled = async (promise) => {
const unique = Symbol('unique');
const result = await Promise.race([
promise.then(() => 'resolved').catch(() => 'rejected'),
Promise.resolve(unique),
]);
return result !== unique;
};
// 使用
const promise = http(config);
console.log('是否已完成:', await isPromiseSettled(promise));
问题排查清单
当遇到类似的"页面卡死"问题时,可按以下步骤排查:
-
检查 Promise 是否正确 resolve/reject
- 搜索代码中的
return;是否在 Promise executor 内部 - 确保所有分支都有明确的 resolve 或 reject
- 搜索代码中的
-
检查 loading/mask 的关闭逻辑
- 是否依赖 Promise 的 finally/then/catch
- 是否有遗漏的关闭路径
-
检查防重复请求的实现
- 是否正确处理了重复请求的情况
- 被拦截的请求是否返回了可用的 Promise
-
网络请求超时设置
- 是否设置了合理的超时时间
- 超时后是否正确触发了 reject
更多推荐
所有评论(0)