本文深入解析JavaScript单线程模型下的事件循环机制。通过区分同步与异步代码,详细阐述宏任务与微任务的执行优先级,结合Promise、setTimeout及async/await等实例,逐步拆解Event Loop的执行流程,帮助开发者准确掌握异步代码的执行顺序与底层原理。
前言
JavaScript作为单线程语言,同一时刻仅能执行单一任务。然而在处理异步操作时,主线程并未被阻塞,而是继续执行后续代码。这种非阻塞特性源于其独特的事件循环机制,本文将对此进行系统梳理。
1、JavaScript是单线程的
JavaScript采用单线程架构,仅包含一个调用栈,这决定了其并发处理能力受限。代码执行时,通过压入不同函数的执行上下文来确保顺序执行。当遇到异步任务时,JavaScript引擎不会挂起等待,而是将其挂起并继续执行栈中剩余任务。因此,JavaScript被定义为一种非阻塞、异步且具备并发能力的编程语言。
2、同步和异步
同步与异步的关系可类比餐厅排队:同步操作如同依次排队买饭,必须按顺序执行;而异步操作则像是在排队间隙玩手机,无需等待当前任务完成即可进行其他活动。两者的核心区别在于任务执行的时序安排。
同步:指在主线程中排队执行的任务,必须等待前一个任务完全结束后,才能开始执行下一个任务。这类似于函数或方法执行后,程序处于阻塞状态,直到获取返回值或消息才继续执行后续命令。
异步:指不进入主线程的任务,只有当异步任务就绪时,才会被推入主线程执行。函数或方法执行后,无需阻塞等待,只需向系统注册异步回调。当系统收到结果时,会自动触发回调函数,从而完成整个流程。
console.log(1);
setTimeout(() => {
console.log(2);
}, 0);
setTimeout(() => {
console.log(3);
}, 0);
setTimeout(() => {
console.log(4);
}, 0);
console.log(5);上述代码的执行输出顺序为 1、5、2、3、4。这一结果由事件循环机制决定,具体分析如下。
3、事件循环
事件循环过程可简述为以下步骤:
a、函数入栈,当 Stack 中执行到异步任务的时候,就将他丢给 WebAPIs ,接着执行同步任务,直到 Stack 为空;
b、在此期间 WebAPIs 完成这个事件,把回调函数放入 CallbackQueue (任务队列)中等待;
c、当执行栈为空时,Event Loop 把 Callback Queue中的一个任务放入Stack中,回到第1步。
事件循环(Event Loop)是JavaScript实现单线程且不阻塞的核心机制,也是其并发模型的基础。它负责协调事件、用户交互、脚本执行、UI渲染及网络请求等多种活动。在执行和协调各类任务时,Event Loop 维护着自己的事件队列。
事件队列存储着待执行任务,严格按照时间顺序执行。队头任务优先执行,队尾任务最后执行。每次仅执行一个任务,且执行期间不会被其他任务中断。当执行栈为空时,JS引擎会检查事件队列,若不为空,则将队首任务压入执行栈运行。
任务队列:在JavaScript中,异步任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务:
宏任务的例子很多,包括创建主文档对象、解析HTML、执行主线(或全局)JavaScript代码,更改当前URL以及各种事件,如页面加载、输入、网络事件和定时器事件。从浏览器的角度来看,宏任务代表一个个离散的、独立工作单元。运行完任务后,浏览器可以继续其他调度,如重新渲染页面的UI或执行垃圾回收。
而微任务是更小的任务。微任务更新应用程序的状态,但必须在浏览器任务继续执行其他任务之前执行,浏览器任务包括重新渲染页面的UI。微任务的案例包括promise回调函数、DOM发生变化等。微任务需要尽可能快地、通过异步方式执行,同时不能产生全新的微任务。微任务使得我们能够在重新渲染UI之前执行指定的行为,避免不必要的UI重绘,UI重绘会使应用程序的状态不连续。
当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。每次宏任务执行完毕,都会去判断微任务队列是否产生新任务,若存在就优先执行微任务,否则按序执行宏任务。
事件循环通常至少需要两个任务队列:宏任务队列和微任务队列。两种队列在同一时刻都只执行一个任务。
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
Promise.resolve()
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});
console.log("script end");按照上面的内容,分析执行步骤:
1、宏任务:执行整体代码(相当于<script>中的代码):
- 输出: script start
- 遇到 setTimeout,加入宏任务队列,当前宏任务队列(setTimeout)
- 遇到 promise,加入微任务,当前微任务队列(promise1)
- 输出:script end
2、微任务:执行微任务队列(promise1)
- 输出:promise1,then 之后产生一个微任务,加入微任务队列,当前微任务队列(promise2)
- 执行 then,输出promise2
- 执行渲染操作,更新界面。
- 宏任务:执行 setTimeout
- 输出:setTimeout
注意:new Promise(..)中的代码,也是同步代码,会立即执行。只有then之后的代码,才是异步执行的代码,是一个微任务。
console.log("script start");
setTimeout(function () {
console.log("timeout1");
}, 10);
new Promise((resolve) => {
console.log("promise1");
resolve();
setTimeout(() => console.log("timeout2"), 10);
}).then(function () {
console.log("then1");
});
console.log("script end");步骤解析:
当前任务队列:微任务: [], 宏任务:[<script>]
宏任务:
- 输出: script start
- 遇到 timeout1,加入宏任务
- 遇到 Promise,输出promise1,直接 resolve,将 then 加入微任务,遇到 timeout2,加入宏任务。
- 输出script end
- 宏任务第一个执行结束
- 当前任务队列:微任务[then1],宏任务[timeou1, timeout2]
微任务:
- 执行 then1,输出then1
- 微任务队列清空
- 当前任务队列:微任务[],宏任务[timeou1, timeout2]
宏任务:
- 输出timeout1
- 输出timeout2
- 当前任务队列:微任务[],宏任务[timeou2]
微任务:
- 为空跳过
- 当前任务队列:微任务[],宏任务[timeou2]
宏任务:
输出timeout2
注意:async 和 await 其实就是 Generator 和 Promise 的语法糖。async 函数和普通 函数没有什么不同,他只是表示这个函数里有异步操作的方法,并返回一个 Promise 对象
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
// Promise 写法
async function async1() {
console.log("async1 start");
Promise.resolve(async2()).then(() => console.log("async1 end"));
}下面例子:
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
setTimeout(() => {
console.log("timeout");
}, 0);
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");步骤解析:
当前任务队列:宏任务:[<script>],微任务: []
宏任务:
- 输出:async1 start
- 遇到 async2,输出:async2,并将 then(async1 end)加入微任务
- 遇到 setTimeout,加入宏任务。
- 遇到 Promise,输出:promise1,直接 resolve,将 then(promise2)加入微任务
- 输出:script end
- 当前任务队列:微任务[promise2, async1 end],宏任务[timeout]
微任务:
- 输出:promise2
- promise2 出队
- 输出:async1 end
- async1 end 出队
- 微任务队列清空
- 当前任务队列:微任务[],宏任务[timeout]
宏任务:
- 输出:timeout
- timeout 出队,宏任务清空
到此这篇关于一文详解JS中的事件循环机制的文章就介绍到这了,更多相关JS事件循环机制内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!