【万字长文 + 代码实例】Javascript 异步函数 Promise详解:定义,方法,异常处理
Promise详解:异步操作的核心机制
Promise构造函数语法
new Promise(resolveFunc, rejectFunc) {
// 异步操作代码
}
resolveFunc: 一个函数,当异步操作成功时调用,将Promise状态改为fulfilledrejectFunc: 一个函数,当异步操作失败时调用,将Promise状态改为rejected
resolveFunc(value); // 操作成功,传入成功值
rejectFunc(reason); // 操作失败,传入失败原因
参数含义:
value: 任何JavaScript值,表示操作成功的结果reason: 通常是Error对象,表示操作失败的原因
特殊情况:
- 如果
value是另一个Promise对象,则当前Promise的状态会与该Promise保持一致 - 如果省略参数,Promise将以
undefined值完成或拒绝
-
Promise的生命周期
-
创建阶段:构造函数实例化Promise对象,同时生成与之绑定的resolveFunc和rejectFunc函数
-
执行阶段:executor函数立即同步执行,通常会启动某个异步任务
-
状态改变:异步任务完成后,通过调用resolveFunc或rejectFunc改变Promise状态
-
后续处理:Promise状态确定后,触发通过then()、catch()或finally()注册的回调函数
Promise状态特性:
- 单向流转:Promise一旦从pending转为fulfilled或rejected,状态就不可再变
- 一次解析:只有第一次调用resolveFunc或rejectFunc才会影响Promise状态,后续调用被忽略
- 状态独立:Promise状态的改变完全由executor内部决定,与executor函数本身的返回值无关
什么叫"解析一次"?
“解析一次”(resolved once)意味着Promise的状态只能被改变一次。这不是指网络请求只发送一次,而是指一旦Promise的状态从pending变为fulfilled或rejected,后续再调用resolve或reject函数都不会产生效果。
例如,如果你在异步操作中多次调用resolve函数:
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("第一次结果"), 1000);
setTimeout(() => resolve("第二次结果"), 2000); // 这次调用将被忽略
setTimeout(() => reject("错误信息"), 3000); // 这次调用也被忽略
});
promise.then(value => console.log(value)); // 只会输出"第一次结果"
应用场景与代码演示
场景一:封装传统回调式API:将Node.js中基于回调的文件操作转换为Promise形式
// 基于回调的文件系统API
const fs = require('fs');
// 封装为Promise版本
function readFilePromise(path, options = 'utf8') {
return new Promise((resolve, reject) => {
fs.readFile(path, options, (error, data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}
// 使用Promise
readFilePromise('./config.json')
.then(data => {
const config = JSON.parse(data);
console.log('成功读取配置:', config);
return config;
})
.catch(error => {
console.error('读取配置失败:', error.message);
});
// 使用async/await简化代码
async function loadConfig() {
try {
const data = await readFilePromise('./config.json');
const config = JSON.parse(data);
console.log('成功读取配置:', config);
return config;
} catch (error) {
console.error('读取配置失败:', error.message);
return null;
}
}
loadConfig();
场景二:实现可控超时的网络请求:给不能设置超时的API添加超时功能
// 创建可超时的fetch函数
function fetchWithTimeout(url, options = {}, timeout = 5000) {
return new Promise((resolve, reject) => {
// 设置超时定时器
const timeoutId = setTimeout(() => {
reject(new Error(`请求超时: 超过${timeout}ms`));
}, timeout);
// 发起实际请求
fetch(url, options)
.then(response => {
clearTimeout(timeoutId); // 清除超时定时器
resolve(response);
})
.catch(error => {
clearTimeout(timeoutId); // 清除超时定时器
reject(error);
});
});
}
// 使用示例:获取用户数据并展示
async function displayUserProfile(userId) {
try {
console.log('正在获取用户数据...');
const response = await fetchWithTimeout(
`https://api.example.com/users/${userId}`,
{ headers: { 'Authorization': 'Bearer token123' } },
3000 // 3秒超时
);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const userData = await response.json();
// 使用获取的数据
console.log('用户资料:', userData);
document.getElementById('username').textContent = userData.name;
document.getElementById('email').textContent = userData.email;
document.getElementById('profile-pic').src = userData.avatar;
return userData;
} catch (error) {
console.error('获取用户数据失败:', error.message);
document.getElementById('user-profile').innerHTML =
`<div class="error">加载用户数据时出错: ${error.message}</div>`;
return null;
}
}
// 调用函数显示用户资料
displayUserProfile('user123');
常见误区
误区一:认为executor中的return会影响Promise结果
// ❌ 错误理解
const promise = new Promise((resolve, reject) => {
return "成功"; // 这不会影响Promise的状态
});
// ✅ 正确做法
const promise = new Promise((resolve, reject) => {
resolve("成功"); // 这才会将Promise状态设为fulfilled
});
误区二:忽略Promise的错误处理
// ❌ 错误做法:未处理拒绝状态
fetchData().then(data => {
processData(data);
});
// ✅ 正确做法:添加错误处理
fetchData()
.then(data => {
processData(data);
})
.catch(error => {
console.error("处理数据失败:", error);
});
误区三:Promise链中断问题
// ❌ 错误做法:链中断
getData()
.then(data => {
// 这里忘记return处理结果
processData(data);
})
.then(result => {
// result实际上是undefined,因为上一个then没有返回值
displayResult(result);
});
// ✅ 正确做法:保持链连接
getData()
.then(data => {
// 返回处理结果以传递给下一个then
return processData(data);
})
.then(result => {
displayResult(result);
});
Promise有几种写法?何时return?
为什么在async函数中很少看到return语句?因为:
- async函数会自动将返回值包装为Promise
- 在使用await时,通常直接使用变量存储结果,流程更加线性
- 对于错误处理,习惯使用try/catch而不是返回错误状态的Promise
然而,在async函数中使用return是完全有效的,它相当于resolve这个值:
// 这两种方式效果相同
async function foo() {
return "成功";
}
function foo() {
return Promise.resolve("成功");
}
假设我们需要从API获取用户数据,然后获取该用户的订单,最后计算订单总额。下面展示不同实现方式
// 模拟API函数
function getUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId <= 0) {
reject(new Error("无效的用户ID"));
return;
}
const userData = { id: userId, name: "张三", email: "zhangsan@example.com" };
resolve(userData);
}, 500);
});
}
function getUserOrders(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId <= 0) {
reject(new Error("无效的用户ID"));
return;
}
const orders = [
{ id: 101, amount: 100 },
{ id: 102, amount: 200 }
];
resolve(orders);
}, 500);
});
}
function calculateDiscount(total) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const discount = total > 200 ? 0.1 : 0;
resolve(discount);
}, 300);
});
}
// 1. 清晰的链式写法(推荐)- 每个步骤都有明确的return
getUserData(123)
.then(user => {
console.log("获取到用户:", user.name);
return getUserOrders(user.id); // 必须return,传递Promise到下一步
})
.then(orders => {
console.log("获取到订单数:", orders.length);
const total = orders.reduce((sum, order) => sum + order.amount, 0);
console.log("原始订单总额:", total);
return total; // 必须return,传递计算结果到下一步
})
.then(total => {
return calculateDiscount(total); // 必须return,传递Promise到下一步
})
.then(discount => {
console.log("折扣率:", discount * 100 + "%");
// 没有return,链到此结束
})
.catch(error => {
console.error("操作失败:", error.message);
});
// 2. 简洁的箭头函数写法(推荐用于简单操作)
console.log("\n简洁写法示例:");
getUserData(123)
.then(user => getUserOrders(user.id)) // 单行箭头函数隐式return
.then(orders => orders.reduce((sum, order) => sum + order.amount, 0)) // 隐式return
.then(total => calculateDiscount(total)) // 隐式return
.then(discount => console.log("折扣率:", discount * 100 + "%")) // 没有return值
.catch(error => console.error("操作失败:", error.message));
// 3. 使用中间变量存储Promise的写法(不推荐,但展示了过程)
console.log("\n使用中间变量的写法:");
const userPromise = getUserData(123);
userPromise.then(user => {
console.log("获取到用户:", user.name);
const ordersPromise = getUserOrders(user.id);
ordersPromise.then(orders => {
console.log("获取到订单数:", orders.length);
const total = orders.reduce((sum, order) => sum + order.amount, 0);
console.log("订单总额:", total);
const discountPromise = calculateDiscount(total);
discountPromise.then(discount => {
console.log("折扣率:", discount * 100 + "%");
const finalAmount = total * (1 - discount);
console.log("最终金额:", finalAmount);
});
});
});
方式3:使用async/await(最现代的方式)
// 复用上面的Promise版本的函数
async function processUserData(userId) {
try {
const user = await getUserData(userId);
console.log("用户:", user);
const orders = await getUserOrders(user.id);
const total = orders.reduce((sum, order) => sum + order.amount, 0);
console.log("订单总额:", total);
return total;
} catch (error) {
console.error("操作失败:", error);
return 0;
}
}
// 调用async函数
processUserData(123).then(total => {
console.log("处理完成,总额:", total);
});
何时需要return
- 当你需要将值传递给Promise链中的下一个then时:
.then(user => {
// 处理user数据
return getUserOrders(user.id); // 将新的Promise传递下去
})
- 当你返回一个Promise对象时:
return getUserOrders(user.id); // 返回Promise,下一个then会等待这个Promise解决 - 当你计算了新值需要在链中传递时:
.then(orders => { const total = orders.reduce((sum, order) => sum + order.amount, 0); return total; // 传递计算结果到下一个then })
何时不需要return
-
当使用单行箭头函数时,会自动隐式return:
.then(user => getUserOrders(user.id)) // 等同于 .then(user => { return getUserOrders(user.id); }) -
当你处于链的最后一个then,且不需要传递值时:
.then(result => { console.log("最终结果:", result); // 不需要return,链到此结束 }) -
在catch或finally块中,除非你想恢复链:
.catch(error => { console.error("发生错误:", error); // 通常不需要return })
最佳实践总结
-
始终在then回调中使用return,除非:在链的最后一步 / 使用单行箭头函数(隐式return)
-
避免创建不必要的中间变量来存储Promise:
// 不推荐 const userPromise = getUserData(123); userPromise.then(...) // 推荐 getUserData(123).then(...) -
利用箭头函数简化简单操作:
// 冗长写法 .then(total => { return calculateDiscount(total); }) // 简洁写法 .then(total => calculateDiscount(total)) // 更简洁 (当参数直接传递时) .then(calculateDiscount)
函数返回Promise与内联的区别
两种方法的核心区别在于封装方式和使用灵活性。下面详细解释这两种方式的区别:
内联executor方式
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("完成"), 1000);
});
函数返回Promise方式
function createDelayedPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => resolve("完成"), 1000);
});
}
const promise3 = createDelayedPromise();
区别:
-
可重用性: 内联方式:Promise只创建一次,不可重用 函数方式:每次调用函数时创建新的Promise,可以反复调用
-
参数: 内联方式*:不能轻松地传递参数(除非使用外部作用域的变量)函数方式:可以接受参数来自定义Promise行为
-
执行时机: 内联方式:Promise在定义时立即创建并开始执行 函数方式:只有在函数被调用时才创建并执行Promise
-
作用域和封装: 内联方式:可以直接访问外部作用域的变量 函数方式:提供更好的封装,内部变量作用域限制在函数内
-
代码组织: 内联方式:适合一次性使用的简单异步操作 函数方式:适合需要重复使用的复杂异步操作
function fetchData(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then(response => response.json())
.then(resolve)
.catch(reject);
});
}
// 可以多次调用并传入不同参数
fetchData('https://api.example.com/users');
fetchData('https://api.example.com/products');
实际应用对比
场景:用户登录
内联方式(适合一次性操作):
// 登录当前用户(特定场景,一次性操作)
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const loginPromise = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/login');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = () => xhr.status === 200 ? resolve(JSON.parse(xhr.response)) : reject(xhr.statusText);
xhr.onerror = () => reject('网络错误');
xhr.send(JSON.stringify({ username, password }));
});
loginPromise
.then(user => console.log('登录成功:', user))
.catch(error => console.error('登录失败:', error));
函数方式(适合可重用操作):
// 可重用的登录函数
function login(username, password) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/login');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = () => xhr.status === 200 ? resolve(JSON.parse(xhr.response)) : reject(xhr.statusText);
xhr.onerror = () => reject('网络错误');
xhr.send(JSON.stringify({ username, password }));
});
}
// 可以在应用的不同部分重复使用
login('user1', 'pass123')
.then(user => console.log('用户1登录成功:', user))
.catch(error => console.error('用户1登录失败:', error));
// 在另一个地方再次使用
document.getElementById('loginBtn').addEventListener('click', () => {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
login(username, password)
.then(handleLoginSuccess)
.catch(handleLoginError);
});
实际应用场景:数据缓存与刷新机制
在前端应用中,我们经常需要获取API数据,并实现缓存机制提高性能:
// 一个带缓存和过期机制的API请求函数
function fetchWithCache(url, options = {}) {
const cache = fetchWithCache.cache || (fetchWithCache.cache = new Map());
const cacheKey = url;
const cacheTTL = options.cacheTTL || 60000; // 默认缓存1分钟
// 检查是否有有效的缓存数据
const cachedItem = cache.get(cacheKey);
if (cachedItem && Date.now() - cachedItem.timestamp < cacheTTL) {
console.log(`[缓存] 使用缓存数据: ${url}`);
return Promise.resolve(cachedItem.data);
}
// 没有缓存或缓存过期,发起新请求
console.log(`[网络] 从API获取: ${url}`);
return fetch(url, options)
.then(response => {
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`);
}
return response.json();
})
.then(data => {
// 保存到缓存
cache.set(cacheKey, {
data,
timestamp: Date.now()
});
return data;
});
}
// 实际使用场景
function loadUserData(userId) {
return fetchWithCache(`/api/users/${userId}`);
}
function loadProductData(productId) {
return fetchWithCache(`/api/products/${productId}`);
}
// 在应用中使用
loadUserData(123)
.then(userData => {
console.log("用户数据:", userData);
// 做一些操作...
// 短时间内再次请求同一用户,会使用缓存
return loadUserData(123);
})
.then(cachedUserData => {
console.log("来自缓存的用户数据:", cachedUserData);
// 处理缓存数据...
});
// 同时,在另一部分代码中也可以使用这个函数
document.getElementById('loadBtn').addEventListener('click', () => {
const productId = document.getElementById('productId').value;
loadProductData(productId)
.then(renderProductDetails)
.catch(handleError);
});
Promise链中的错误处理与catch机制详解
Promise错误传播原理:
在Promise链中,错误会沿着链条向下传播,直到遇到第一个能处理它的rejection处理函数(即.then()的第二个参数或.catch())。这种机制称为"错误冒泡",类似于传统的try-catch块中的错误冒泡。.catch()可以捕获链中任何位置的未处理错误,您可以选择在每个步骤处理特定错误(使用.then()的第二个参数),也可以在链末尾使用.catch()集中处理所有错误。两种方法可以混合使用
错误处理策略与模式
- 逐层处理错误
可以在每个.then()中提供第二个参数来处理错误。允许在每一步定制错误处理逻辑。举个例子,这段代码展开
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve("foo");
}, 300);
});
myPromise
.then(handleFulfilledA, handleRejectedA)
.then(handleFulfilledB, handleRejectedB)
.then(handleFulfilledC, handleRejectedC);
如果展开,不使用独立的命名处理函数:
myPromise
.then(
// handleFulfilledA的实现
function(result) {
console.log("A处理成功状态:", result);
// 假设这里对结果进行某些处理
return result * 2; // 返回变换后的结果给下一个then
},
// handleRejectedA的实现
function(error) {
console.error("A处理失败状态:", error);
// 可以选择恢复链(返回一个值)
return 10; // 这会将Promise链恢复到fulfilled状态
// 或者继续拒绝(抛出错误或返回拒绝的Promise)
// throw new Error('仍然失败');
}
)
.then(
// handleFulfilledB的实现
function(result) {
console.log("B处理成功状态:", result);
// 这里可能会产生新的错误
if (result < 20) {
throw new Error('结果太小');
}
return result + 5;
},
// handleRejectedB的实现
function(error) {
console.error("B处理失败状态:", error);
// 同样可以选择恢复或继续拒绝
return 25; // 链恢复到fulfilled状态
}
)
.then(
// handleFulfilledC的实现
function(result) {
console.log("C处理成功状态:", result);
return "最终结果: " + result;
},
// handleRejectedC的实现
function(error) {
console.error("C处理失败状态:", error);
return "错误处理后的结果";
}
);
- 集中式错误处理
可以使用.catch()在链的末尾处理所有错误:
myPromise
.then(function(result) {
console.log("A处理成功:", result);
return result * 2;
})
.then(function(result) {
console.log("B处理成功:", result);
if (result < 20) {
throw new Error('结果太小');
}
return result + 5;
})
.then(function(result) {
console.log("C处理成功:", result);
return "最终结果: " + result;
})
.catch(function(error) {
// 处理链中任何地方发生的错误
console.error("链中有错误发生:", error);
return "错误已处理";
});
在rejection处理函数中,可以:
- 继续失败状态:通过抛出新错误或返回一个rejected Promise
.catch(error => {
throw new Error('处理错误失败');
// 或 return Promise.reject(new Error('处理错误失败'));
})
- 恢复到成功状态:通过返回一个正常值或resolved Promise
.catch(error => {
console.log('处理了错误,恢复正常流程');
return '恢复值'; // 链将继续以fulfilled状态执行
})
错误恢复后继续链
fetchData()
.then(data => {
// 正常处理数据
return processData(data);
})
.catch(error => {
console.error("获取或处理数据时出错:", error);
// 恢复到默认数据
return getDefaultData();
})
.then(result => {
// 这里的result可能来自processData(data)或getDefaultData()
displayResult(result);
});
错误分类处理
fetchUserData(userId)
.then(userData => {
return updateUserProfile(userData);
})
.catch(error => {
if (error.name === 'NetworkError') {
showOfflineMessage();
return Promise.reject(error); // 继续拒绝状态
} else if (error.name === 'AuthError') {
redirectToLogin();
return Promise.reject(error); // 继续拒绝状态
} else {
// 其他错误类型可以尝试恢复
console.warn("非致命错误,使用缓存数据:", error);
return getCachedUserData(userId);
}
})
.then(profile => {
// 只有在没有NetworkError或AuthError的情况下才会执行
renderUserProfile(profile);
})
.catch(error => {
// 处理所有未恢复的错误
console.error("无法显示用户配置文件:", error);
showErrorPage();
});
更多推荐

所有评论(0)