JavaScript Promise链式调用:优雅处理连续异步任务
项目开发中经常遇到这种情况:先获取用户信息,再用用户ID查订单,之后用订单ID查详情。这种连续异步操作如果用回调函数写,很快就会陷入回调地狱。而Promise链式调用(Promise Chaining)就是解决这个问题的利器。
什么是Promise链式调用?
简单说,就是在then()方法里返回一个新的Promise,让下一个then()等待这个新Promise完成后再执行。这样就能把多个异步操作像链条一样串起来。
// 基本结构
doFirstThing()
.then(result => doSecondThing(result))
.then(result => doThirdThing(result))
.then(finalResult => console.log('最终结果:', finalResult))
.catch(error => console.error('出错了:', error));
链式调用的两种写法
写法一:显式返回新Promise
代码号学习: 用户登录流程
function login(username, password) {
return new Promise((resolve, reject) => {
// 模拟登录验证
setTimeout(() => {
if (username === 'admin' && password === '123') {
resolve({ userId: 1, token: 'abc123' });
} else {
reject('用户名或密码错误');
}
}, 1000);
});
}
function getUserInfo(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
userId: userId,
name: '张三',
email: 'zhangsan@example.com'
});
}, 500);
});
}
function getUserOrders(userId) {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{ id: 1001, amount: 299 },
{ id: 1002, amount: 599 }
]);
}, 800);
});
}
// 链式调用
login('admin', '123')
.then(user => {
console.log('登录成功:', user);
return getUserInfo(user.userId);
})
.then(userInfo => {
console.log('用户信息:', userInfo);
return getUserOrders(userInfo.userId);
})
.then(orders => {
console.log('用户订单:', orders);
console.log('订单总数:', orders.length);
})
.catch(error => {
console.error('处理失败:', error);
});
写法二:直接传递函数引用
如果下一个函数直接接受上一个结果作为参数,可以简化写法:
// 前提:getUserInfo 接受 userId 参数
login('admin', '123')
.then(user => getUserInfo(user.userId))
.then(userInfo => getUserOrders(userInfo.userId))
.then(orders => {
console.log('最终拿到的订单:', orders);
});
// 更简洁的写法(参数自动传递)
login('admin', '123')
.then(getUserInfo) // 自动传入 user 对象
.then(getUserOrders) // 自动传入 userInfo
.then(orders => {
console.log('最终拿到的订单:', orders);
});
个人经验:第二种写法代码更简洁,但前提是函数设计要合理——刚好接收上一个Promise返回的值作为参数。我一般会在项目里统一这种风格,让代码更易读。
链式调用的实际应用场景
场景1:数据处理流水线
// 读取文件 -> 处理数据 -> 保存结果
function readFile(filePath) {
return new Promise((resolve) => {
console.log(`读取文件: ${filePath}`);
setTimeout(() => {
resolve('原始数据: 1,2,3,4,5');
}, 1000);
});
}
function processData(data) {
return new Promise((resolve) => {
console.log('处理数据中...');
setTimeout(() => {
const numbers = data.split(':')[1].trim().split(',').map(Number);
const processed = numbers.map(n => n * 2);
resolve(`处理结果: ${processed.join(',')}`);
}, 800);
});
}
function saveResult(result) {
return new Promise((resolve) => {
console.log('保存结果...');
setTimeout(() => {
console.log('保存成功:', result);
resolve('操作完成');
}, 600);
});
}
// 链式执行
readFile('/data/info.txt')
.then(processData)
.then(saveResult)
.then(message => {
console.log('最终状态:', message);
});
场景2:分页数据获取
function fetchPage(pageNum) {
return new Promise((resolve) => {
console.log(`获取第${pageNum}页数据`);
setTimeout(() => {
// 模拟分页数据
const data = Array.from({ length: 10 }, (_, i) => ({
id: (pageNum - 1) * 10 + i + 1,
name: `商品${(pageNum - 1) * 10 + i + 1}`
}));
resolve({
page: pageNum,
data: data,
hasMore: pageNum < 5
});
}, 600);
});
}
function loadAllPages() {
let allData = [];
function loadNext(pageNum) {
return fetchPage(pageNum)
.then(result => {
allData = allData.concat(result.data);
console.log(`第${pageNum}页加载完成,累计${allData.length}条`);
if (result.hasMore) {
return loadNext(pageNum + 1);
} else {
return allData;
}
});
}
return loadNext(1);
}
// 使用
loadAllPages()
.then(allData => {
console.log('所有数据加载完成,总数:', allData.length);
});
需要注意的坑:多个then不是链式调用
新手常犯的错误:在一个Promise上多次调用then,这不是链式调用,而是添加多个独立的处理器:
let promise = new Promise((resolve) => {
resolve(5);
});
// 错误写法:这不是链式调用!
promise.then(result => {
console.log('处理器1:', result);
return result * 2;
});
promise.then(result => {
console.log('处理器2:', result); // 还是5,不是10
return result * 3;
});
promise.then(result => {
console.log('处理器3:', result); // 还是5,不是15
return result * 4;
});
// 输出:
// 处理器1: 5
// 处理器2: 5
// 处理器3: 5
正确写法:要链式调用,必须连续使用then:
promise
.then(result => {
console.log('处理器1:', result);
return result * 2;
})
.then(result => {
console.log('处理器2:', result); // 现在是10
return result * 3;
})
.then(result => {
console.log('处理器3:', result); // 现在是30
return result * 4;
});
// 输出:
// 处理器1: 5
// 处理器2: 10
// 处理器3: 30
错误处理的实践
链式调用中,错误会沿着链条往下传递,直到被catch捕获:
function step1() {
return new Promise((resolve) => {
console.log('步骤1执行');
resolve(10);
});
}
function step2(num) {
return new Promise((resolve, reject) => {
console.log('步骤2执行');
if (num > 5) {
reject('步骤2出错:数字太大');
} else {
resolve(num * 2);
}
});
}
function step3(num) {
return new Promise((resolve) => {
console.log('步骤3执行');
resolve(num + 10);
});
}
// 错误会被后面的catch捕获
step1()
.then(step2)
.then(step3)
.then(final => {
console.log('最终结果:', final);
})
.catch(error => {
console.log('捕获到错误:', error); // 步骤2出错会直接跳到这里
});
// 也可以在不同位置捕获错误
step1()
.then(step2)
.catch(error => {
console.log('步骤2出错,但可以恢复');
return 20; // 返回一个默认值继续执行
})
.then(step3)
.then(final => {
console.log('最终结果:', final);
})
.catch(error => {
console.log('之后的错误处理');
});
个人建议:在较长的Promise链中,可以考虑在中间加catch来处理可恢复的错误,给用户更好的体验。比如某个步骤出错时,用默认值继续,而不是整个流程都中断。
链式调用与async/await的对比
Promise链式调用是async/await的基础,两者可以混用:
// Promise链式调用
function loadUserData(userId) {
return getUser(userId)
.then(user => getPosts(user.id))
.then(posts => {
// 在链中混合async/await
const comments = await getComments(posts[0].id);
return { user, posts, comments };
})
.catch(error => handleError(error));
}
// 纯async/await写法更直观
async function loadUserData(userId) {
try {
const user = await getUser(userId);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
return { user, posts, comments };
} catch (error) {
handleError(error);
}
}
个人观点:新代码我更倾向用async/await,但理解Promise链式调用很重要,因为:
-
很多老项目还在用Promise链
-
Promise.all、Promise.race等方法返回的就是Promise
-
async/await本质上还是Promise,只是语法糖
课程知识要点
-
Promise链式调用:在then()中返回新Promise,实现异步操作顺序执行
-
两种写法:显式返回Promise vs 直接传递函数引用
-
错误传递:链中任何环节出错,都会跳到最近的catch
-
与多个then的区别:链式调用是连续执行,多个then是并行添加处理器
-
实际应用:数据流水线、分页加载、依赖请求等场景
-
与async/await关系:链式调用是基础,async/await是更现在的写法