玩转事件循环机制

1.引言

  1. 事件循环机制就是JavaScript中处理异步操作的核心机制,它确保了代码的执行顺序符合预期的顺序。
  2. 众所周知,JavaScript是一个单线程的语言 ,这就意味着它一次只会执行一个任务,那这样的话就会造成一个问题就是如果有一个线程阻塞的话,整个程序都会被阻塞
  3. 为了解决这个问题,JavaScript引入了事件循环机制,它允许JavaScript在执行任务的同时,处理异步操作。 这样我们提高了程序的性能,同时也确保了代码的执行顺序符合预期的顺序。
  4. 循环 就体现了这个过程它是往复的,直到没有任务需要处理。 4 事件循环机制 是我们异步编程 的基础,Promise,Generator,Async/Await等都是基于事件循环机制的。

2.基础理论

2.1 事件循环机制的基本原理

  • 事件循环机制的基本原理 就是JavaScript它会去维护一个执行栈和一个任务队列,每一次执行任务的时候,都会将任务放到执行栈中去执行。
  • JS任务分为同步任务和异步任务,同步任务会直接进入执行栈中执行,而异步任务则会先被放到任务队列中等待执行。
  • 执行栈中的任务执行完毕后,JS引擎会去任务队列中读取一个待执行的任务,将其放到执行栈中执行。
  • 如此往复,直到任务队列为空,事件循环机制结束。

2.2 这里我们来举个例子讲述一下setTimeout/setInterval(指定定时任务)以及XHR/fetch(发送网络请求)它们到底做了什么事情

setTimeout/setInterval以及XHR/fetch这些代码执行时,本身是一个同步任务,但是它们的回调函数是异步任务,

  1. 当遇到setTimeout/setInterval代码时,JS引擎会先通知定时触发器线程,告诉它有一个定时任务需要执行, 然后继续执行后面的同步任务,定时触发器线程会等待到指定的时间后,将回调函数放到任务队列中等待执行。
  2. 当遇到XHR/fetch代码时 ,JS引擎会先通知异步http请求线程,告诉它有一个网络请求需要发送, 然后继续执行后面的同步任务,异步http请求线程会等待网络请求的响应,在请求成功之后,异步http请求线程将回调函数放到任务队列中等待执行。
  • 当我们同步任务执行完之后,JS引擎会询问事件触发线程,是否有待执行的回调函数,
  • 如果有,则将回调函数放到执行栈中执行,如果没有,JS引擎线程将保持空闲状态,等待新的任务到来。
  • 这样就实现了异步任务和同步任务的交替执行。

3.宏任务与微任务

** 3.1 宏任务与微任务的概念**

  • 宏任务(macrotask)和微任务(microtask)是事件循环机制中的两个重要概念。
  • 宏任务通常包括:setTimeout、setInterval、I/O、UI渲染等。
  • 微任务通常包括:Promise、MutationObserver、process.nextTick等。

3.2 宏任务与微任务的执行顺序

  • 宏任务是在事件循环的每个迭代中按顺序执行,每次迭代从宏任务队列中取出一个任务来执行。
  • 微任务在当前宏任务执行完毕后、下一次宏任务开始前执行,且会立即在当前执行栈中连续执行直到微任务队列为空。

4.实践技巧

4.1 下面我们玩几个例子,来熟悉一下事件循环机制

javascript 复制代码
   例1:
   console.log('Start');

   setTimeout(() => {
     console.log('setTimeout Callback');
   }, 0);

   Promise.resolve().then(() => {
     console.log('Promise then');
   });

   console.log('End');
  来来来,让我们一起来狠狠地分析一波
  • 首先肯定是执行同步代码 ,所以先打印出Start,然后遇到了setTimeout
  • 它是一个宏任务 ,所以将其放入宏任务队列 中等待执行,然后遇到了Promise
  • 它是一个微任务 ,所以将其放入微任务队列 中等待执行,然后继续执行同步代码 ,打印出End
  • 然后就要去执行微任务 ,此时微任务队列中有一个微任务
  • 于是将其取出并执行,打印出Promise then
  • 然后再去执行宏任务 ,此时宏任务队列 中有一个宏任务
  • 于是将其取出并执行,打印出setTimeout Callback
  • 最后事件循环机制结束。

所以打印结果应为 Start End Promise then setTimeout Callback

javascript 复制代码
 例2:
 console.log('Start');

 new Promise((resolve) => {
   console.log('Promise Executor');
   resolve();
 }).then(() => {
   console.log('Promise then');
 });

 console.log('End');

