【JS】事件循环

引言

事件循环也很重要,面试提问率很高,平时用的也多,很重要很重要很重要!

消息队列与事件循环

在浏览器中,每个渲染进程都有一个主线程,并且主线程非常繁忙,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要一个系统来统筹调度这些任务,这个统筹调度系统就是消息队列事件循环系统。
消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。
主线程执行的任务都全部从消息队列中获取。所以如果有其他线程想要发送任务让主线程去执行,只需要将任务添加到该消息队列中就可以了。渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务添加入消息队列。
JS是单线程执行的,例如一些DOM操作,采用同步的方式可能会影响主线程执行效率,如果采用异步的方式又不能保证监控的实时性,所以诞生了二者相平衡的微任务。
通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,如果 DOM 有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此也就解决了执行效率的问题。等宏任务中的主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题。
而对于单线程造成的单个任务执行时间过长的问题,可以使用回调来解决。

事件循环原理

JavaScript的一大特点就是单线程,而这个线程中拥有唯一的一个事件循环。
JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。
一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。
任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。

  • macro-task(宏任务)大概包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering,用户交互。
  • micro-task(微任务)大概包括: process.nextTick, Promise(Async/Await), Object.observe(已废弃), MutationObserver(html5新特性)

setTimeout/Promise等我们称之为任务源,而进入任务队列的是他们指定的具体执行任务。即setTimeout会立即执行,但它里面的第一个参数会进入队列,等待执行。
来自不同任务源的任务会进入到不同的任务队列。其中setTimeoutsetInterval是同源的。
事件循环的顺序,决定了JavaScript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。

  • script开始第一次循环
  • 全局上下文进入函数调用栈
  • 函数依次入栈,依次执行,依次出栈,直到只有全局上下文
  • 执行微任务
  • 从宏任务开始,入栈出栈
  • 执行微任务
  • 。。。。。。

其中每一个任务的执行,无论是macro-task还是micro-task,都是借助函数调用栈来完成。

如何实现setTimeout?

在 Chrome 中除了正常使用的消息队列之外,还有另外一个消息队列,这个队列中维护了需要延迟执行的任务列表,包括了定时器和 Chromium 内部一些需要延迟执行的任务。所以当通过 JavaScript 创建一个定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中。
当通过 JavaScript 调用 setTimeout 设置回调函数的时候,渲染进程将会创建一个回调任务,包含了回调函数、当前发起时间、延迟执行时间,创建好回调任务之后,再将该任务添加到延迟执行队列中。
在处理完消息队列中的一个任务之后,就开始执行调用延迟队列函数。该函数会根据发起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务。等到期的任务执行完成之后,再继续下一个循环过程。通过这样的方式,一个完整的定时器就实现了。
如果当前任务执行时间过久,会影响定时器任务的执行。如果当前任务已经超出了定时任务的执行时间,定时器也只能推后执行。
如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒。因为在 Chrome 中,定时器被嵌套调用 5 次以上,系统会判断该函数方法被阻塞了,如果定时器的调用时间间隔小于 4 毫秒,那么浏览器会将每次调用的时间间隔设置为 4 毫秒。

宏任务与微任务

消息队列中的任务都是宏任务,但是添加事件是由系统操作的,JavaScript 代码不能准确掌控任务要添加到队列中的位置,控制不了任务在消息队列中的位置,所以很难控制开始执行任务的时间。
微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。当 JavaScript 执行一段脚本的时候,V8 会为其创建一个全局执行上下文,在创建全局执行上下文的同时,V8 引擎也会在内部创建一个微任务队列。也就是说每个宏任务都关联了一个微任务队列。
在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。

MutationObserver

在每次 DOM 节点发生变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加进当前的微任务队列中。这样当执行到检查点的时候,V8 引擎就会按照顺序执行微任务了。
MutationObserver 采用了“异步 + 微任务”的策略。通过异步操作解决了同步操作的性能问题;通过微任务解决了实时性的问题。

Promise

Promise 中为什么要引入微任务?

由于promise采用.then延时绑定回调机制,而new Promise时又需要直接执行promise中的方法,即发生了先执行方法后添加回调的过程,此时需等待then方法绑定两个回调后才能继续执行方法回调,便可将回调添加到当前js调用栈中执行结束后的任务队列中,由于宏任务较多容易堵塞,则采用了微任务。

Promise 中是如何实现回调函数返回值穿透的?

首先Promise的执行结果保存在promisedata变量中,然后是.then方法返回值为使用resolvedrejected回调方法新建的一个promise对象,即如果成功则返回new Promise(resolved),将前一个promisedata值赋给新建的promise

Promise 出错后,是怎么通过“冒泡”传递给最后那个捕获

promise内部有resolved_rejected_变量保存成功和失败的回调,进入.then(resolved,rejected)时会判断rejected参数是否为函数,若是函数,错误时使用rejected处理错误;若不是,则错误时直接throw错误,一直传递到最后的捕获,若最后没有被捕获,则会报错。可通过监听unhandledrejection事件捕获未处理的promise错误。

例题1
console.log('start here')

new Promise((resolve, reject) => {
   
   
  console.log('first promise constructor')
  resolve()
})
  .then(() => {
   
   
    console.log('first promise then')
    return new Promise((resolve, reject) => {
   
   
      console.log('second promise')
      resolve()
    })
      .then(() => {
   
   
        console.log('second promise then')
      })
  })
  .then(() => {
   
   
    console.log('another first promise then')
  })

console.log('end here')

我们来分析一下:
首先输出 start here 没有问题;
接着到了一个 Promise 构造函数中,同步代码执行,输出 first promise constructor,同时将第一处 promise then 完成处理函数逻辑放入任务队列
继续执行同步代码,输出 end here
同步代码全部执行完毕,执行任务队列中的逻辑,输出 first promise then 以及 second promise
当在 then 方法中返回一个 Promise 时(第 9 行),第一个 promise 的第二个完成处理函数(第 17 行)会置于返回的这个新 Promise 的 then 方法(第 13 行)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值