玩转事件循环机制

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(...)来精确控制执行时机。
相关推荐
meilindehuzi_a7 分钟前
深入理解 JavaScript 的同步与异步机制:从单线程设计到 Promise 核心应用
开发语言·javascript·ecmascript
如烟花的信页9 分钟前
加速乐cookie逆向分析
javascript·爬虫·python·js逆向
永远的WEB小白14 分钟前
css改变svg图标的颜色
前端·javascript·css
ikoala34 分钟前
Codex 不得不装的 12 个插件,都在这了
前端·javascript·后端
赵庆明老师1 小时前
JS检查提交的文件是否合规
开发语言·前端·javascript
颂love2 小时前
Vue的两大生态以及组件通信
前端·javascript·vue.js·typescript
光影少年2 小时前
js单线程,为什在node环境下的js可以处理高并发请求?
前端·javascript·掘金·金石计划
moMo2 小时前
# JavaScript 的“等等我”:聊聊同步与异步
javascript
Cobyte2 小时前
19.Vue Vapor 的实现原理原来这么简单
前端·javascript·vue.js
JackieDYH2 小时前
uniapp vue3 常用的生命周期和作用使用时机
javascript·vue.js·uni-app