来呗又满上,

  • 首先肯定是执行同步代码 ,所以先打印出Start

  • 然后遇到了**PromisePromise构造函数内的代码立即执行(作为当前宏任务的一部分),其 then回调**作为微任务在所有同步代码执行完后执行,

  • 然后继续执行同步代码 ,打印出End

  • 然后就要去执行微任务,此时微任务队列中有一个微任务,

  • 于是将其取出并执行,打印出Promise then

  • 最后事件循环机制结束。

    所以打印结果应为 Start Promise Executor End Promise then

javascript 复制代码
例3:
console.log('Start');

async function asyncFunction() {
  await new Promise((resolve) => {
    console.log('Promise');
    setTimeout(resolve, 0)
  });
  console.log('asyncawait');
}

asyncFunction();

console.log('End');

两个怎么够呢,再来一个

  • 同步代码执行 :首先确实会打印出 "Start",因为这是最先遇到的同步代码。

  • 进入asyncFunction :接着执行asyncFunction。在asyncFunction内部,首先打印出 "Promise",这是因为在Promise构造函数内的同步代码会立即执行。

  • 遇到await :当执行到await new Promise(...)时,asyncFunction会在此暂停,等待Promise解决。

  • 继续执行全局脚本:在await等待期间,控制权返回到调用者,因此console.log('End')被执行,打印出 "End"

  • 事件循环与微任务 :当setTimeout设定的0毫秒延迟到达后,其回调函数(即resolve)被加入到宏任务队列(而非微任务队列,这是一个常见的误解,因为setTimeout是典型的宏任务源)。当当前执行栈为空,且微任务队列处理完毕后,事件循环会检查宏任务队列并执行setTimeout的回调,从而解决之前的Promise

  • Promise解决后的微任务Promise被解决后,await后面的代码(console.log('asyncawait'))被加入到微任务队列。在下一次事件循环检查微任务队列时,这部分代码会被执行,因此打印出 "asyncawait"。

  • 最后事件循环机制结束。

    所以打印结果应为 Start Promise End asyncawait

相信你通过上面三个例子,对这个事件循环机制能够有很好的理解

4.2 性能优化:利用事件循环机制

  1. 减少UI阻塞 :将耗时操作放入微任务或宏任务队列末尾,确保UI线程可以及时响应用户交互。例如,使用requestAnimationFrame进行动画渲染,确保与浏览器的绘制周期同步,减少页面重绘的开销。
  2. 拆分长任务 :如果某个任务执行时间过长,考虑将其拆分为多个小任务,利用事件循环机制插入其他任务,比如UI更新,这样可以保持应用的响应性。例如,将大数据量的处理分割成多次处理,每处理一部分就yield出控制权。
  3. 优先使用Promiseasync/await :相比传统的回调函数,Promiseasync/await提供了更清晰的代码结构和更好的错误处理机制,同时它们对事件循环的管理更加高效,特别是async/await使得异步代码看起来更像同步代码,易于理解和维护。
  4. 避免过度使用微任务:虽然微任务有较高的优先级,但过度依赖微任务会导致它们堆积,特别是在递归调用或复杂逻辑中,可能无意中造成性能瓶颈。合理安排宏任务与微任务的使用,平衡执行效率和响应性。
  5. 利用nextTicknextTickVue.js中用于在DOM更新后执行某些操作的API,它利用了事件循环机制,确保在DOM更新完成后执行。利用nextTick可以避免在DOM更新期间进行DOM操作,提高性能。

5.深入理解

5.1 Node.js事件循环模型

  • 关于Node.js事件循环模型,可以参考Node.js��方文档,
  • 这里简单介绍一下,Node.js事件循环模型分为6个阶段,
  • 每个阶段都有一个FIFO的宏任务队列和一个FIFO的微任务队列,
  • 每个阶段执行完毕后都会去检查微任务队,
  • 只有当微任务队为空时才会去执行下一个阶段。
  • 每个阶段的具体任务如下: 1.timers(定时器):执行setTimeoutsetInterval的回调函数。 2.I/O callbacks(I/O轮询):执行除了close事件、定时器和setImmediate的回调函数,比如IO回调,比如文件操作、网络请求等。这个阶段会不断轮询检查是否有已完成的I/O操作,如果有,则执行相应的回调。 3.idle, prepare(闲置、准备):Node.js内部使用,与用户代码关系不大。 4.poll(轮询):获取新的I/O事件,适当的条件下node将阻塞在这里。 5.check(检查):执行setImmediate的回调函数。 6.close callbacks(关闭回调):执行close事件的回调函数。 Node.js事件循环模型是Node.js平台的核心组件,它确保了事件驱动的异步编程模型能够高效地运行。

