知识篇 -- JS事件循环与异步
Ray Shine
2024/2/24 JavaScript基础知识异步
本文为博主原创文章,遵循
CC 4.0 BY-SA
版权协议,转载请附上原文出处链接和本声明。
如有侵权,请联系
本博主
删除。
JavaScript是一种单线程语言,这意味着它一次只能执行一个任务。然而,在Web开发中,我们经常需要处理耗时的操作,如网络请求、定时器、用户交互等。如果这些操作阻塞了主线程,页面就会变得无响应。为了解决这个问题,JavaScript引入了事件循环 (Event Loop) 机制,使得JavaScript能够以非阻塞的方式处理异步任务。
# 一、JavaScript的单线程特性 核心
- 定义:JavaScript引擎只有一个主线程来执行所有代码。
- 优点:避免了多线程编程中复杂的同步问题(如死锁、竞态条件)。
- 缺点:耗时操作会阻塞主线程,导致页面卡顿。
为了在单线程模型下实现非阻塞的异步操作,JavaScript将任务分为两类:同步任务 (Synchronous Tasks) 和 异步任务 (Asynchronous Tasks)。
# 二、同步任务与异步任务 分类
# 1. 同步任务 阻塞
- 定义:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务。
- 特点:按照代码的顺序依次执行,阻塞主线程。
- 示例:函数调用、变量赋值、算术运算等大部分代码。
# 2. 异步任务 非阻塞
- 定义:不进入主线程,而是进入 任务队列 (Task Queue) 的任务。只有当主线程上的同步任务执行完毕,主线程空闲时,才会从任务队列中读取异步任务到主线程执行。
- 特点:不会阻塞主线程,允许页面保持响应。
- 常见异步操作:
- 定时器:
setTimeout,setInterval - 事件监听:
click,load,scroll等DOM事件 - 网络请求:
Ajax,Fetch - Promise
requestAnimationFrame
- 定时器:
# 三、事件循环 (Event Loop) 的工作原理 机制
事件循环是JavaScript运行时环境(如浏览器或Node.js)中的一个核心组件,它负责协调同步任务和异步任务的执行。
基本流程:
- 执行栈 (Call Stack):所有同步任务都在这里执行。当函数被调用时,它会被推入栈中;当函数执行完毕,它会被弹出栈。
- 任务队列 (Task Queue):异步任务的回调函数在满足触发条件后,会被放入任务队列中排队。
- 事件循环:
- 不断检查执行栈是否为空。
- 如果执行栈为空,事件循环就会检查任务队列。
- 如果任务队列中有任务,事件循环就会将队列中的第一个任务推入执行栈,使其执行。
示例:
console.log("Start"); // 同步任务
setTimeout(() => {
console.log("Timeout Callback"); // 异步任务(宏任务)
}, 0);
Promise.resolve().then(() => {
console.log("Promise Callback"); // 异步任务(微任务)
});
console.log("End"); // 同步任务
预期输出:
Start
End
Promise Callback
Timeout Callback
解释:
console.log("Start")进入执行栈并立即执行。setTimeout被调用,其回调函数被放入 宏任务队列。Promise.resolve().then()被调用,其回调函数被放入 微任务队列。console.log("End")进入执行栈并立即执行。- 此时,执行栈为空。事件循环开始工作。
- 事件循环首先检查 微任务队列,发现
Promise Callback,将其推入执行栈执行。 - 微任务队列清空后,事件循环检查 宏任务队列,发现
Timeout Callback,将其推入执行栈执行。
# 四、宏任务 (MacroTask) 与微任务 (MicroTask) 进阶
在ES6及之后,任务队列被进一步细分为宏任务队列和微任务队列。
# 1. 宏任务 (MacroTask) 大任务
- 来源:
setTimeout,setInterval,setImmediate(Node.js),I/O,UI rendering。 - 特点:每次事件循环只会从宏任务队列中取出一个任务执行。
# 2. 微任务 (MicroTask) 小任务
- 来源:
Promise.then(),Promise.catch(),Promise.finally(),process.nextTick(Node.js),MutationObserver。 - 特点:在当前宏任务执行完毕后,下一个宏任务开始之前,会清空所有微任务队列中的任务。
事件循环的详细步骤:
- 执行一个宏任务(从宏任务队列中取出)。
- 执行过程中如果遇到微任务,就将其添加到微任务队列中。
- 当前宏任务执行完毕,检查微任务队列。
- 如果微任务队列不为空,就依次执行所有微任务,直到微任务队列清空。
- 如果微任务在执行过程中又产生了新的微任务,也会被添加到微任务队列的末尾,并在当前轮次中被执行。
- 微任务队列清空后,渲染引擎可能会更新UI。
- 开始下一轮事件循环,重复步骤1。
示例:
console.log('script start'); // 同步任务
setTimeout(function() { // 宏任务1
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() { // 微任务1
console.log('promise1');
}).then(function() { // 微任务2
console.log('promise2');
});
console.log('script end'); // 同步任务
输出顺序:
script start(同步)script end(同步)promise1(微任务1)promise2(微任务2)setTimeout(宏任务1)
# 五、总结
事件循环是JavaScript异步编程的基石。通过将任务划分为同步和异步,并进一步细分为宏任务和微任务,JavaScript引擎能够在单线程环境下高效地处理各种操作,确保用户界面的流畅性和响应性。理解事件循环的工作机制,对于编写高性能、无阻塞的JavaScript代码至关重要。