介绍
首先,地球人都知道JS是单线程的
,所以JS同时只能执行一个任务,也就是只有一个调用栈,先执行同步任务,再执行异步任务。
虽然HTML5允许JS脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变JS单线程的本质。
什么是异步任务
异步任务就是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。
什么是事件循环
事件循环(Event Loop)是 JavaScript 引擎处理异步任务的机制
。它用来管理所有的任务队列,包括宏任务和微任务队列。当 JavaScript 引擎遇到异步任务时,会将其放入相应的任务队列中,并继续执行同步代码,直到同步代码执行完毕或遇到下一个异步任务。当当前的宏任务执行完毕后,JavaScript 引擎会按照一定的规则从微任务队列中取出任务执行,直到微任务队列为空;然后再从宏任务队列中取出下一个宏任务执行。这个过程就是事件循环。
宏任务和微任务都有
宏任务有
事件的回调函数
,新程序或子程序被直接执行\<script>
,setTimeout()和setInterval()
requestAnimationFrame, i/o操作,setImmediate, UI rendering
微任务有
promise.then() catch() finally()
MutationObserver. Object.observe async/await node.js 中的process.nextTick() queueMicrotask()
事件循环怎么算一轮呢?
事件循环(Event Loop)是一个持续运行的机制,它不断地执行任务队列中的任务。一轮事件循环通常包括以下几个阶段:
- 执行当前宏任务:从宏任务队列中取出一个宏任务执行。
- 执行当前宏任务中产生的微任务:当一个宏任务执行完毕后,会立即处理所有已经排队的微任务,按照它们被添加到微任务队列的顺序依次执行。
- 更新渲染:如果需要进行页面渲染,会执行相应的渲染操作。
- 检查是否有 Web Worker 任务:如果有,则执行 Web Worker 任务。
- 进入下一轮事件循环:检查是否有新的宏任务需要执行。如果有,跳转到第 1 步,否则继续等待新的任务被添加到队列中。 一轮事件循环的结束并不一定意味着整个程序的结束,它只是按照上述流程完成了一次任务的执行。事件循环会持续不断地运行,等待新的任务被添加到队列中,并按照上述流程执行。
所以输出顺序为 同步任务->异步任务(微任务->宏任务)
或者 宏任务->微任务->宏任务
代码例子
html
<script>
console.log("Start");
setTimeout(function () {
console.log("这是定时器");
}, 0);
new Promise(() => {
console.log("这是Promise构造函数");
resolve();
}).then(() => {
console.log("这是Promise.Then");
});
console.log("End");
</script>
事件循环流程
- 整体script作为第一个宏任务进入主线程,遇到console.log(Start),输出Start
- 遇到setTimeout,其回调函数被分发到宏任务中
- 遇到newPromise,new Promise构造函数执行,输出"这是Promise构造函数"
- 遇到then被分发到微任务中
- 遇到console.log("End"),输出End
- 调用栈被清空以后 事件循环就会优先寻找微任务队列里面的任务
- 我们发现了then在微任务里面,执行输出"这是Promise构造函数"
- 第一轮事件循环结束,开始第二轮事件循环
- 宏任务有
setTimeout
对应的回调函数,立即执行输出"这是定时器" 输出结果 - Start
- 这是Promise构造函数
- End
- 这是Promise.Then
- 这是定时器
这次来个复杂的例子 宏任务嵌套微任务 微任务嵌套宏任务 这次把script这个大宏任务忽略 以同步任务角度开始看👀
js
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout1");
});
setTimeout(function () {
console.log("setTimeout2");
Promise.resolve().then(() => {
console.log("then1");
});
Promise.resolve().then(() => {
console.log("then2");
});
});
async1();
new Promise((res) => {
console.log("this is Promise");
res();
}).then(() => {
console.log("then3");
setTimeout(() => {
console.log("then3 setTimeout3");
});
});
console.log("script end");
- 遇到函数async1 async2没有执行跳过
- 遇到console.log('script start'),输出script start
- 遇到setTimeout1其回调函数分发到宏任务中
- 遇到setTimeout2其回调函数分发到宏任务中
- 遇到async1()函数执行, 遇到console.log("async1 start"),输出async start
- 在async1函数中遇到await async2() async2()优先级高于await运算符 async2()函数执行
- 在async2函数中遇到console.log("async2")输出 async2
- 回到async1函数中 ,由于
async
函数使用await
后的语句会被放入一个回调函数中,所以await后续代码分发到微任务中 - 遇到new Promise构造函数中 console.log("this is Promise"),直接执行 输出this is Promise
- then方法被分发到微任务中
- 遇到console.log("script end")
- 同步任务执行完了 开始执行异步任务 根据eventloop先执行任务队列中的微任务
- 任务队列先入先出 所以先输出'async1 end' 后输出 new Promise.then中的内容 then3,
- new Promise.then()中遇到setTimeout放到宏任务队列中,
- 事件循环第一轮结束,开始第二轮
- 宏任务队列setTimeout1拿出来输出
- 事件循环第二轮结束,开始第三轮
- setTimeout2 执行 输出setTimeout1和setTimeout2
- setTimeout2中有两个.then方法分发到微任务 再执行输出 then1和then2
- 事件循环第三轮结束,开始第四轮
- 最后一个宏任务 输出 then3 setTimeout3 所以代码输出结果
- script start
- async1 start
- async2
- this is Promise
- script end
- async1 end
- then3
- setTimeout1
- setTimeout2
- then1
- then2
- then3 setTimeout3
需要注意的是,微任务的执行顺序是按照它们被添加到微任务队列的顺序来执行的。即使某个微任务的产生时间晚于其他微任务,但如果它被添加到队列较早,那么它仍然会先于其他微任务执行。