5.2 浏览器事件循环模型即上面的分析方式就是浏览器事件循环模型

5.3 边缘情况分析:

  • 1.微任务嵌套 微任务可以嵌套,即在一个微任务的执行过程中可以继续添加新的微任务到队列中。 这可能导致大量的微任务累积,若不谨慎处理,可能会引起事件循环的"饿死"现象,即长时间无法执行宏任务。
  • 2.宏任务嵌套 虽然直接的宏任务嵌套(如在一个setTimeout回调中立即调用另一个setTimeout)不会改变执行顺序, 但复杂的宏任务嵌套逻辑可能会影响事件循环的流畅性,尤其是当它们涉及I/O操作或大量计算时。
  • 3.定时器的不准确性 无论是setTimeout还是setInterval,它们的执行时间都只能保证在指定时间后至少执行一次,实际执行时间可能会晚于预期,原因包括但不限于:
    • 当前执行栈未清空,必须等待当前任务完成。
    • 宏任务队列中有其他任务等待执行。
    • 系统资源限制或高CPU占用导致的延迟。

虽然这两种环境在细节上面有所差异,但是都遵守宏任务和微任务的区分原则。

5.4 怎么理解nextTick

  • nextTickVue.js提供的一种方法,用于在Vue的异步更新队列清空后执行的一个回调函数。Vue.js为了提高性能,采用了异步更新DOM的策略,这意味着数据变化后,并不会立即去更新视图,
  • 而是当同步代码执行完成之后,进行批量更新。这样减少了对DOM的操作,提高应用性能。
  • nextTick的原理也是基于JavaScript的事件循环机制的。
  • nextTick的使用场景:
    • 1.获取更新之后的DOM元素:可以在nextTick中获取更新之后的DOM元素,确保获取到的DOM元素是最新更新的。
    • 2.避免不必要的渲染:在某些情况下,可以合并多次数据修改,通过nextTick来确保DOM元素只更新一次,减少渲染次数,提高性能。

6.最佳实践

6.1 Web WorkersService Workers 关于Web WorkersService Workers,到时候专门玩一下

7.总结

7.1 关键概念

  • 1.事件循环基础 :事件循环基础是JavaScript实现异步执行的重要机制。它基于任务队列的概念,分为宏任务和微任务
  • 2.宏任务 :主要包括DOM事件,setTimeout/setIntervalI/OUI渲染等,这些任务在每次事件循环结束之后再执行。
  • 3.微任务 :主要包括Promise回调,process.nextTick(Node.js特有)等,在JS引擎将执行栈的任务处理完之后,会在执行在同一事件循环的微任务,优先级会高于宏任务
  • 4.执行流程:先执行当前宏任务,然后清空所有微任务,接着检查是否有新的宏任务需要执行,如此循环往复。

7.2 实践技巧

  • 1.避免过度嵌套,因为微任务或宏任务嵌套过于复杂之后,会发现性能非常的低
  • 2.尽量使用async/await的组合,这回提高效率以及对异步代码会非常清晰,因为使用这个组合,会让异步代码看起来像是同步的代码。 (查阅资料发现)还有下面这一点
  • 3.控制微任务执行时机:在某些场景下,比如数据更新后立即读取DOM,使用Promise.resolve().then(...)queueMicrotask(...)来精确控制执行时机。
相关推荐
selfsuer1 小时前
Element-plus 【el-input输入框】和【el-select下拉选择框】样式修改
前端·javascript·vue.js
_志哥_4 小时前
web开发环境下启动HTTPS服务访问
前端·javascript·https
爱健身的小刘同学4 小时前
安装 electron 依赖报错
前端·javascript·electron
耶啵奶膘4 小时前
uniapp+vue2+uview2.0导航栏组件二次封装
前端·javascript·uni-app
雨中奔跑的小孩4 小时前
electron打包部署vue项目
javascript·vue.js·electron
khatung4 小时前
React——useReducer
前端·javascript·vscode·react.js·前端框架·1024程序员节
AndyGoWei6 小时前
react react-router-dom history 实现原理,看这篇就够了
前端·javascript·react.js
小仓桑6 小时前
深入理解 JavaScript 中的 AbortController
前端·javascript
换个名字不能让人发现我在摸鱼6 小时前
裁剪保存的图片黑边问题
前端·javascript
小牛itbull6 小时前
Mono Repository方案与ReactPress的PNPM实践
开发语言·前端·javascript·reactpress