JavaScript的同步与异步

一、开篇:为什么 JS 需要同步与异步?

JavaScript 作为浏览器和 Node.js 的核心脚本语言,单线程是其天生特性 ------ 同一时间只能执行一段代码。这一设计源于 JS 的核心用途:处理页面交互(DOM 操作)和网络请求,若所有操作都同步执行,一个耗时的网络请求就会导致页面卡死(俗称 "阻塞")。

比如:

复制代码

// 同步代码的阻塞问题

function syncTask() {

let start = Date.now();

while (Date.now() - start 0) {} // 模拟3秒耗时操作

console.log("同步任务完成");

}

syncTask();

console.log("后续代码"); // 必须等待3秒后才执行

此时页面会卡顿 3 秒,用户无法点击、滚动。而异步机制的出现,正是为了解决 "单线程阻塞" 问题,让 JS 能在等待耗时操作时,继续执行其他任务。

二、核心概念:同步与异步的本质区别

1. 同步(Synchronous)
  • 定义:代码按顺序执行,前一个任务完成后,才执行后一个任务,任务执行期间会阻塞线程。
  • 特点:顺序执行、阻塞线程、结果即时返回。
  • 常见场景:基本运算(加减乘除)、变量赋值、普通函数调用、for 循环等。

示例:

复制代码

let a = 1;

let b = 2;

let c = a + b; // 同步执行,立即得到结果3

console.log(c); // 顺序输出3

2. 异步(Asynchronous)
  • 定义:任务启动后不等待其完成,直接执行后续任务,耗时任务在后台完成后,通过回调 / Promise 等方式通知并执行结果处理。
  • 特点:非顺序执行、不阻塞线程、结果延迟返回。
  • 常见场景:网络请求(AJAX/fetch)、定时器(setTimeout/setInterval)、文件读写(Node.js)、DOM 事件监听(click/load)等。

示例:

复制代码

console.log("1. 启动异步任务");

setTimeout(() => {

console.log("3. 异步任务完成"); // 延迟1秒执行

}, 1000);

console.log("2. 继续执行后续代码"); // 不等待异步任务,立即执行

// 输出顺序:1 → 2 → 3

三、底层原理:JS 的异步执行机制

要彻底理解同步异步,必须搞懂调用栈(Call Stack)、任务队列(Task Queue)、事件循环(Event Loop) 这三大核心组件。

1. 核心组件分工
  • 调用栈:负责执行同步代码,遵循 "先进后出" 原则。同步任务执行时入栈,执行完毕后出栈。
  • 任务队列:存放异步任务的回调函数,分为 "宏任务队列" 和 "微任务队列"(优先级:微任务 > 宏任务)。
    • 宏任务:setTimeout、setInterval、DOM 事件、AJAX 请求、script 标签执行。
    • 微任务:Promise.then/catch/finally、async/await、process.nextTick(Node.js,优先级最高)。
  • 事件循环:持续监控调用栈和任务队列,当调用栈为空时,将任务队列中的回调函数压入调用栈执行。
2. 异步执行流程(关键!)
  1. 执行同步代码,同步任务依次入栈、出栈。
  1. 遇到异步任务时,启动异步操作(如定时器计时、网络请求),并将回调函数注册到对应任务队列。
  1. 同步代码执行完毕(调用栈为空),事件循环开始工作。
  1. 优先清空微任务队列:将微任务队列中所有回调函数依次压入调用栈执行,直到微任务队列为空。
  1. 再从宏任务队列中取出第一个回调函数,压入调用栈执行。
  1. 重复步骤 3-5,形成循环。
3. 经典案例:验证执行顺序
复制代码

console.log("1. 同步代码开始");

setTimeout(() => {

console.log("6. 宏任务:setTimeout回调");

}, 0);

Promise.resolve().then(() => {

console.log("4. 微任务:Promise.then");

}).then(() => {

console.log("5. 微任务:第二个then");

});

console.log("2. 同步代码中间");

async function asyncTask() {

console.log("3. 同步:async函数执行");

await Promise.resolve(); // await后相当于微任务

console.log("7. 微任务:await后续代码");

}

asyncTask();

console.log("8. 同步代码结束");

执行顺序解析

  1. 同步代码入栈:输出 "1"→"2"→"3"→"8"(调用栈为空)。
  1. 事件循环触发,先处理微任务队列:
    • 第一个 Promise.then:输出 "4",第二个 then 入微任务队列。
    • 第二个 Promise.then:输出 "5",微任务队列暂空。
    • await 对应的微任务:输出 "7",微任务队列彻底清空。
  1. 处理宏任务队列:取出 setTimeout 回调,输出 "6"。
  1. 最终输出:1 → 2 → 3 → 8 → 4 → 5 → 7 → 6。

四、异步编程的演进:从回调到 async/await

JS 异步编程经历了三次重要演进,核心目标是解决 "回调地狱",让代码更易读、易维护。

1. 第一代:回调函数(Callback)
  • 特点:直接使用回调函数处理异步结果,简单直观但易产生 "回调地狱"。
  • 问题:嵌套层级深、代码可读性差、错误处理困难。

示例(回调地狱):

复制代码

// 模拟获取用户信息→获取用户订单→获取订单详情

