单线程的 JavaScript 是怎么实现异步操作的?
本质就是通过 JavaScript 的事件循环来实现的,也可以将此理解为 JavaScript 的执行机制。
为什么被设计成单线程
简化并发问题,避免了多线程的并发问题、避免线程之间的通信,减少复杂度。
单线程设计有什么缺点
阻塞、难以利用多核CPU、处理大量并发请求效率低。
执行顺序
-
首先,执行同步代码(包括脚本的初始部分)。
-
当同步代码执行完毕后,事件循环开始。
-
事件循环(Event Loop)会首先检查微任务队列。如果微任务队列中有任务,它会一直执行这些任务,直到微任务队列为空。
- 如果微任务中又遇到了新的微任务,会把新的微任务也加入到微任务队列
-
微任务队列为空后,事件循环会从宏任务队列中取出一个宏任务并执行它。
-
在执行宏任务的过程中,如果遇到新的微/宏任务,它们会被添加到微/宏任务队列中,但不会立即执行。
-
当宏任务执行完毕后,事件循环会再次检查微任务队列并执行其中的所有任务。这个过程会不断重复。
console.log('1'); // 1. 同步代码,立即执行
setTimeout(() => {
console.log('2'); // 5. 宏任务,稍后执行
Promise.resolve().then(() => {
console.log('3'); // 7. 微任务,在宏任务中创建,稍后执行
});
console.log('4'); // 6. 宏任务中的同步代码,紧随Promise之前执行
}, 0);
Promise.resolve().then(() => {
console.log('5'); // 3. 微任务,放入微任务队列,在当前宏任务之后执行
Promise.resolve().then(() => {
console.log('6'); // 4. 微任务中的微任务
});
setTimeout(() => {
console.log('7'); // 8. 微任务中的宏任务
}, 0);
});
console.log('8'); // 2. 同步代码,立即执行
// 输出顺序:1 8 5 6 2 4 3 7
任务队列(Task Queue)
任务队列用于存储待处理的任务。这些任务可以是宏任务(MacroTask)或微任务(MicroTask)。
| 宏任务(Macrotask) | 微任务(Microtask) |
|---|---|
| setTimeout | requestAnimationFrame(有争议) |
| setInterval | MutationObserver(浏览器环境) |
| MessageChannel | Promise.[ then/catch/finally ] |
| I/O,事件队列 | process.nextTick(Node环境) |
| setImmediate(Node环境) | queueMicrotask |
| script(整体代码块) |
注意⚠️:
-
整体代码块也是一个宏任务,在整体代码执行的过程中你看到的延迟任务(例如 setTimeout)将被放到下一轮宏任务中来执行。
-
宏任务和微任务都是先进先出。
console.log('1'); new Promise(function(resolve){ console.log('2'); resolve() }).then(function(){ console.log('3') setTimeout(function(){ console.log('4') }); }); setTimeout(function(){ console.log('5') }); // 1 2 3 5 4
setTimeout 注意事项
调用 setTimeout 函数时,您实际上是在告诉浏览器:在未来的某个时间点,将这个函数添加到宏任务队列中以供执行,而不是立即加入到宏任务队列,在x秒后开始执行代码。
//代码1:
setTimeout(()=>{
console.log(1);
}, 5000);
setTimeout(()=>{
console.log(2);
}, 5000);
setTimeout(()=>{
console.log(3);
}, 0);
// 先输出 3,5秒钟后几乎同时输出 1 和 2
//代码2:
Promise.resolve().then(() => {
// 假设这里的操作需要5秒
const startTime = Date.now();
while (Date.now() - startTime < 5000) {}
console.log('1'); // 微任务
});
Promise.resolve().then(() => {
// 假设这里的操作需要5秒
const startTime = Date.now();
while (Date.now() - startTime < 5000) {}
console.log('2'); // 微任务
});
Promise.resolve().then(() => {
console.log('3'); // 微任务
});
// 等5秒输出 1,再等5秒输出 2,然后几乎同时输出 3
为什么我们在实际开发中,使用 axios 调用各个接口时几乎是同时进行的?
当你使用 axios 或其他 HTTP 客户端库发起一个HTTP请求时,HTTP 请求本身不会被视为一个宏任务或微任务。HTTP 请求会在浏览器的网络层(或在 Node.js 的 HTTP 客户端)中开始执行。这些请求是异步的,因此 JavaScript 会继续执行后续的代码,而不会等待请求完成。
当 HPPT 请求完成时,如果使用的是 axios,浏览器会返回一个 Promise 对象,Promise 会被解决(resolve),并传递一个响应对象给 .then() 方法的回调函数。如果请求失败(例如,网络错误或服务器返回了错误状态码),Promise 会被拒绝(reject),并传递一个错误对象给 .catch() 方法的回调函数。Promise 的解决和拒绝会触发微任务的执行。具体来说,当 Promise 被解决或拒绝时,对应的 .then() 或 .catch() 回调函数会被添加到微任务队列中。
这就是各个接口时几乎是同时进行的原因。
async/await
在很多时候 async 和 Promise 的解法差不多,又有些不一样。
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
console.log('start')
答案:
'async1 start'
'async2'
'start'
'async1 end'
可以理解为:紧跟着 await 后面的语句相当于放到了 new Promise 中,下一行及之后的语句相当于放在 Promise.then 中。
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
setTimeout(() => {
console.log('timer')
}, 0)
console.log("async2");
}
async1();
console.log("start")
答案:
'async1 start'
'async2'
'start'
'async1 end'
'timer'
没错,定时器始终还是最后执行的,它被放到下一条宏任务的延迟队列中。