JavaScript 异步编程完整教程(setTimeout、Promise、async/await)
上一篇学完 ES6+,你已经能看懂大部分现代 JS 代码了。但有一类代码你可能还是觉得”怪”——比如 setTimeout 里的回调、then 链、async/await。这些都属于异步编程,是 JavaScript 里最重要也最容易绕晕的概念。
这一篇我会用一个生活比喻贯穿始终,保证你听完不再懵。
一、同步 vs 异步:用”餐厅点餐”理解
同步(Synchronous):排队办理
想象你去银行柜台办业务:只有一个窗口,你必须站着等前面的人办完,才轮到你。你干等着,什么都做不了。
代码里的同步就是:一行代码执行完,才执行下一行。如果某行代码很慢(比如要等 3 秒),后面的代码就全部卡住。
console.log("第一行");console.log("第二行");console.log("第三行");// 输出顺序:第一行 → 第二行 → 第三行(按顺序,没毛病)异步(Asynchronous):扫码点餐
想象你到餐厅吃饭:服务员收走你的菜单,告诉你”稍等,菜好了叫你”,然后你就自由了,可以刷手机、聊天。菜做好了,服务员再端给你。
代码里的异步就是:先不等着慢任务完成,继续往下执行其他代码。等慢任务完成时,再回来处理结果。
console.log("开始点餐");setTimeout(() => { console.log("菜做好了!");}, 3000);console.log("继续刷手机");// 输出顺序:开始点餐 → 继续刷手机 → (3秒后)菜做好了!注意!setTimeout 不会让程序等 3 秒,而是”3 秒后执行里面的函数”。程序先打印”继续刷手机”,3 秒后才打印”菜做好了”。
二、单线程:JS 为什么需要异步?
JavaScript 是单线程语言——同一时间只能干一件事。就像只有一个厨师,一次只能做一道菜。
如果所有事情都同步处理,遇到慢操作(比如请求网络数据要 2 秒),页面就”卡死”了:按钮点不动、动画停了、页面白屏。这体验太差了。
所以 JS 设计了事件循环(Event Loop)机制:
- 主线程先执行同步代码
- 遇到异步任务(定时器、网络请求),把它交给浏览器”后台处理”,主线程继续干别的
- 异步任务完成后,把回调函数放到一个”待办队列”里
- 主线程空闲时,从队列里取出回调执行
你不用记住全部细节,只要理解:异步 = 不阻塞主线程,回头再处理。
三、定时器:setTimeout 和 setInterval
setTimeout:等一会儿执行一次
// 3 秒后执行一次setTimeout(() => { console.log("3 秒到了");}, 3000);单位是毫秒(1 秒 = 1000 毫秒)。
传参数
function greet(name) { console.log(`你好,${name}`);}setTimeout(greet, 2000, "小明"); // 2秒后:你好,小明清除定时器:clearTimeout
let timer = setTimeout(() => { console.log("这条不会打印");}, 5000);
clearTimeout(timer); // 取消定时器setInterval:每隔一段时间重复执行
let count = 0;let interval = setInterval(() => { count++; console.log(`第 ${count} 次`); if (count >= 3) { clearInterval(interval); // 到 3 次停止 }}, 1000);// 输出:第 1 次 → 第 2 次 → 第 3 次(每秒一次)四、回调函数:异步的”老办法”
回调(callback)就是把一个函数作为参数,传给另一个函数,让它”办完事后调用你”。
function 买菜(蔬菜, 完成) { console.log(`去买${蔬菜}`); setTimeout(() => { console.log(`${蔬菜}买回来了`); 完成(); // 办完了,调用回调 }, 2000);}
买菜("白菜", () => { console.log("开始做饭");});// 2秒后:白菜买回来了 → 开始做饭回调地狱:多层嵌套的噩梦
如果要做的事有先后顺序:买菜 → 洗菜 → 切菜 → 炒菜,用回调写就会一层套一层:
买菜("白菜", () => { 洗菜(() => { 切菜(() => { 炒菜(() => { console.log("开饭!"); }); }); });});这还只是 4 层,实际项目可能十几层。代码向右歪成三角形,读起来想哭——这就是著名的回调地狱(Callback Hell)。
解决办法: Promise。这正是下一篇讲 Promise 的原因。
五、Promise:把异步”包装”成可读的流程
Promise 是什么?
Promise 是一个”承诺”对象,代表一个还没完成、但将来会完成的操作。它有三种状态:
- pending(进行中):操作还没完成
- fulfilled(成功):操作完成,有结果
- rejected(失败):操作失败,有错误原因
状态只能从 pending 变成 fulfilled 或 rejected,一旦改变就定死了。
创建一个 Promise
let promise = new Promise((resolve, reject) => { // 这里放异步任务 setTimeout(() => { let success = true; // 模拟成功/失败 if (success) { resolve("数据加载成功"); // 成功 → 调用 resolve 传结果 } else { reject("出错了"); // 失败 → 调用 reject 传错误 } }, 2000);});resolve 和 reject 是两个函数,由 JS 提供,你不用自己实现。
使用 Promise:then / catch
promise .then(result => { console.log("成功:", result); // 成功:数据加载成功 }) .catch(error => { console.log("失败:", error); });.then(函数):成功时执行,参数是 resolve 传的值.catch(函数):失败时执行,参数是 reject 传的值
读起来像人话: “先做这个,成功就怎样,失败就怎样”。
链式调用:解决回调地狱
then 可以链式写,每个 then 的返回值会传给下一个:
function 买菜(蔬菜) { return new Promise((resolve) => { setTimeout(() => { console.log(`${蔬菜}买回来了`); resolve(蔬菜); }, 1000); });}
买菜("白菜") .then(菜 => { console.log("洗" + 菜); return 菜; // 传给下一个 then }) .then(菜 => { console.log("切" + 菜); return 菜; }) .then(菜 => { console.log("炒" + 菜); console.log("开饭!"); });// 依次输出:白菜买回来了 → 洗白菜 → 切白菜 → 炒白菜 → 开饭!对比之前的回调地狱,现在代码是从上往下一条直线,清楚多了。
finally:无论成败都执行
promise .then(result => console.log("成功")) .catch(error => console.log("失败")) .finally(() => console.log("收尾工作,比如关闭加载动画"));Promise.all:多个异步一起等
let p1 = Promise.resolve("第一个结果");let p2 = new Promise(resolve => setTimeout(() => resolve("第二个结果"), 2000));
Promise.all([p1, p2]).then(results => { console.log(results); // ["第一个结果", "第二个结果"](等全部完成)});Promise.all 等所有 Promise 都成功才执行 then,任何一个失败就进 catch。适合”同时请求多个数据”的场景。
六、async/await:让异步代码”看起来像同步”
Promise 已经比回调好了,但 .then() 链还是不够直观。ES2017 推出的 async/await,让你用写同步代码的感觉来写异步代码——这现在是主流写法。
基本用法
// 1. 函数前面加 asyncasync function getData() { // 2. 在 Promise 前面加 await,等它完成 let result = await 一个Promise; return result;}规则只有两条:
async放在函数前,函数就变成了”异步函数”- 在异步函数里,
await后面跟一个 Promise,代码会停下来等这个 Promise 完成,然后拿到结果
完整示例
function 买菜(蔬菜) { return new Promise((resolve) => { setTimeout(() => resolve(`${蔬菜}买回来了`), 1000); });}
async function 做饭流程() { let a = await 买菜("白菜"); // 等1秒,拿到结果 console.log(a); let b = await 买菜("猪肉"); // 再等1秒 console.log(b); console.log("开饭!");}
做饭流程();// 白菜买回来了 → 猪肉买回来了 → 开饭!注意:虽然 await 让这段代码看起来是”一步一步走”,但它不会卡住页面——其他代码照样可以运行。
错误处理:try / catch
async/await 里处理错误,用和同步代码一样的 try/catch:
function 请求数据() { return new Promise((resolve, reject) => { setTimeout(() => { reject("网络中断"); }, 1000); });}
async function main() { try { let data = await 请求数据(); console.log(data); } catch (error) { console.log("出错了:", error); // 出错了:网络中断 }}
main();await 的 Promise 如果失败(reject),就会抛出一个错误,被 catch 接住。这样错误处理清清楚楚。
async 函数总是返回 Promise
async function foo() { return 42; // 返回普通值}
foo().then(value => console.log(value)); // 42即使 async 函数里返回普通值,外部拿到的也是一个 Promise,需要用 then 或 await 取。
七、模拟一个真实的异步场景
把学到的串起来:模拟”请求用户数据 → 处理后显示”:
// 模拟网络请求:1.5 秒后返回数据function 请求用户(id) { return new Promise((resolve, reject) => { setTimeout(() => { if (id > 0) { resolve({ id, name: "小明", score: 95 }); } else { reject("用户不存在"); } }, 1500); });}
async function 展示用户(id) { console.log("加载中..."); try { let user = await 请求用户(id); console.log(`用户:${user.name},成绩:${user.score}`); } catch (error) { console.log("加载失败:", error); }}
展示用户(1); // 加载中... → 用户:小明,成绩:95展示用户(-1); // 加载中... → 加载失败:用户不存在“加载中”先出现(异步不阻塞),1.5 秒后结果出现。这就是真实项目中”先显示 loading,再显示数据”的原理。
八、总结
这一篇的核心要点:
- 同步:一行接一行执行,慢操作会卡住页面
- 异步:先干别的,慢任务完成后再回来处理(餐厅点餐比喻)
- 单线程 + 事件循环:JS 不阻塞主线程的底层机制
- setTimeout / setInterval:定时器,异步入门
- 回调函数:老办法,嵌套多了就是回调地狱
- Promise:三种状态(pending/fulfilled/rejected),then/catch/finally 链式处理
- async/await:用同步的写法写异步,try/catch 处理错误,现在是主流
下一篇我们学 fetch 网络请求,把 async/await 用在真正的”请求后端数据”上——那时你会发现,前面学的全都在真实项目里用上了。