getUserInfo(userId, (userErr, userData) => {

if (userErr) throw userErr;

getOrderList(userData.id, (orderErr, orderData) => {

if (orderErr) throw orderErr;

getOrderDetail(orderData[0].id, (detailErr, detailData) => {

if (detailErr) throw detailErr;

console.log("订单详情:", detailData);

});

});

});

2. 第二代:Promise
  • 特点:用链式调用(.then ())替代嵌套回调,解决回调地狱,统一错误处理(.catch ())。
  • 核心状态:pending(进行中)→ fulfilled(成功)/rejected(失败),状态一旦改变不可逆转。

示例(Promise 链式调用):

复制代码

getUserInfo(userId)

.then(userData => getOrderList(userData.id))

.then(orderData => getOrderDetail(orderData[0].id))

.then(detailData => console.log("订单详情:", detailData))

.catch(err => console.error("错误:", err));

3. 第三代:async/await
  • 特点:ES7 语法,基于 Promise,用同步的写法编写异步代码,可读性最强,是当前最优异步方案。
  • 核心规则:
    • async 函数返回值是 Promise 对象。
    • await 只能在 async 函数中使用,后面跟 Promise 对象,会暂停函数执行,等待 Promise 决议后继续。
    • 错误处理用 try/catch,比 Promise.catch 更直观。

示例(async/await 优化):

复制代码

async function getOrderInfo(userId) {

try {

const userData = await getUserInfo(userId); // 等待Promise成功

const orderData = await getOrderList(userData.id);

const detailData = await getOrderDetail(orderData[0].id);

console.log("订单详情:", detailData);

} catch (err) {

console.error("错误:", err); // 统一捕获所有错误

}

}

getOrderInfo(userId);

五、实战避坑:95 分必备的关键知识点

1. setTimeout 的 "延迟" 不是绝对的

setTimeout 的延迟时间是 "最小延迟",而非 "精确延迟"。因为回调函数需等待调用栈为空、微任务清空后才执行。

示例:

复制代码

setTimeout(() => {

console.log("延迟1秒执行?");

}, 1000);

// 同步代码阻塞3秒

let start = Date.now();

while (Date.now() - start 0) {}

// 实际延迟3秒才执行,而非1秒

2. async/await 的本质是 Promise 语法糖

await 后的代码会被包装成微任务,且 await 会暂停当前 async 函数,而非整个线程。

示例:

复制代码

async function test() {

console.log("1");

await Promise.resolve();

console.log("3"); // 微任务

}

test();

console.log("2"); // 同步代码,不被await阻塞

// 输出:1 → 2 → 3

3. 事件循环的浏览器与 Node.js 差异
  • 浏览器:微任务队列优先级高于宏任务队列,微任务执行完再执行宏任务。
  • Node.js(v11+):与浏览器一致;v11 前:先执行完一个宏任务,再执行所有微任务。
4. 避免异步代码中的常见错误
  • 不要在 for 循环中使用 var 声明变量(变量提升导致异步回调获取错误值),改用 let/const。
  • 不要忽略 Promise 错误(未写.catch () 会导致未捕获异常)。
  • 避免过度嵌套 async/await(可用 Promise.all 并行执行多个独立异步任务,提升性能)。

示例(Promise.all 并行优化):

复制代码

// 串行执行:总耗时 = 1秒 + 2秒 = 3秒

async function serialTask() {

const res1 = await fetch("/api/1");

const res2 = await fetch("/api/2");

}

// 并行执行:总耗时 = 2秒(取最长任务时间)

async function parallelTask() {

const promise1 = fetch("/api/1");

const promise2 = fetch("/api/2");

const [res1, res2] = await Promise.all([promise1, promise2]);

}

六、总结:同步与异步的核心逻辑

JS 的同步异步本质是单线程模型下的效率优化方案

  • 同步保证代码执行的顺序性和确定性,适用于简单、无耗时的操作。
  • 异步通过 "任务队列 + 事件循环" 实现非阻塞执行,适用于耗时操作(网络、IO 等)。
  • 异步编程的演进(回调→Promise→async/await),核心是 "降低复杂度、提升可读性"。

掌握本文的原理、案例和避坑点,你就能轻松应对面试中的同步异步问题,以及实际开发中的异步场景优化啦!

相关推荐
囊中之锥.2 小时前
《HTML 网页构造指南:从基础结构到实用标签》
前端·html
饼饼饼2 小时前
从 0 到 1:前端 CI/CD 实战(第二篇:用Docker 部署 GitLab)
前端·自动化运维
colus_SEU2 小时前
【计算机网络笔记】第三章 传输层
网络·笔记·计算机网络
beckyyy2 小时前
ant design vue Table根据数据合并单元格
前端·ant design
用户8168694747252 小时前
Commit 阶段的 3 个子阶段与副作用执行全解析
前端·react.js
岭子笑笑2 小时前
Vant4图片懒加载源码解析(一)
前端
不染尘.2 小时前
应用层之HTTP
服务器·网络·网络协议·计算机网络·http
老华带你飞2 小时前
婚纱摄影网站|基于java + vue婚纱摄影网站系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
Rysxt_2 小时前
UniApp App.vue 文件完整教程
开发语言·前端·javascript