一、进程与线程:JavaScript 的 "工作模式"
要理解 JavaScript 的异步特性,首先得搞懂两个基础概念:进程和线程。
- 进程:可以理解为一个 "独立的工厂",是程序运行的基本单位,拥有独立的内存空间。比如打开一个浏览器标签页,就是启动了一个进程。
- 线程:进程内的 "工人",负责执行具体的代码。一个进程可以有多个线程(多线程),但多个线程共享进程的内存资源。
为什么 JavaScript 是单线程?
JavaScript 被设计为单线程(一个进程里只有一个 "工人"),原因很简单:它最初是为浏览器设计的脚本语言,主要用来操作 DOM。如果有多个线程同时操作 DOM,可能会导致冲突(比如一个线程删除 DOM,另一个线程修改 DOM)。
v8 在执行js代码时, 默认只开启一个线程工作,可以通过操作开启多线程,线程之间通常可以同时工作, 但是有 js 引擎线程和渲染线程是互斥的
单线程意味着:代码只能 "从头到尾依次执行",但这会带来问题 —— 如果遇到耗时操作(比如网络请求),会阻塞后面的代码。为了解决这个问题,JavaScript 引入了同步与异步的执行机制。
二、同步与异步:代码的 "执行节奏"
JavaScript 代码分为两种执行方式,就像现实中的 "立即做" 和 "稍后做":
1. 同步代码
同步代码会立即执行,按顺序一步一步执行,前一句没执行完,后一句不会开始。
let a = 1; // 同步:立即执行,a=1
console.log(a); // 同步:立即执行,打印1
2. 异步代码
异步代码不会立即执行,而是先 "登记" 到任务队列中,等所有同步代码执行完后,再按顺序执行。常见的异步操作有:setTimeout、setInterval、网络请求(fetch)、DOM 事件(click)等。
let a = 1; // 同步:先执行,a=1
// 异步:登记到任务队列,1秒后执行
setTimeout(() => {
a = 2;
console.log(a); // 1秒后打印2
}, 1000);
console.log(a); // 同步:先执行,打印1(此时a还没被修改)
执行顺序解析:
- 先执行同步代码:
a=1→console.log(a)(输出 1) - 同步代码执行完后,从任务队列中取出异步代码执行:
a=2→console.log(a)(输出 2)
三、Promise:解决 "回调地狱" 的利器
早期处理异步操作依赖回调函数,但多层嵌套会导致 "回调地狱"(代码像金字塔一样嵌套,可读性极差)。例如:
// 回调地狱示例:获取用户信息→获取订单→获取物流,嵌套三层
getUser(userId, (user) => {
getOrder(user.orderId, (order) => {
getLogistics(order.logisticsId, (logistics) => {
console.log(logistics);
});
});
});
Promise 的出现就是为了让异步代码更扁平、更易读。
1. Promise 是什么?
Promise 是一个异步操作的 "容器" ,它有三种状态:
-
pending(等待中):初始状态,操作未完成 -
fulfilled(已成功):操作完成,调用resolve()触发 -
rejected(已失败):操作出错,调用reject()触发
状态一旦改变(从 pending → fulfilled 或 rejected),就不会再变。
2. Promise 的基本用法
// 创建一个Promise实例
const promise = new Promise((resolve, reject) => {
// 异步操作(比如定时器、网络请求)
setTimeout(() => {
const success = true;
if (success) {
resolve("操作成功"); // 成功时调用,状态变为fulfilled
} else {
reject("操作失败"); // 失败时调用,状态变为rejected
}
}, 1000);
});
// 用then处理成功,catch处理失败
promise
.then((result) => {
console.log(result); // 输出"操作成功"
})
.catch((error) => {
console.log(error); // 若失败,输出"操作失败"
});
3. 链式调用:解决嵌套问题
Promise 的 then 方法会返回一个新的 Promise,因此可以链式调用,让多层异步操作像同步代码一样线性书写。
// 示例:按顺序执行 xq→m→b 三个异步操作
function xq() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("1");
resolve(); // 完成后通知下一步
}, 1000);
});
}
function m() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("2");
resolve();
}, 2000);
});
}
function b() {
setTimeout(() => {
console.log("3");
}, 500);
}
// 链式调用:依次执行
xq()
.then(() => m()) // xq完成后执行m
.then(() => b()) // m完成后执行b
.catch((error) => console.log("出错了:", error));
执行顺序:1(1 秒后)→ 2(再等 2 秒)→ 3(再等 0.5 秒),完美按顺序执行。
四、Event-loop:JavaScript 的 "事件循环" 机制
JavaScript 单线程如何同时处理同步和异步操作?秘密在于Event-loop(事件循环) ,它像一个 "调度员",不断协调同步和异步任务的执行。
1. 任务队列的分类
异步任务分为两类,优先级不同:
-
微任务(Microtasks) :优先级高,会在同步代码执行完后立即执行。包括:
- Promise 的
then/catch/finally process.nextTick(Node.js 特有)MutationObserver(DOM 变化监听)
- Promise 的
-
宏任务(Macrotasks) :优先级低,在所有微任务执行完后才执行。包括:
setTimeout/setIntervalsetImmediate(Node.js 特有)- 网络请求(
fetch) - DOM 事件(
click/load) - 脚本执行(整体代码属于第一个宏任务)
2. Event-loop 执行流程
-
执行同步代码(属于当前宏任务),遇到异步任务就按类型放入对应的队列(微任务 / 宏任务)。
-
同步代码执行完后,清空微任务队列(按顺序执行所有微任务)。
-
微任务执行完后,浏览器可能会进行页面渲染。
-
从宏任务队列中取出第一个任务执行,重复步骤 1-4,形成循环。
简单说:同步 → 微任务 → 渲染 → 宏任务 → 同步...
3. 实例分析:代码执行顺序
console.log(1); // 同步代码(宏任务)
new Promise((resolve) => {
console.log(2); // Promise构造函数内是同步代码,立即执行
resolve();
}).then(() => {
console.log(3); // 微任务
setTimeout(() => {
console.log(4); // 宏任务
}, 0);
});
setTimeout(() => {
console.log(5); // 宏任务
setTimeout(() => {
console.log(6); // 宏任务
}, 0);
}, 0);
console.log(7); // 同步代码(宏任务)
分步解析:
-
执行同步代码(宏任务):
- 打印
1→ 执行 Promise 构造函数(打印2)→ 打印7 - 此时微任务队列有:
console.log(3) - 宏任务队列有:第一个
setTimeout(打印 5)、then里的setTimeout(打印 4)
- 打印
-
同步代码执行完,清空微任务队列:
- 执行
console.log(3)→ 打印3 - 此时微任务队列空,宏任务队列新增了
console.log(4)
- 执行
-
渲染页面(假设需要)。
-
执行第一个宏任务(打印 5):
- 打印
5→ 遇到setTimeout(打印 6),放入宏任务队列。
- 打印
-
重复步骤 1-4:
-
同步代码(无新同步)→ 微任务队列空 → 执行下一个宏任务(打印 4)→ 打印
4 -
再取下一个宏任务(打印 6)→ 打印
6
-
最终输出顺序:1 → 2 → 7 → 3 → 5 → 4 → 6
五、async/await:异步代码的 "同步写法"
async/await 是 ES2017 引入的语法,基于 Promise,让异步代码看起来像同步代码,可读性更强。
1. 基本用法
-
async:修饰函数,表明该函数是异步的,返回值会自动包装成 Promise。 -
await:只能用在async函数内,后面跟一个 Promise,会暂停函数执行,等待 Promise 完成后再继续。
// 用async/await改写前面的Promise链式调用
function xq() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("1");
resolve();
}, 1000);
});
}
function m() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("2");
resolve();
}, 2000);
});
}
function b() {
setTimeout(() => {
console.log("3");
}, 500);
}
// async函数:用同步的写法执行异步操作
async function run() {
await xq(); // 等待xq完成
await m(); // 等待m完成
b(); // 最后执行b
}
run(); // 执行结果和链式调用完全一致
2. 错误处理
await 后的 Promise 如果失败(reject),会抛出错误,可以用 try/catch 捕获:
async function fetchData() {
try {
const res = await fetch("https://api.example.com/data"); // 网络请求返回Promise
const data = await res.json();
console.log("数据:", data);
} catch (error) {
console.log("请求失败:", error); // 捕获所有错误
}
}
3. 复杂实例:结合 Event-loop
console.log("start");
async function async1() {
await async2(); // 等待async2完成
console.log("async1 end"); // 这行属于微任务
}
async function async2() {
console.log("async2"); // 同步执行
}
async1();
setTimeout(() => {
console.log("setTimeout"); // 宏任务
}, 0);
new Promise((resolve) => {
console.log("Promise"); // 同步执行
resolve();
}).then(() => {
console.log("Promise then"); // 微任务
});
console.log("end");
执行顺序解析:
-
同步代码:
start→async2(async1 调用 async2)→Promise→end -
微任务队列:
async1 end(await 后的代码)→Promise then(按顺序执行) -
宏任务队列:
setTimeout(最后执行)
输出结果:start → async2 → Promise → end → async1 end → Promise then → setTimeout
V8引擎对已 resolved 的 Promise 使用了 快速路径优化 :
- 如果 await 等待的 Promise 已经 resolved,后续代码会被延迟到 下一轮微任务
- 这导致 Promise then (当前微任务)比 async1 end (下一轮微任务)先执行
console.log("start");
async function async1() {
await async2(); // 等待async2完成
console.log("async1 end"); // 这行属于微任务
}
async function async2() {
console.log("async2"); // 同步执行
return Promise.resolve(); // 返回一个已完成的Promise
}
async1();
setTimeout(() => {
console.log("setTimeout"); // 宏任务
}, 0);
new Promise((resolve) => {
console.log("Promise"); // 同步执行
resolve();
}).then(() => {
console.log("Promise then"); // 微任务
});
console.log("end");
总结
JavaScript 异步编程的核心知识点:
-
单线程:只有一个 "工人",避免 DOM 操作冲突。
-
同步 vs 异步:同步按顺序执行,异步 "先登记后执行"。
-
Promise:用链式调用解决回调地狱,三种状态控制流程。
-
Event-loop:同步 → 微任务 → 宏任务的循环机制,决定代码执行顺序。
-
async/await:Promise 的语法糖,让异步代码像同步一样易读。
掌握这些概念,就能轻松应对 JavaScript 中的异步场景,写出高效且易维护的代码!