天天看点

JS 中的事件循环(Event Loop)和微任务与宏任务完全解析

作者:互联网高级架构师

介绍

首先,地球人都知道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)是一个持续运行的机制,它不断地执行任务队列中的任务。一轮事件循环通常包括以下几个阶段:

  1. 执行当前宏任务:从宏任务队列中取出一个宏任务执行。
  2. 执行当前宏任务中产生的微任务:当一个宏任务执行完毕后,会立即处理所有已经排队的微任务,按照它们被添加到微任务队列的顺序依次执行。
  3. 更新渲染:如果需要进行页面渲染,会执行相应的渲染操作。
  4. 检查是否有 Web Worker 任务:如果有,则执行 Web Worker 任务。
  5. 进入下一轮事件循环:检查是否有新的宏任务需要执行。如果有,跳转到第 1 步,否则继续等待新的任务被添加到队列中。 一轮事件循环的结束并不一定意味着整个程序的结束,它只是按照上述流程完成了一次任务的执行。事件循环会持续不断地运行,等待新的任务被添加到队列中,并按照上述流程执行。

所以输出顺序为 同步任务->异步任务(微任务->宏任务)

或者 宏任务->微任务->宏任务

代码例子

<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.then”
  • 第一轮事件循环结束,开始第二轮事件循环
  • 宏任务有setTimeout对应的回调函数,立即执行输出“这是定时器”

输出结果

  • Start
  • 这是Promise构造函数
  • End
  • 这是Promise.Then
  • 这是定时器

这次来个复杂的例子 宏任务嵌套微任务 微任务嵌套宏任务 这次把script这个大宏任务忽略 以同步任务角度开始看

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

需要注意的是,微任务的执行顺序是按照它们被添加到微任务队列的顺序来执行的。即使某个微任务的产生时间晚于其他微任务,但如果它被添加到队列较早,那么它仍然会先于其他微任务执行。

文章到这里就结束了,希望对你有所帮助

作者:ZhaiMou

链接:https://juejin.cn/post/7330300019022970915