JS 异步编程封神指南:从回调地狱到 async/await,90% 开发者都在用的避坑技巧
我当初第一次写的时候,随手在普通函数里加了个 await,结果控制台红一片,调试了十分钟才反应过来忘了加 async,尴尬得不行。用 async/await 写的时候,先让加载动画显示,然后在 try 里先后 await 两个请求,成功了就渲染页面,失败了就在 catch 里显示 “加载失败”,最后不管成功失败都关掉加载动画。就像你煮泡面的时候,总不会守着锅等水开,肯定会先去拿调料、找筷子,水开了
你有没有过这种经历?写 JS 的时候想先拿接口数据,结果代码跑太快,数据还没到就执行下一步了,页面直接一片空白?或者点击按钮触发两个操作,明明写了先后顺序,结果后面的操作先完成了?这其实都是 JS 的 “异步特性” 在搞鬼,今天就跟你好好掰扯掰扯这个让人又爱又恨的东西。
简单说就是 JS 是 “单线程” 的,一次只能干一件事。但遇到像请求接口、读本地文件、设置延迟这类要 “等” 的活儿,总不能让程序卡着不动吧?就像你煮泡面的时候,总不会守着锅等水开,肯定会先去拿调料、找筷子,水开了再回来下面 ——JS 的异步就是这个逻辑,把要等的活儿丢到 “后台”,先干别的,等活儿有结果了再回头处理。
最早处理异步全靠回调函数,比如用 jQuery 的 ajax 请求数据:
$.ajax({
url: 'user.json',
success: function(data) {
console.log('拿到用户数据', data);
},
error: function(err) {
console.log('请求失败', err);
}
});
这看着还行对不对?但要是遇到多个有依赖的异步操作,比如先拿用户 ID,再用 ID 拿订单,再用订单 ID 拿详情,代码就会变成这样:
$.ajax({
url: 'user.json',
success: function(user) {
$.ajax({
url: `order/${user.id}`,
success: function(order) {
$.ajax({
url: `detail/${order.id}`,
success: function(detail) {
console.log('订单详情', detail);
}
})
}
})
}
});
这缩进一层叠一层,能堆到屏幕右边去,老开发都叫它 “回调地狱”,别说改代码了,看着都头疼!
后来 ES6 出了 Promise,可算把大家从地狱里捞出来了。它把异步操作包装成一个 “承诺”,成功了就 “兑现”(resolve),失败了就 “反悔”(reject),然后用.then()链式调用,代码一下子就平了。刚才的例子用 Promise 改写是这样的:
function getUser() {
return new Promise((resolve, reject) => {
$.ajax({
url: 'user.json',
success: resolve,
error: reject
});
});
}
getUser()
.then(user => {
return new Promise((resolve) => {
$.ajax({url: `order/${user.id}`, success: resolve});
});
})
.then(order => {
$.ajax({url: `detail/${order.id}`, success: detail => {
console.log('订单详情', detail);
}});
})
.catch(err => {
console.log('哪里出错了', err);
});
是不是清爽多了?所有错误还能统一用.catch()处理,不用每个回调里都写 error 逻辑。
但.then()多了还是有点啰嗦,于是 ES2017 又出了 async/await—— 这简直是异步编程的 “语法糖”,把异步代码写出了同步的感觉。还是刚才的需求,用 async/await 写是这样:
async function getOrderDetail() {
try {
const user = await getUser(); // 等拿到用户数据再往下走
const order = await new Promise(resolve => {
$.ajax({url: `order/${user.id}`, success: resolve});
});
const detail = await new Promise(resolve => {
$.ajax({url: `detail/${order.id}`, success: resolve});
});
console.log('订单详情', detail);
} catch (err) {
console.log('出错了', err);
}
}
你看看,没有嵌套,没有一堆.then(),逻辑从头到尾顺下来,跟写 “先煮水、再下面、最后放调料” 的步骤一样直观。我第一次用的时候都惊了,这也太方便了!
不过这里有个坑得提醒你 ——await 只能用在 async 函数里,不然会直接报错。我当初第一次写的时候,随手在普通函数里加了个 await,结果控制台红一片,调试了十分钟才反应过来忘了加 async,尴尬得不行。还有哦,Promise 有三种状态:pending(进行中)、fulfilled(成功)、rejected(失败),状态一旦变了就改不了了,比如已经成功的 Promise,再调用 reject 也没用,这点得记牢。
亲测有效,上次做商品列表页,要先请求分类数据,再根据分类请求商品,还要处理加载状态。用 async/await 写的时候,先让加载动画显示,然后在 try 里先后 await 两个请求,成功了就渲染页面,失败了就在 catch 里显示 “加载失败”,最后不管成功失败都关掉加载动画。整个逻辑清晰得很,同事看了都问我是不是重构了代码。
说到这儿可能有人会问:如果多个异步操作之间没依赖呢?比如同时请求商品分类和用户信息,总不能一个个 await 等着吧?这时候就该用 Promise.all () 了,它能把多个 Promise 打包成一个,同时触发所有异步操作,等全部成功了再一起处理结果。比如:
async function getMultiData() {
const [category, user] = await Promise.all([
getCategory(),
getUser()
]);
console.log('分类', category, '用户', user);
}
这样比挨个等快多了,效率直接拉满。
不过这里有个点想跟大家聊聊:你平时处理异步错误的时候,是习惯在每个 await 后面加.catch (),还是统一用 try/catch 包起来?我之前试过前者,结果代码里全是.catch (),看着很乱;后来换成统一 try/catch,逻辑反而更清晰。但也有同事说某些关键请求得单独捕获错误,不然一个错了全停了。你们是怎么处理的?评论区说说看~
我是【即兴小索奇】,点击关注,后台回复 领取,获取更多相关资源
更多推荐
所有评论(0)