JavaScript 同步、异步与 Promise 详解
前言
在 JavaScript 学习过程中,同步、异步、Promise、async/await 是必须掌握的核心内容。
很多初学者刚接触这些概念时,都会有类似疑问:
- 为什么代码输出顺序和书写顺序不一样?
setTimeout为什么不会立即执行?Promise到底解决了什么问题?async/await和Promise是什么关系?- 为什么
await看起来像同步,但本质还是异步?
这些问题背后,其实都和 JavaScript 的执行机制有关。
本文会从 同步与异步的区别 讲起,逐步过渡到 回调函数、Promise、事件循环、async/await,并通过大量代码示例帮助你真正理解 JavaScript 异步编程。
一、什么是同步?
同步(Synchronous) 指的是:代码按照书写顺序一行一行执行,前面的代码执行完成后,后面的代码才能开始执行。
console.log('第一步');
console.log('第二步');
console.log('第三步');
输出结果:
第一步
第二步
第三步
这就是最典型的同步代码。
同步代码的特点
- 执行顺序清晰
- 逻辑直观,容易理解
- 一个任务没有执行完,后面的任务必须等待
- 遇到耗时操作时,会阻塞后续代码
比如下面这个例子:
function heavyTask() {
const start = Date.now();
while (Date.now() - start < 3000) {}
}
console.log('开始');
heavyTask();
console.log('结束');
这段代码会阻塞大约 3 秒,只有 heavyTask() 执行结束后,console.log('结束') 才会执行。
二、什么是异步?
异步(Asynchronous) 指的是:发起一个任务之后,不需要原地等待它执行完成,程序可以继续执行后面的代码;等这个任务完成后,再通过某种方式处理结果。
console.log('开始');
setTimeout(() => {
console.log('异步任务完成');
}, 1000);
console.log('结束');
输出结果:
开始
结束
异步任务完成
在这个例子中:
setTimeout注册了一个延迟 1 秒执行的任务- JavaScript 不会停下来等这 1 秒
- 它会继续执行后面的
console.log('结束') - 1 秒后,再执行回调函数
异步代码的特点
- 不阻塞后续代码执行
- 适合处理耗时任务
- 能提升程序响应性
- 执行顺序不一定和书写顺序一致
三、为什么 JavaScript 需要异步?
JavaScript 在浏览器环境中,通常由 单线程主线程 执行代码。
这意味着同一时刻只能处理一件事情。
如果所有任务都用同步方式执行,那么遇到网络请求、文件读取、定时器等耗时操作时,整个页面就可能卡住,用户无法点击、输入、滚动,体验会非常差。
所以 JavaScript 的运行环境提供了异步机制,让程序在等待某些耗时任务时,仍然可以继续执行其他代码。
常见的异步场景
setTimeout/setInterval- Ajax /
fetch网络请求 - 文件读取
- 用户点击、输入、滚动等事件
- 数据库操作
- Node.js 中的 I/O 任务
需要注意的是:
JavaScript 本身并不是同时执行多段代码,而是借助运行环境提供的能力,在等待异步任务完成时继续执行主线程上的其他代码。
四、同步和异步的直观区别
下面用一个更直观的例子来理解。
同步模式
function buyBreakfast() {
console.log('排队买早餐...');
console.log('拿到早餐');
}
console.log('出门');
buyBreakfast();
console.log('去公司');
输出:
出门
排队买早餐...
拿到早餐
去公司
必须先买完早餐,才能去公司。
异步模式
function orderBreakfast(callback) {
console.log('点早餐');
setTimeout(() => {
callback('早餐到了');
}, 2000);
}
console.log('出门');
orderBreakfast((msg) => {
console.log(msg);
});
console.log('先去公司');
输出:
出门
点早餐
先去公司
早餐到了
这里点了早餐以后,不需要站着等,可以先做别的事情,等早餐好了再通知你。
五、最早的异步处理方式:回调函数
在 Promise 出现之前,JavaScript 处理异步最常见的方式是 回调函数(Callback)。
5.1 什么是回调函数?
回调函数就是:把一个函数作为参数传递给另一个函数,在某个时机再调用它。
function fetchData(callback) {
setTimeout(() => {
const data = { name: '张三', age: 18 };
callback(data);
}, 1000);
}
fetchData((data) => {
console.log('获取到数据:', data);
});
输出:
获取到数据: { name: '张三', age: 18 }
这里传进去的 (data) => { ... } 就是回调函数。
5.2 回调函数的问题
回调函数本身没有错,但当异步逻辑变复杂时,会带来很多问题。
1. 回调地狱
多个异步任务如果一层套一层,代码会越来越难看:
getUser(function(user) {
getOrders(user.id, function(orders) {
getOrderDetail(orders[0].id, function(detail) {
getLog(detail.id, function(log) {
console.log(log);
});
});
});
});
这样的代码:
- 嵌套太深
- 可读性差
- 维护困难
这就是著名的 回调地狱(Callback Hell)。
2. 错误处理麻烦
每一层都可能要处理错误,代码会变得很乱。
3. 流程控制不方便
比如并行执行多个异步任务、等待全部完成、获取第一个结果等,用回调都不太优雅。
六、Promise 是什么?
为了解决回调函数在复杂场景中的问题,JavaScript 引入了 Promise。
6.1 Promise 的定义
Promise 是一个对象,用来表示一个异步操作最终会得到的结果。
你可以把它理解成:
“先给你一个承诺,未来这个异步任务要么成功,要么失败,等结果出来后再告诉你。”
6.2 Promise 的三种状态
Promise 有三种状态:
pending:等待中fulfilled:已成功rejected:已失败
状态一旦从 pending 变成 fulfilled 或 rejected,就不会再改变。
const p = new Promise((resolve, reject) => {
resolve('成功');
reject('失败'); // 不会生效
});
七、如何创建 Promise?
const myPromise = new Promise((resolve, reject) => {
const success = true;
if (success) {
resolve('操作成功');
} else {
reject('操作失败');
}
});
这里:
resolve()用来表示成功reject()用来表示失败
7.1 一个重要细节:Promise 执行器是同步执行的
很多初学者会误以为 new Promise() 里面的代码是异步执行的,其实不是。
console.log('1');
const p = new Promise((resolve) => {
console.log('2');
resolve();
});
console.log('3');
输出结果:
1
2
3
说明 Promise 构造函数里的执行器会立即执行。
真正异步的通常是里面包着的 setTimeout、网络请求等操作。
八、如何使用 Promise?
8.1 then()
then() 用于处理成功结果。
Promise.resolve('成功')
.then((result) => {
console.log(result);
});
8.2 catch()
catch() 用于处理失败结果。
Promise.reject('失败')
.catch((error) => {
console.log(error);
});
8.3 finally()
finally() 无论成功还是失败都会执行,适合做收尾工作。
Promise.resolve('请求成功')
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
})
.finally(() => {
console.log('请求结束');
});
九、Promise 链式调用
Promise 最常见的用法就是链式调用。
function getUser() {
return Promise.resolve({ id: 1, name: 'Tom' });
}
function getOrders(userId) {
return Promise.resolve([{ id: 101, userId }]);
}
function getOrderDetail(orderId) {
return Promise.resolve({ id: orderId, price: 100 });
}
getUser()
.then((user) => {
console.log('用户信息:', user);
return getOrders(user.id);
})
.then((orders) => {
console.log('订单列表:', orders);
return getOrderDetail(orders[0].id);
})
.then((detail) => {
console.log('订单详情:', detail);
})
.catch((error) => {
console.error('出错了:', error);
});
这样写相比回调嵌套,更平坦,也更容易维护。
9.1 then() 会返回一个新的 Promise
这是 Promise 能链式调用的核心原因。
Promise.resolve(1)
.then((num) => {
return num + 1;
})
.then((num) => {
console.log(num); // 2
});
如果在 then() 中返回的是普通值,这个值会自动包装成成功状态的 Promise。
如果返回的是另一个 Promise,后面的 then() 会等待它完成。
Promise.resolve(1)
.then((num) => {
return new Promise((resolve) => {
setTimeout(() => resolve(num + 1), 1000);
});
})
.then((num) => {
console.log(num); // 2
});
9.2 Promise 中的错误会向后传递
Promise.resolve()
.then(() => {
throw new Error('发生错误');
})
.then(() => {
console.log('这里不会执行');
})
.catch((err) => {
console.log('捕获到错误:', err.message);
});
这说明 Promise 链中的错误,可以统一交给后面的 catch() 处理。
十、Promise 到底解决了什么问题?
Promise 主要解决了以下几个问题:
10.1 让异步流程更清晰
回调嵌套写法不容易读,Promise 链式调用更平坦。
10.2 统一错误处理
可以在最后通过一个 catch() 集中处理错误。
10.3 状态更明确
Promise 有明确的状态流转:
pendingfulfilledrejected
这让异步逻辑更可预测。
十一、事件循环:为什么异步代码会“后执行”?
如果想真正理解同步、异步、Promise,就一定绕不开 事件循环(Event Loop)。
11.1 JavaScript 执行的大致流程
JavaScript 执行代码时,通常会涉及这些概念:
- 调用栈(Call Stack):执行同步代码
- 宏任务队列(Macrotask Queue)
- 微任务队列(Microtask Queue)
- 事件循环(Event Loop)
简单理解执行顺序可以记成:
- 先执行同步代码
- 同步代码执行完后,清空微任务队列
- 再执行一个宏任务
- 再清空微任务
- 重复循环
11.2 宏任务和微任务
常见宏任务
setTimeoutsetInterval- DOM 事件
- script 整体代码
常见微任务
Promise.then / catch / finallyqueueMicrotaskMutationObserver
11.3 经典输出顺序题
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
输出结果:
1
4
3
2
为什么?
执行顺序如下:
console.log('1')是同步代码,立即执行setTimeout(..., 0)注册一个宏任务Promise.resolve().then(...)注册一个微任务console.log('4')是同步代码,立即执行- 当前同步代码执行完后,先执行微任务,输出
3 - 再执行宏任务,输出
2
所以最终结果是:
1
4
3
2
这个例子非常关键,因为它能帮助你真正理解:
Promise.then()的优先级通常高于setTimeout()回调。
十二、Promise 的静态方法
在实际开发中,我们经常会同时处理多个 Promise。
这时候就会用到 Promise 的静态方法。
12.1 Promise.all()
所有 Promise 都成功,整体才成功;只要有一个失败,整体就失败。
Promise.all([
Promise.resolve('用户数据'),
Promise.resolve('订单数据'),
Promise.resolve('评论数据')
])
.then((results) => {
console.log(results);
})
.catch((error) => {
console.error('至少有一个失败:', error);
});
适用场景:
- 多个请求必须都成功才能继续
- 页面初始化依赖多个接口结果
12.2 Promise.allSettled()
等待所有 Promise 都执行结束,不管成功还是失败。
Promise.allSettled([
Promise.resolve('成功1'),
Promise.reject('失败2'),
Promise.resolve('成功3')
]).then((results) => {
console.log(results);
});
返回结果会包含每个 Promise 的状态:
fulfilledrejected
适用场景:
- 需要拿到所有请求结果汇总
- 不希望一个失败影响整体结果展示
12.3 Promise.race()
谁先结束,就返回谁的结果。
const fast = new Promise((resolve) => {
setTimeout(() => resolve('快的先完成'), 100);
});
const slow = new Promise((resolve) => {
setTimeout(() => resolve('慢的后完成'), 1000);
});
Promise.race([fast, slow]).then((result) => {
console.log(result); // 快的先完成
});
注意:
- “先结束”可能是成功,也可能是失败
- 所以
race()返回的第一个结果不一定是成功结果
12.4 Promise.any()
只要有一个成功,就返回成功结果;只有全部失败时才会失败。
Promise.any([
Promise.reject('失败1'),
Promise.resolve('成功'),
Promise.reject('失败2')
]).then((result) => {
console.log(result); // 成功
});
适用场景:
- 多个备用接口,只要有一个成功即可
- 多个资源地址,谁成功就用谁
12.5 Promise.resolve() 和 Promise.reject()
快速创建一个已经完成的 Promise。
Promise.resolve('立即成功');
Promise.reject('立即失败');
十三、async/await:Promise 的语法糖
随着异步逻辑越来越复杂,Promise 链写起来虽然比回调好很多,但依然可能显得冗长。
于是 JavaScript 又提供了 async/await。
13.1 async 的作用
async 用来声明一个异步函数。
async 函数一定会返回一个 Promise。
async function test() {
return 'hello';
}
等价于:
function test() {
return Promise.resolve('hello');
}
13.2 await 的作用
await 用来等待一个 Promise 完成,并拿到成功结果。
async function getData() {
const result = await Promise.resolve('数据');
console.log(result);
}
getData();
await 看起来像“暂停”了代码,但它并不会阻塞整个 JavaScript 主线程,只是暂停当前 async 函数内部后续代码的执行。
十四、用 async/await 改写 Promise 链
Promise 写法
getUser()
.then((user) => {
return getOrders(user.id);
})
.then((orders) => {
return getOrderDetail(orders[0].id);
})
.then((detail) => {
console.log(detail);
})
.catch((error) => {
console.error(error);
});
async/await 写法
async function main() {
try {
const user = await getUser();
const orders = await getOrders(user.id);
const detail = await getOrderDetail(orders[0].id);
console.log(detail);
} catch (error) {
console.error(error);
}
}
main();
可以看到,async/await 更接近同步代码的书写方式,可读性通常更高。
十五、串行执行与并行执行
学习 async/await 后,一个非常容易踩坑的问题就是:什么时候串行,什么时候并行?
15.1 串行执行
如果后面的任务依赖前面的结果,就必须串行执行。
async function loadData() {
const user = await getUser();
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
return { user, posts, comments };
}
这里:
getPosts()依赖user.idgetComments()依赖posts[0].id
所以必须一步一步来。
15.2 并行执行
如果多个任务互不依赖,就应该并行执行,提高效率。
async function loadAll() {
const [user, products, news] = await Promise.all([
getUser(),
getProducts(),
getNews()
]);
return { user, products, news };
}
这样三个请求会同时发出,比一个接一个等待更快。
15.3 一个常见误区
很多人会这样写:
const user = await getUser();
const products = await getProducts();
const news = await getNews();
如果这三个请求互不依赖,这种写法其实是串行执行,效率较低。
更好的写法是:
const [user, products, news] = await Promise.all([
getUser(),
getProducts(),
getNews()
]);
十六、实际开发中的常见例子
16.1 用 Promise 封装异步操作
function delay(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
delay(1000).then(() => {
console.log('1秒后执行');
});
16.2 使用 fetch 请求接口
fetch('/api/user')
.then((response) => {
if (!response.ok) {
throw new Error(`请求失败:${response.status}`);
}
return response.json();
})
.then((data) => {
console.log('用户数据:', data);
})
.catch((error) => {
console.error('请求出错:', error);
});
16.3 使用 async/await 请求接口
async function fetchUserData() {
try {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error(`请求失败:${response.status}`);
}
const data = await response.json();
console.log('用户数据:', data);
return data;
} catch (error) {
console.error('请求出错:', error);
throw error;
}
}
十七、常见陷阱与注意事项
17.1 不要误以为 Promise 会自动变快
Promise 只是异步结果的表示方式,不会让代码自动更快。
真正影响效率的是你是否合理地使用串行和并行。
17.2 Promise 一旦创建就会立即执行
const p = new Promise((resolve) => {
console.log('开始执行');
resolve();
});
只要执行到 new Promise(...),里面的代码就立刻运行,不需要等 then()。
17.3 then() 的回调是异步执行的
Promise.resolve().then(() => {
console.log('then');
});
console.log('sync');
输出:
sync
then
说明 then() 回调会进入微任务队列,不会同步立即执行。
17.4 不要简单地说“循环里不能用 await”
更准确的说法是:
- 如果任务彼此独立,尽量并行执行
- 如果任务有顺序要求,循环里使用
await是合理的
串行
for (const item of items) {
await processItem(item);
}
并行
await Promise.all(items.map(item => processItem(item)));
17.5 fetch 遇到 404/500 不一定会进入 catch
这是很多初学者容易误解的地方。
const response = await fetch('/not-found');
console.log(response.ok); // false
fetch 只有在网络错误时才会真正 reject。
如果服务器返回了 404 或 500,请求本身可能仍然是成功完成的,所以要手动判断 response.ok。
十八、从回调到 Promise 再到 async/await
理解这三者的演进关系很重要。
18.1 回调版本
function getUser(callback) {
setTimeout(() => {
callback({ id: 1, name: 'Tom' });
}, 1000);
}
getUser((user) => {
console.log(user);
});
18.2 Promise 版本
function getUser() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ id: 1, name: 'Tom' });
}, 1000);
});
}
getUser().then((user) => {
console.log(user);
});
18.3 async/await 版本
async function main() {
const user = await getUser();
console.log(user);
}
main();
可以这样理解:
- 回调:最基础,但容易嵌套
- Promise:结构更清晰,支持链式调用
- async/await:基于 Promise,写法更接近同步
十九、同步、异步、Promise 的关系总结
我们可以用一句话概括它们之间的关系:
同步和异步描述的是任务执行方式,Promise 是 JavaScript 用来管理异步结果的对象,async/await 则是 Promise 的语法糖。
更具体地说:
同步
- 按顺序执行
- 当前任务不结束,后面任务不能开始
- 容易阻塞
异步
- 发起任务后不必等待完成
- 适合耗时操作
- 执行顺序可能变化
Promise
- 用来表示异步任务未来的结果
- 提供
then / catch / finally - 支持链式调用和统一错误处理
async/await
- Promise 的语法糖
- 让异步代码更接近同步风格
- 通常配合
try/catch使用
二十、学习建议
如果你是初学者,我建议按这个顺序掌握:
- 先理解同步和异步的区别
- 再理解回调函数是什么
- 掌握 Promise 的三种状态和
then/catch/finally - 理解 Promise 链式调用
- 学会用
async/await改写 Promise - 最后深入理解事件循环、微任务和宏任务
这样学会更顺,也更容易真正吃透 JavaScript 异步编程。
结语
同步、异步和 Promise 是 JavaScript 的核心内容,也是前端开发、Node.js 开发中最常见的知识点之一。
如果只会写同步代码,就很难真正理解:
- 接口请求为什么是异步的
- 页面为什么不会因为请求而卡死
setTimeout和Promise.then为什么执行顺序不同async/await为什么让异步代码更优雅
理解这些内容后,你不仅能写出更清晰的 JavaScript 代码,也能更从容地面对面试和实际开发中的异步场景。