Promise最佳实践
本章将介绍Promise使用的最佳实践,帮助您写出更健壮、更易维护的异步代码,避免常见的陷阱和错误。
7.1 命名规范和代码组织
良好的命名和代码组织是编写高质量Promise代码的基础。
清晰的函数命名
示例:清晰的Promise函数命名
// 好的命名
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
// 实现
});
}
function validateUserCredentials(username, password) {
return new Promise((resolve, reject) => {
// 实现
});
}
function saveUserPreferences(userId, preferences) {
return new Promise((resolve, reject) => {
// 实现
});
}
// 使用
fetchUserData(123)
.then(user => validateUserCredentials(user.username, user.password))
.then(isValid => {
if (isValid) {
return saveUserPreferences(123, { theme: 'dark' });
}
throw new Error('用户验证失败');
})
.then(() => {
console.log('用户偏好设置保存成功');
})
.catch(error => {
console.error('操作失败:', error.message);
});
避免嵌套Promise
示例:避免嵌套Promise
// 不好的做法:嵌套Promise
fetchUserData(userId)
.then(user => {
return fetchUserPosts(user.id)
.then(posts => {
return fetchPostComments(posts[0].id)
.then(comments => {
return { user, posts, comments };
});
});
})
.then(data => {
console.log(data);
});
// 好的做法:链式调用
fetchUserData(userId)
.then(user => {
return fetchUserPosts(user.id).then(posts => ({ user, posts }));
})
.then(({ user, posts }) => {
return fetchPostComments(posts[0].id).then(comments => ({ user, posts, comments }));
})
.then(data => {
console.log(data);
});
// 更好的做法:返回Promise进行链式调用
fetchUserData(userId)
.then(user => {
return fetchUserPosts(user.id)
.then(posts => ({ user, posts }));
})
.then(({ user, posts }) => {
return fetchPostComments(posts[0].id)
.then(comments => ({ user, posts, comments }));
})
.then(({ user, posts, comments }) => {
console.log('用户:', user);
console.log('帖子:', posts);
console.log('评论:', comments);
});
7.2 错误处理最佳实践
正确的错误处理是构建健壮应用的关键。
始终处理Promise拒绝
示例:完整的错误处理
// 好的做法:始终处理错误
function performUserOperation(userId) {
return fetchUserData(userId)
.then(user => {
if (!user.isActive) {
throw new Error('用户账户已被禁用');
}
return processUserData(user);
})
.then(processedData => {
return saveProcessedData(processedData);
})
.then(saveResult => {
console.log('操作成功完成');
return saveResult;
})
.catch(error => {
// 统一错误处理
console.error('用户操作失败:', error.message);
// 根据错误类型进行不同处理
if (error.message.includes('网络')) {
showNetworkErrorMessage();
} else if (error.message.includes('权限')) {
showPermissionErrorMessage();
} else {
showGenericErrorMessage();
}
// 重新抛出错误供上层处理
throw error;
});
}
// 使用
performUserOperation(123)
.then(result => {
// 操作成功
updateUI(result);
})
.catch(error => {
// 错误已在performUserOperation中处理,这里可以进行额外处理
logErrorToServer(error);
});
区分不同类型的错误
示例:自定义错误类型
// 自定义错误类
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = 'NetworkError';
}
}
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
class PermissionError extends Error {
constructor(message) {
super(message);
this.name = 'PermissionError';
}
}
// 使用自定义错误
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
if (!userId) {
reject(new ValidationError('用户ID不能为空'));
}
// 模拟网络请求
setTimeout(() => {
if (Math.random() > 0.8) {
reject(new NetworkError('网络连接失败'));
} else if (userId < 0) {
reject(new PermissionError('权限不足'));
} else {
resolve({ id: userId, name: `用户${userId}` });
}
}, 1000);
});
}
// 根据错误类型处理
fetchUserData(-1)
.then(user => {
console.log('用户数据:', user);
})
.catch(error => {
if (error instanceof ValidationError) {
console.error('验证错误:', error.message);
showValidationMessage(error.message);
} else if (error instanceof NetworkError) {
console.error('网络错误:', error.message);
showNetworkErrorMessage(error.message);
} else if (error instanceof PermissionError) {
console.error('权限错误:', error.message);
showPermissionErrorMessage(error.message);
} else {
console.error('未知错误:', error.message);
showGenericErrorMessage();
}
});
7.3 Promise链的最佳实践
编写清晰、可维护的Promise链需要遵循一些最佳实践。
保持链的简洁性
示例:简洁的Promise链
// 好的做法:每个then只做一件事
fetchUserData(userId)
.then(validateUser)
.then(transformUserData)
.then(saveUserData)
.then(notifyUser)
.then(result => {
console.log('所有操作完成:', result);
return result;
})
.catch(handleError);
// 辅助函数
function validateUser(user) {
if (!user || !user.id) {
throw new Error('无效的用户数据');
}
return user;
}
function transformUserData(user) {
return {
...user,
fullName: `${user.firstName} ${user.lastName}`,
lastModified: new Date().toISOString()
};
}
function saveUserData(user) {
// 模拟保存操作
return new Promise(resolve => {
setTimeout(() => resolve(user), 500);
});
}
function notifyUser(user) {
console.log(`用户 ${user.fullName} 的数据已更新`);
return user;
}
function handleError(error) {
console.error('操作失败:', error.message);
showErrorMessage('操作失败,请稍后重试');
}
合理使用async/await
示例:async/await与Promise的结合
// 在适当的地方使用async/await
async function processUserBatch(userIds) {
try {
// 并行处理多个用户
const userPromises = userIds.map(id => fetchUserData(id));
const users = await Promise.all(userPromises);
// 串行处理用户数据
const processedUsers = [];
for (const user of users) {
const processedUser = await processSingleUser(user);
processedUsers.push(processedUser);
}
// 保存所有处理后的用户数据
const savePromises = processedUsers.map(user => saveUserData(user));
const saveResults = await Promise.all(savePromises);
return saveResults;
} catch (error) {
console.error('批量处理用户失败:', error);
throw error;
}
}
// 辅助函数
async function processSingleUser(user) {
// 模拟复杂的用户处理逻辑
await new Promise(resolve => setTimeout(resolve, 100));
return { ...user, processed: true };
}
async function saveUserData(user) {
// 模拟保存操作
await new Promise(resolve => setTimeout(resolve, 50));
return { ...user, saved: true };
}
// 使用
processUserBatch([1, 2, 3, 4, 5])
.then(results => {
console.log('批量处理完成:', results);
})
.catch(error => {
console.error('批量处理失败:', error);
});
7.4 性能优化最佳实践
在处理大量Promise时,需要注意性能优化。
控制并发数量
示例:控制Promise并发数量
// 控制并发数量的工具函数
function limitConcurrency(promises, limit) {
return new Promise((resolve, reject) => {
const results = [];
let index = 0;
let pending = 0;
let resolved = 0;
function executeNext() {
if (index >= promises.length && pending === 0) {
resolve(results);
return;
}
while (pending < limit && index < promises.length) {
const currentIndex = index++;
const promise = Promise.resolve(promises[currentIndex]);
pending++;
promise
.then(result => {
results[currentIndex] = result;
resolved++;
pending--;
executeNext();
})
.catch(error => {
reject(error);
});
}
}
executeNext();
});
}
// 使用示例
const tasks = Array.from({ length: 100 }, (_, i) => {
return () => new Promise(resolve => {
setTimeout(() => {
console.log(`任务 ${i} 完成`);
resolve(`结果 ${i}`);
}, Math.random() * 1000);
});
});
// 限制并发数量为5
limitConcurrency(tasks.map(task => task()), 5)
.then(results => {
console.log('所有任务完成,结果数量:', results.length);
})
.catch(error => {
console.error('任务执行失败:', error);
});
避免创建不必要的Promise
示例:避免不必要的Promise创建
// 工具函数:确保返回Promise
function ensurePromise(value) {
return value instanceof Promise ? value : Promise.resolve(value);
}
// 好的做法:统一处理同步和异步返回值
function getData(source) {
if (source === 'cache') {
// 同步返回缓存数据
return ensurePromise(getCachedData());
} else {
// 异步获取新数据
return fetchNewData();
}
}
function getCachedData() {
// 模拟缓存数据
const cached = localStorage.getItem('userData');
return cached ? JSON.parse(cached) : null;
}
function fetchNewData() {
// 模拟网络请求
return new Promise(resolve => {
setTimeout(() => {
const data = { id: 1, name: '用户1' };
resolve(data);
}, 1000);
});
}
// 使用
getData('cache')
.then(data => {
if (data) {
console.log('使用缓存数据:', data);
} else {
return getData('network');
}
})
.then(data => {
if (data) {
console.log('获取到数据:', data);
}
})
.catch(error => {
console.error('获取数据失败:', error);
});
7.5 测试Promise代码
正确测试Promise代码对于确保应用质量非常重要。
单元测试Promise
示例:Promise单元测试
// 被测试的函数
function divide(a, b) {
return new Promise((resolve, reject) => {
if (b === 0) {
reject(new Error('除数不能为零'));
} else {
resolve(a / b);
}
});
}
// 测试代码(使用Jest或其他测试框架)
describe('divide函数测试', () => {
test('正常除法运算', () => {
return expect(divide(10, 2)).resolves.toBe(5);
});
test('除零错误', () => {
return expect(divide(10, 0)).rejects.toThrow('除数不能为零');
});
test('使用async/await测试', async () => {
const result = await divide(15, 3);
expect(result).toBe(5);
});
test('使用async/await测试错误情况', async () => {
await expect(divide(10, 0)).rejects.toThrow('除数不能为零');
});
});
// 更复杂的测试示例
function fetchUserDataWithRetry(userId, maxRetries = 3) {
let attempts = 0;
function attempt() {
attempts++;
return fetchUserData(userId)
.catch(error => {
if (attempts < maxRetries) {
return attempt();
}
throw error;
});
}
return attempt();
}
// 测试重试机制
describe('fetchUserDataWithRetry测试', () => {
beforeEach(() => {
// 重置模拟
attempts = 0;
});
test('成功获取用户数据', async () => {
// 模拟第一次就成功
jest.spyOn(global, 'fetchUserData').mockResolvedValue({ id: 1, name: '用户1' });
const user = await fetchUserDataWithRetry(1);
expect(user).toEqual({ id: 1, name: '用户1' });
expect(fetchUserData).toHaveBeenCalledTimes(1);
});
test('重试后成功获取用户数据', async () => {
// 模拟前两次失败,第三次成功
jest.spyOn(global, 'fetchUserData')
.mockRejectedValueOnce(new Error('网络错误'))
.mockRejectedValueOnce(new Error('网络错误'))
.mockResolvedValueOnce({ id: 1, name: '用户1' });
const user = await fetchUserDataWithRetry(1, 3);
expect(user).toEqual({ id: 1, name: '用户1' });
expect(fetchUserData).toHaveBeenCalledTimes(3);
});
test('重试次数用尽后失败', async () => {
// 模拟每次都失败
jest.spyOn(global, 'fetchUserData').mockRejectedValue(new Error('网络错误'));
await expect(fetchUserDataWithRetry(1, 2)).rejects.toThrow('网络错误');
expect(fetchUserData).toHaveBeenCalledTimes(2);
});
});
7.6 调试和监控
有效的调试和监控可以帮助快速定位和解决问题。
添加调试信息
示例:Promise调试工具
// 调试工具函数
function debugPromise(promise, label, options = {}) {
const { logSuccess = true, logError = true, logStart = true } = options;
if (logStart) {
console.log(`[开始] ${label}`);
}
return promise
.then(result => {
if (logSuccess) {
console.log(`[成功] ${label}:`, result);
}
return result;
})
.catch(error => {
if (logError) {
console.error(`[失败] ${label}:`, error);
}
throw error;
});
}
// 使用调试工具
const fetchUserPromise = debugPromise(
fetchUserData(123),
'获取用户数据',
{ logStart: true, logSuccess: true, logError: true }
);
const fetchPostsPromise = debugPromise(
fetchUserPosts(123),
'获取用户帖子',
{ logStart: false } // 不记录开始
);
Promise.all([fetchUserPromise, fetchPostsPromise])
.then(([user, posts]) => {
console.log('所有数据获取完成');
})
.catch(error => {
console.error('数据获取失败:', error);
});
性能监控
示例:Promise性能监控
// 性能监控装饰器
function monitorPromise(promiseFn, name) {
return function(...args) {
const startTime = performance.now();
console.log(`[监控] 开始执行 ${name}`);
return promiseFn.apply(this, args)
.then(result => {
const endTime = performance.now();
const duration = endTime - startTime;
console.log(`[监控] ${name} 执行成功,耗时: ${duration.toFixed(2)}ms`);
return result;
})
.catch(error => {
const endTime = performance.now();
const duration = endTime - startTime;
console.error(`[监控] ${name} 执行失败,耗时: ${duration.toFixed(2)}ms`, error);
throw error;
});
};
}
// 使用性能监控
const monitoredFetchUserData = monitorPromise(fetchUserData, '获取用户数据');
const monitoredProcessUserData = monitorPromise(processUserData, '处理用户数据');
monitoredFetchUserData(123)
.then(user => monitoredProcessUserData(user))
.then(processedUser => {
console.log('用户处理完成:', processedUser);
})
.catch(error => {
console.error('用户处理失败:', error);
});
7.7 常见陷阱和避免方法
了解常见的Promise陷阱并知道如何避免它们。
忘记处理Promise拒绝
示例:避免忘记处理Promise拒绝
// 不好的做法:忘记处理拒绝
const promise = fetchUserData(123);
// 如果fetchUserData失败,这里没有处理,可能导致未捕获的Promise拒绝
// 好的做法:始终处理拒绝
fetchUserData(123)
.then(user => {
console.log(user);
})
.catch(error => {
console.error('获取用户数据失败:', error);
});
// 或者使用async/await
async function handleUser() {
try {
const user = await fetchUserData(123);
console.log(user);
} catch (error) {
console.error('获取用户数据失败:', error);
}
}
在Promise链中吞掉错误
示例:避免吞掉错误
// 不好的做法:吞掉错误
fetchUserData(123)
.then(user => {
console.log(user);
})
.catch(error => {
// 只记录错误,不重新抛出或处理
console.log('记录错误到日志');
// 错误被吞掉,上层调用者不知道发生了错误
});
// 好的做法:正确处理错误
fetchUserData(123)
.then(user => {
console.log(user);
})
.catch(error => {
console.log('记录错误到日志');
// 显示用户友好的错误信息
showErrorMessage('获取用户数据失败');
// 如果需要,重新抛出错误
throw error;
});
// 或者在catch中返回默认值
fetchUserData(123)
.then(user => {
return user;
})
.catch(error => {
console.error('获取用户数据失败:', error);
// 返回默认用户数据
return { id: 0, name: '默认用户' };
})
.then(user => {
// 无论前面是否出错,都会执行到这里
displayUser(user);
});
提示:使用ESLint等工具可以帮助发现未处理的Promise错误。在开发过程中,始终要注意处理Promise的拒绝状态。