JavaScript 同步异步与 Promise 详解

46 阅读14分钟

JavaScript 同步、异步与 Promise 详解

前言

在 JavaScript 学习过程中,同步、异步、Promise、async/await 是必须掌握的核心内容。
很多初学者刚接触这些概念时,都会有类似疑问:

  • 为什么代码输出顺序和书写顺序不一样?
  • setTimeout 为什么不会立即执行?
  • Promise 到底解决了什么问题?
  • async/awaitPromise 是什么关系?
  • 为什么 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 变成 fulfilledrejected,就不会再改变。

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 有明确的状态流转:

  • pending
  • fulfilled
  • rejected

这让异步逻辑更可预测。


十一、事件循环:为什么异步代码会“后执行”?

如果想真正理解同步、异步、Promise,就一定绕不开 事件循环(Event Loop)

11.1 JavaScript 执行的大致流程

JavaScript 执行代码时,通常会涉及这些概念:

  • 调用栈(Call Stack):执行同步代码
  • 宏任务队列(Macrotask Queue)
  • 微任务队列(Microtask Queue)
  • 事件循环(Event Loop)

简单理解执行顺序可以记成:

  1. 先执行同步代码
  2. 同步代码执行完后,清空微任务队列
  3. 再执行一个宏任务
  4. 再清空微任务
  5. 重复循环

11.2 宏任务和微任务

常见宏任务

  • setTimeout
  • setInterval
  • DOM 事件
  • script 整体代码

常见微任务

  • Promise.then / catch / finally
  • queueMicrotask
  • MutationObserver

11.3 经典输出顺序题

console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

console.log('4');

输出结果:

1
4
3
2

为什么?

执行顺序如下:

  1. console.log('1') 是同步代码,立即执行
  2. setTimeout(..., 0) 注册一个宏任务
  3. Promise.resolve().then(...) 注册一个微任务
  4. console.log('4') 是同步代码,立即执行
  5. 当前同步代码执行完后,先执行微任务,输出 3
  6. 再执行宏任务,输出 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 的状态:

  • fulfilled
  • rejected

适用场景:

  • 需要拿到所有请求结果汇总
  • 不希望一个失败影响整体结果展示

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.id
  • getComments() 依赖 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 使用

二十、学习建议

如果你是初学者,我建议按这个顺序掌握:

  1. 先理解同步和异步的区别
  2. 再理解回调函数是什么
  3. 掌握 Promise 的三种状态和 then/catch/finally
  4. 理解 Promise 链式调用
  5. 学会用 async/await 改写 Promise
  6. 最后深入理解事件循环、微任务和宏任务

这样学会更顺,也更容易真正吃透 JavaScript 异步编程。


结语

同步、异步和 Promise 是 JavaScript 的核心内容,也是前端开发、Node.js 开发中最常见的知识点之一。

如果只会写同步代码,就很难真正理解:

  • 接口请求为什么是异步的
  • 页面为什么不会因为请求而卡死
  • setTimeoutPromise.then 为什么执行顺序不同
  • async/await 为什么让异步代码更优雅

理解这些内容后,你不仅能写出更清晰的 JavaScript 代码,也能更从容地面对面试和实际开发中的异步场景。