1. 为什么需要有时间限制的Promise
在前端开发中,Promise已经成为处理异步操作的标准方式。但实际业务场景中,我们经常会遇到这样的需求:某个异步操作如果在一定时间内没有完成,就应该自动终止并抛出超时错误。这就是"有时间限制的Promise"要解决的问题。
想象一个典型的场景:你的应用需要从第三方API获取数据,但该API响应不稳定。如果没有超时控制,用户可能会一直等待,体验极差。更糟的是,如果这个Promise永远不会被resolve或reject,就会造成内存泄漏。
我在实际项目中就遇到过这样的案例:一个文件上传功能,由于网络问题导致Promise一直处于pending状态,最终拖垮了整个页面的性能。这就是为什么我们需要给Promise加上时间限制——它不仅是用户体验的保障,更是系统健壮性的必需。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实现基础版超时Promise
2.1 使用setTimeout的简单方案
最直接的实现方式是结合setTimeout和Promise.race。Promise.race方法接收一个Promise数组,返回最先settled的那个Promise的结果。我们可以利用这个特性来实现超时控制:
javascript复制function timeoutPromise(promise, timeout) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Promise timed out after ${timeout}ms`));
}, timeout);
});
return Promise.race([promise, timeoutPromise]);
}
// 使用示例
const fetchWithTimeout = timeoutPromise(
fetch('https://api.example.com/data'),
3000
);
fetchWithTimeout
.then(response => console.log('成功:', response))
.catch(error => console.log('失败:', error));
这个方案虽然简单,但有几个需要注意的点:
- 超时后原Promise并不会被取消,它仍然会在后台执行
- 如果原Promise在超时后完成,其结果会被忽略
- 错误信息应该足够明确,方便调试
2.2 超时后的资源清理
基础方案的一个明显问题是:超时后,原始操作仍在继续。对于网络请求这类操作,我们可能还需要主动取消。以fetch为例,可以使用AbortController:
javascript复制function timeoutFetch(url, options, timeout = 5000) {
const controller = new AbortController();
const { signal } = controller;
const fetchPromise = fetch(url, { ...options, signal });
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
return fetchPromise
.finally(() => clearTimeout(timeoutId));
}
// 使用示例
timeoutFetch('https://api.example.com/data', {}, 3000)
.then(response => response.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.error('请求超时');
} else {
console.error('其他错误:', err);
}
});
这个改进版在超时后会主动中止请求,而不是让它在后台继续消耗资源。注意我们使用了finally来确保无论成功还是失败都会清理定时器。
3. 高级超时控制策略
3.1 可配置的超时行为
在实际项目中,我们可能需要对超时行为有更精细的控制。比如:
- 某些重要请求可能需要重试机制
- 不同API可能需要不同的超时阈值
- 可能需要记录超时发生的上下文信息
下面是一个更完善的实现:
javascript复制class TimeoutPromise {
constructor(executor, options = {}) {
const {
timeout = 5000,
onTimeout = () => {},
retryTimes = 0,
retryDelay = 1000
} = options;
this.timeout = timeout;
this.onTimeout = onTimeout;
this.retryTimes = retryTimes;
this.retryDelay = retryDelay;
this.attempts = 0;
return this._execute(executor);
}
_execute(executor) {
this.attempts++;
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.onTimeout(this.attempts);
if (this.attempts <= this.retryTimes) {
setTimeout(() => {
this._execute(executor).then(resolve, reject);
}, this.retryDelay);
} else {
reject(new Error(`Timeout after ${this.timeout}ms (attempt ${this.attempts})`));
}
}, this.timeout);
executor(
value => {
clearTimeout(timer);
resolve(value);
},
reason => {
clearTimeout(timer);
reject(reason);
}
);
});
}
}
// 使用示例
new TimeoutPromise((resolve, reject) => {
// 异步操作
someAsyncOperation().then(resolve, reject);
}, {
timeout: 3000,
retryTimes: 2,
retryDelay: 1000,
onTimeout: (attempt) => {
console.log(`第${attempt}次尝试超时`);
}
}).then(
result => console.log('成功:', result),
error => console.log('最终失败:', error)
);
3.2 超时与重试的最佳实践
结合超时和重试机制时,有几个经验值得分享:
-
指数退避:重试延迟应该逐渐增加,而不是固定值。这可以避免短时间内大量重试导致的服务雪崩。
-
熔断机制:当连续超时达到一定次数后,应该暂时停止请求,直接失败,避免持续重试拖垮系统。
-
上下文传递:确保每次重试都能获取到完整的原始请求上下文,包括请求参数、headers等。
-
监控报警:记录超时发生的频率和场景,设置合理的报警阈值。
4. 实际应用中的陷阱与解决方案
4.1 内存泄漏问题
一个容易被忽视的问题是:即使Promise被reject了,如果原始操作没有正确清理,仍可能导致内存泄漏。比如:
javascript复制function leakyTimeout(promise, timeout) {
let completed = false;
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
if (!completed) {
reject(new Error('Timeout'));
}
}, timeout);
});
return Promise.race([
promise.then(result => {
completed = true;
return result;
}),
timeoutPromise
]);
}
这个实现看起来没问题,但如果promise永远不会settle,timeoutPromise中引用的completed变量就会一直存在于内存中。正确的做法是确保所有引用都能被垃圾回收:
javascript复制function safeTimeout(promise, timeout) {
let timer;
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(() => {
reject(new Error('Timeout'));
}, timeout);
});
return Promise.race([
promise.finally(() => clearTimeout(timer)),
timeoutPromise
]).finally(() => clearTimeout(timer));
}
4.2 错误处理的一致性
超时引入的另一个复杂性是错误类型的多样性。我们需要确保:
- 超时错误能被明确识别
- 原始错误信息不丢失
- 错误处理逻辑一致
建议定义一个自定义错误类:
javascript复制class TimeoutError extends Error {
constructor(message, originalError) {
super(message);
this.name = 'TimeoutError';
this.originalError = originalError;
}
}
function timeoutPromise(promise, timeout, message = 'Operation timed out') {
let timer;
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(() => {
reject(new TimeoutError(message));
}, timeout);
});
return Promise.race([
promise.catch(err => {
clearTimeout(timer);
throw new TimeoutError(message, err);
}),
timeoutPromise
]).finally(() => clearTimeout(timer));
}
这样处理错误时就能明确区分超时和其他类型的错误:
javascript复制timeoutPromise(someOperation(), 3000)
.catch(err => {
if (err.name === 'TimeoutError') {
console.error('操作超时');
if (err.originalError) {
console.error('原始错误:', err.originalError);
}
} else {
console.error('其他错误:', err);
}
});
4.3 测试策略
测试超时Promise时,需要考虑多种场景:
- 正常完成的情况
- 超时发生的情况
- 原始Promise reject的情况
- 边界条件(如timeout=0)
使用Jest测试的例子:
javascript复制describe('timeoutPromise', () => {
jest.useFakeTimers();
it('应该在超时前完成时返回原始结果', async () => {
const fastPromise = Promise.resolve('success');
const resultPromise = timeoutPromise(fastPromise, 1000);
await expect(resultPromise).resolves.toBe('success');
});
it('应该在超时时reject', async () => {
const slowPromise = new Promise(() => {}); // 永远不会resolve
const resultPromise = timeoutPromise(slowPromise, 1000);
jest.advanceTimersByTime(1000);
await expect(resultPromise).rejects.toThrow('Operation timed out');
});
it('应该保留原始错误', async () => {
const failingPromise = Promise.reject(new Error('Original error'));
const resultPromise = timeoutPromise(failingPromise, 1000);
await expect(resultPromise).rejects.toThrow('Original error');
});
});
5. 性能优化与高级技巧
5.1 批量请求的超时控制
当需要同时发送多个请求时,合理的超时策略尤为重要。常见的模式有:
- 全局超时+单个超时:为整个批量操作设置一个总超时,同时每个请求有自己的超时
- 动态超时调整:根据历史响应时间动态调整超时阈值
- 优先级队列:重要请求使用较长的超时时间,次要请求使用较短时间
实现示例:
javascript复制async function batchRequestsWithTimeout(requests, globalTimeout = 10000) {
const controller = new AbortController();
const { signal } = controller;
const globalTimer = setTimeout(() => {
controller.abort();
}, globalTimeout);
try {
const results = await Promise.all(requests.map(req => {
return timeoutFetch(req.url, {
...req.options,
signal
}, req.timeout || 3000);
}));
return results;
} finally {
clearTimeout(globalTimer);
}
}
5.2 基于响应时间的自适应超时
更高级的实现可以根据历史响应时间动态调整超时阈值:
javascript复制class AdaptiveTimeout {
constructor(baseTimeout = 3000, options = {}) {
this.baseTimeout = baseTimeout;
this.history = [];
this.maxHistorySize = options.maxHistorySize || 10;
this.multiplier = options.multiplier || 1.5;
}
async execute(promiseFactory) {
const timeout = this.calculateTimeout();
const start = Date.now();
try {
const result = await timeoutPromise(
promiseFactory(),
timeout
);
const duration = Date.now() - start;
this.recordSuccess(duration);
return result;
} catch (error) {
if (error.name === 'TimeoutError') {
this.recordTimeout();
}
throw error;
}
}
calculateTimeout() {
if (this.history.length === 0) return this.baseTimeout;
const avg = this.history.reduce((sum, val) => sum + val, 0) / this.history.length;
return Math.max(this.baseTimeout, avg * this.multiplier);
}
recordSuccess(duration) {
this.history.push(duration);
if (this.history.length > this.maxHistorySize) {
this.history.shift();
}
}
recordTimeout() {
// 超时后稍微增加基础超时时间
this.baseTimeout = Math.min(
this.baseTimeout * 1.2,
30000 // 最大不超过30秒
);
}
}
// 使用示例
const adapter = new AdaptiveTimeout(3000);
for (let i = 0; i < 10; i++) {
try {
const result = await adapter.execute(() => fetch('https://api.example.com/data'));
console.log('成功:', result);
} catch (error) {
console.error('失败:', error);
}
}
5.3 Web Worker中的超时控制
在Web Worker中使用Promise时,超时控制同样重要。但由于Worker的特殊环境,实现方式略有不同:
javascript复制// worker.js
self.addEventListener('message', async (event) => {
const { id, timeout } = event.data;
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('Worker timeout'));
}, timeout);
});
try {
const result = await Promise.race([
performHeavyTask(),
timeoutPromise
]);
self.postMessage({ id, result });
} catch (error) {
self.postMessage({ id, error: error.message });
}
});
function performHeavyTask() {
return new Promise(resolve => {
// 模拟耗时操作
const result = doComplexCalculation();
resolve(result);
});
}
主线程调用:
javascript复制function runWorkerWithTimeout(worker, task, timeout) {
return new Promise((resolve, reject) => {
const id = Math.random().toString(36).substr(2, 9);
worker.addEventListener('message', function handler(event) {
if (event.data.id === id) {
worker.removeEventListener('message', handler);
if (event.data.error) {
reject(new Error(event.data.error));
} else {
resolve(event.data.result);
}
}
});
worker.postMessage({ id, task, timeout });
});
}
// 使用示例
const worker = new Worker('worker.js');
try {
const result = await runWorkerWithTimeout(
worker,
{ /* 任务参数 */ },
5000 // 5秒超时
);
console.log('Worker结果:', result);
} catch (error) {
console.error('Worker错误:', error);
}
6. 实际案例分析
6.1 文件上传的超时控制
文件上传是常见的需要超时控制的场景。以下是一个完整的实现示例:
javascript复制class FileUploader {
constructor(options = {}) {
this.defaultTimeout = options.timeout || 30000;
this.chunkSize = options.chunkSize || 1024 * 1024; // 1MB
this.retryTimes = options.retryTimes || 3;
}
async upload(file, onProgress) {
const fileSize = file.size;
let uploaded = 0;
let chunks = Math.ceil(fileSize / this.chunkSize);
for (let i = 0; i < chunks; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, fileSize);
const chunk = file.slice(start, end);
let attempts = 0;
let lastError;
while (attempts <= this.retryTimes) {
attempts++;
try {
const formData = new FormData();
formData.append('file', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', chunks);
await this._uploadChunk(formData, this.defaultTimeout);
uploaded += chunk.size;
onProgress(uploaded / fileSize);
break;
} catch (error) {
lastError = error;
if (attempts > this.retryTimes) {
throw new Error(`上传失败: ${error.message}`);
}
// 等待一段时间再重试
await new Promise(resolve =>
setTimeout(resolve, 1000 * attempts)
);
}
}
}
return this._completeUpload(file.name);
}
async _uploadChunk(formData, timeout) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch('/upload', {
method: 'POST',
body: formData,
signal: controller.signal
});
if (!response.ok) {
throw new Error(`服务器错误: ${response.status}`);
}
return response.json();
} finally {
clearTimeout(timer);
}
}
async _completeUpload(filename) {
const response = await timeoutPromise(
fetch('/complete', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ filename })
}),
5000
);
if (!response.ok) {
throw new Error('完成上传失败');
}
return response.json();
}
}
// 使用示例
const uploader = new FileUploader({
timeout: 15000,
chunkSize: 5 * 1024 * 1024 // 5MB
});
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
await uploader.upload(file, (progress) => {
console.log(`上传进度: ${(progress * 100).toFixed(1)}%`);
});
console.log('上传成功');
} catch (error) {
console.error('上传失败:', error);
}
});
6.2 API请求的熔断机制
结合超时和熔断机制,可以构建更健壮的API客户端:
javascript复制class ResilientAPIClient {
constructor(options = {}) {
this.baseURL = options.baseURL || '';
this.timeout = options.timeout || 5000;
this.circuitBreaker = {
threshold: options.threshold || 3,
cooldown: options.cooldown || 30000,
failures: 0,
lastFailure: 0,
isOpen: false
};
}
async request(endpoint, options = {}) {
if (this.circuitBreaker.isOpen) {
const now = Date.now();
if (now - this.circuitBreaker.lastFailure < this.circuitBreaker.cooldown) {
throw new Error('服务不可用(熔断中)');
}
this.circuitBreaker.isOpen = false;
}
try {
const response = await timeoutPromise(
fetch(`${this.baseURL}${endpoint}`, options),
options.timeout || this.timeout
);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
this.circuitBreaker.failures = 0;
return response.json();
} catch (error) {
this.circuitBreaker.failures++;
this.circuitBreaker.lastFailure = Date.now();
if (this.circuitBreaker.failures >= this.circuitBreaker.threshold) {
this.circuitBreaker.isOpen = true;
}
throw error;
}
}
}
// 使用示例
const api = new ResilientAPIClient({
baseURL: 'https://api.example.com',
timeout: 3000,
threshold: 2,
cooldown: 60000
});
// 在React组件中使用
async function fetchData() {
try {
const data = await api.request('/data');
setState({ data });
} catch (error) {
if (error.message.includes('熔断')) {
showAlert('服务暂时不可用,请稍后再试');
} else {
showAlert(`请求失败: ${error.message}`);
}
}
}
6.3 数据库查询的超时控制
Node.js后端应用中,数据库查询同样需要超时控制:
javascript复制const { Pool } = require('pg');
const { TimeoutError } = require('./errors');
class Database {
constructor(config) {
this.pool = new Pool(config);
this.defaultTimeout = config.timeout || 5000;
}
async query(text, params, timeout = this.defaultTimeout) {
const client = await this.pool.connect();
try {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new TimeoutError('数据库查询超时'));
}, timeout);
});
const queryPromise = client.query(text, params)
.finally(() => client.release());
return await Promise.race([queryPromise, timeoutPromise]);
} catch (error) {
client.release();
throw error;
}
}
}
// 使用示例
const db = new Database({
user: 'dbuser',
host: 'localhost',
database: 'mydb',
password: 'secret',
port: 5432,
timeout: 3000
});
async function getUsers() {
try {
const { rows } = await db.query(
'SELECT * FROM users WHERE active = $1',
[true],
2000 // 2秒超时
);
return rows;
} catch (error) {
if (error.name === 'TimeoutError') {
console.error('数据库查询超时');
// 可能返回缓存数据或重试
return [];
}
throw error;
}
}
7. 浏览器兼容性与替代方案
7.1 兼容旧版浏览器的策略
如果需要支持不支持AbortController的旧浏览器,可以使用这些替代方案:
- XMLHttpRequest:原生支持超时属性
- 包装setTimeout:虽然不能真正取消请求,但可以忽略响应
- 第三方库:如axios已经内置了超时和取消支持
XMLHttpRequest示例:
javascript复制function legacyTimeoutRequest(url, options, timeout) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(options.method || 'GET', url);
// 设置headers
if (options.headers) {
for (const [key, value] of Object.entries(options.headers)) {
xhr.setRequestHeader(key, value);
}
}
xhr.timeout = timeout;
xhr.ontimeout = () => {
reject(new Error(`请求超时 (${timeout}ms)`));
};
xhr.onerror = () => {
reject(new Error('网络错误'));
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(new Error(`HTTP错误: ${xhr.status}`));
}
};
if (options.body) {
xhr.send(options.body);
} else {
xhr.send();
}
});
}
7.2 特性检测与渐进增强
更健壮的实现应该包含特性检测:
javascript复制function smartFetch(url, options, timeout) {
if (typeof AbortController !== 'undefined') {
return timeoutFetch(url, options, timeout); // 使用现代实现
} else if (typeof XMLHttpRequest !== 'undefined') {
return legacyTimeoutRequest(url, options, timeout);
} else {
throw new Error('当前环境不支持可取消的请求');
}
}
7.3 第三方库集成
许多流行的HTTP客户端库已经内置了超时支持:
Axios示例:
javascript复制// 直接使用axios的timeout配置
axios.get('/api/data', {
timeout: 3000
})
.then(response => console.log(response.data))
.catch(error => {
if (error.code === 'ECONNABORTED') {
console.error('请求超时');
} else {
console.error('其他错误:', error);
}
});
// 使用CancelToken实现更灵活的控制
const source = axios.CancelToken.source();
setTimeout(() => {
source.cancel('请求超时');
}, 3000);
axios.get('/api/data', {
cancelToken: source.token
})
.then(response => console.log(response.data))
.catch(thrown => {
if (axios.isCancel(thrown)) {
console.log('请求取消:', thrown.message);
} else {
console.error('其他错误:', thrown);
}
});
jQuery AJAX示例:
javascript复制$.ajax({
url: '/api/data',
timeout: 3000,
success: function(data) {
console.log('成功:', data);
},
error: function(xhr, status, error) {
if (status === 'timeout') {
console.error('请求超时');
} else {
console.error('其他错误:', error);
}
}
});
8. 性能考量与最佳实践
8.1 定时器的性能影响
大量使用setTimeout可能会带来性能问题,特别是在Node.js服务器端。需要注意:
- 定时器数量:避免创建大量并发的定时器
- 及时清理:确保所有定时器都被正确清除
- 重用定时器:对于频繁使用的超时,考虑重用同一个定时器
优化示例:
javascript复制class TimerManager {
constructor() {
this.timers = new Map();
}
setTimer(id, callback, delay) {
this.clearTimer(id);
const timer = setTimeout(() => {
this.timers.delete(id);
callback();
}, delay);
this.timers.set(id, timer);
}
clearTimer(id) {
if (this.timers.has(id)) {
clearTimeout(this.timers.get(id));
this.timers.delete(id);
}
}
clearAll() {
for (const timer of this.timers.values()) {
clearTimeout(timer);
}
this.timers.clear();
}
}
// 使用示例
const timerManager = new TimerManager();
function makeRequestWithCleanTimeout(requestId, url, timeout) {
return new Promise((resolve, reject) => {
timerManager.setTimer(
requestId,
() => reject(new Error('Timeout')),
timeout
);
fetch(url)
.then(response => {
timerManager.clearTimer(requestId);
resolve(response);
})
.catch(error => {
timerManager.clearTimer(requestId);
reject(error);
});
});
}
8.2 内存管理
Promise超时实现中常见的内存泄漏场景:
- 闭包引用:定时器回调中引用了外部变量
- 未清理的引用:已完成的Promise仍然被其他对象引用
- 事件监听器:未正确移除的事件监听器
防范措施:
javascript复制// 反模式 - 可能导致内存泄漏
function leakyTimeout(promise, timeout) {
let completed = false;
const timer = setTimeout(() => {
if (!completed) {
console.log('超时');
}
}, timeout);
return promise.finally(() => {
completed = true;
});
}
// 正确模式
function safeTimeout(promise, timeout) {
let timer;
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(() => {
reject(new Error('Timeout'));
}, timeout);
});
return Promise.race([
promise.finally(() => clearTimeout(timer)),
timeoutPromise
]).finally(() => clearTimeout(timer));
}
8.3 错误监控与日志
对于生产环境,完善的错误监控很重要:
javascript复制class TimedOperation {
constructor(operationName, options = {}) {
this.name = operationName;
this.timeout = options.timeout || 3000;
this.logger = options.logger || console;
this.metrics = options.metrics || {
increment: () => {},
timing: () => {}
};
}
async execute(promise) {
const start = Date.now();
let timer;
try {
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(() => {
const duration = Date.now() - start;
this.metrics.timing(`timeout.${this.name}`, duration);
reject(new Error(`${this.name} timed out after ${duration}ms`));
}, this.timeout);
});
const result = await Promise.race([promise, timeoutPromise]);
const duration = Date.now() - start;
this.metrics.timing(`success.${this.name}`, duration);
return result;
} catch (error) {
const duration = Date.now() - start;
this.metrics.increment(`error.${this.name}`);
this.logger.error(`${this.name} failed after ${duration}ms:`, error);
throw error;
} finally {
clearTimeout(timer);
}
}
}
// 使用示例
const fetchWithMonitoring = new TimedOperation('fetchUserData', {
timeout: 2000,
logger: myLogger,
metrics: statsdClient
});
app.get('/user/:id', async (req, res) => {
try {
const data = await fetchWithMonitoring.execute(
fetchUserData(req.params.id)
);
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
9. 测试策略与调试技巧
9.1 单元测试模式
测试超时逻辑的特殊考虑:
- 避免真实等待:使用假定时器加速测试
- 测试边界条件:刚好超时和刚好不超时的情况
- 并发测试:多个超时Promise同时运行的情况
Jest测试示例:
javascript复制describe('timeoutPromise', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('应在超时前完成时不抛出错误', async () => {
const fastPromise = Promise.resolve('done');
const testPromise = timeoutPromise(fastPromise, 1000);
// 让所有pending的Promise先执行
await Promise.resolve();
await expect(testPromise).resolves.toBe('done');
});
it('应在超时时拒绝Promise', async () => {
const pendingPromise = new Promise(() => {}); // 永远不会resolve
const testPromise = timeoutPromise(pendingPromise, 1000);
// 推进时间
jest.advanceTimersByTime(1000);
await expect(testPromise).rejects.toThrow('Timeout');
});
it('应在原始Promise拒绝时传递错误', async () => {
const failingPromise = Promise.reject(new Error('Original error'));
const testPromise = timeoutPromise(failingPromise, 1000);
await expect(testPromise).rejects.toThrow('Original error');
});
it('应清理定时器以防内存泄漏', async () => {
const mockClearTimeout = jest.spyOn(global, 'clearTimeout');
const resolvingPromise = Promise.resolve('done');
await timeoutPromise(resolvingPromise, 1000);
expect(mockClearTimeout).toHaveBeenCalled();
mockClearTimeout.mockRestore();
});
});
9.2 E2E测试策略
对于端到端测试,可能需要真实等待:
javascript复制describe('API超时 (E2E)', () => {
it('应返回504当处理时间超过配置', async () => {
// 启动一个测试服务器,配置3秒超时
const server = startTestServer({ timeout: 3000 });
// 创建一个5秒才能完成的端点
server.get('/slow', async (req, res) => {
await new Promise(resolve => setTimeout(resolve, 5000));
res.send('ok');
});
const start = Date.now();
const response = await fetch(`${server.url}/slow`);
const duration = Date.now() - start;
expect(response.status).toBe(504);
expect(duration).toBeGreaterThanOrEqual(2900);
expect(duration).toBeLessThan(3500); // 允许一些缓冲
await server.close();
});
});
9.3 调试技巧
调试超时问题时的一些有用技巧:
- 添加调试标识:为每个超时Promise添加唯一ID,方便追踪
- 记录时间线:记录Promise创建、超时触发和实际完成的时间点
- 可视化工具:使用Chrome DevTools的Performance面板分析
调试帮助函数示例:
javascript复制class DebuggableTimeout {
constructor() {
this.pending = new Map();
this.nextId = 0;
}
create(promise, timeout, description) {
const id = ++this.nextId;
const start = Date.now();
this.pending.set(id, {
description,
start,
timeout,
stack: new Error().stack
});
const timer = setTimeout(() => {
console.warn(`Timeout ${id} "${description}" triggered after ${Date.now() - start}ms`);
this.pending.delete(id);
}, timeout);
const wrapped = promise.finally(() => {
clearTimeout(timer);
const duration = Date.now() - start;
if (this.pending.has(id)) {
console.log(`Operation ${id} "${description}" completed in ${duration}ms`);
this.pending.delete(id);
}
});
return wrapped;
}
logPending() {
const now = Date.now();
console.log(`Pending timeouts (${this.pending.size}):`);
for (const [id, entry] of this.pending.entries()) {
console.log(
`#${id} "${entry.description}" - ` +
`started ${now - entry.start}ms ago, ` +
`timeout in ${entry.timeout - (now - entry.start)}ms\n` +
`Created at: ${entry.stack}`
);
}
}
}
// 使用示例
const debugTimeout = new DebuggableTimeout();
// 在应用代码中
function fetchWithDebug(url, timeout) {
return debugTimeout.create(
fetch(url),
timeout,
`Fetch ${url}`
);
}
// 当怀疑有未清理的Promise时
setInterval(() => {
debugTimeout.logPending();
}, 10000);
10. 未来演进与替代方案
10.1 AbortSignal的未来发展
AbortController/AbortSignal正在被更多API支持:
- Node.js支持:从v15开始内置支持
- 更多Web API:如WebSocket、WebRTC等正在添加支持
- 组合信号:可以组合多个AbortSignal
组合信号示例:
javascript复制async function fetchWithMultipleSignals(url, options) {
const controller = new AbortController();
const timeoutSignal = AbortSignal.timeout(3000);
// 当任一信号触发时,controller就会abort
timeoutSignal.addEventListener('abort', () => {
controller.abort();
});
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
return response;
} catch (error) {
if (timeoutSignal.aborted) {
console.error('请求超时');
}
throw error;
}
}
10.2 可取消的Async Functions
TC39正在讨论的提案可能引入直接取消async function的机制:
javascript复制// 未来可能的语法 (目前只是提案)
async function cancellableTask({ signal }) {
if (signal.aborted) throw new Error('已取消');
await someAsyncWork();
signal.throwIfAborted();
await moreAsyncWork();
}
const controller = new AbortController();
const taskPromise = cancellableTask({ signal: controller.signal });
// 取消任务
controller.abort();
