聊一聊 js的事件循环、进程、线程、定时器延迟问题

5 篇文章 0 订阅

概括来说是什么?

所谓Event Loop,就是事件循环,其实就是JS管理事件执行的一个流程,具体的管理办法由他具体的运行环境确定。目前JS的主要运行环境有两个,浏览器和Node.js。这两个环境的Event Loop还有点区别,本文章主要分析浏览器中的事件循环。

JS异步是怎么实现的?

我们都知道JS是单线程的,那单线程是怎么实现异步的呢?事实上所谓的"JS是单线程的"只是指JS的主运行线程只有一个,而不是整个运行环境都是单线程。JS的运行环境主要是浏览器,以大家都很熟悉的Chrome的内核为例,他不仅是多线程的,而且是多进程的:

进程和线程的关系,可以用个比喻来理解:
一个进程中有多个线程,就可以理解为一趟火车有多个车厢,火车是进程,每节车厢是线程;
所以容易理解到进程之间的通信成本更高,线程之间容易通信。

那浏览器的进程和线程如下图:

浏览器中新开的一个tab标签就是一个新的进程,崩溃了不影响其他的tab,不会直接导致整个浏览器崩溃。

在这里插入图片描述

浏览器的Event Loop

事件循环就是一个循环,是各个异步线程用来通讯和协同执行的机制。各个线程为了交换消息,还有一个公用的数据区,这就是事件队列。各个异步线程执行完后,通过事件触发线程将回调事件放到事件队列,主线程每次干完手上的活儿就来看看这个队列有没有新活儿,有的话就取出来执行。画成一个流程图就是这样:

在这里插入图片描述
流程讲解如下:

  • 主线程每次执行时,先看看要执行的是同步任务,还是异步的API
  • 同步任务就继续执行,一直执行完
  • 遇到异步API就将它交给对应的异步线程,自己继续执行同步任务
  • 异步线程执行异步API,执行完后,将异步回调事件放入事件队列上
  • 主线程手上的同步任务干完后就来事件队列看看有没有任务
  • 主线程发现事件队列有任务,就取出里面的任务执行
  • 主线程不断循环上述流程

看一下同步中有耗时的任务时的情况如下:(会导致定时器不准)

const syncFunc = () => {
  const time = new Date().getTime();
  while(true) {
    if(new Date().getTime() - time > 2000) {
      break;
    }
  }
  console.log(2);
}

console.log(1);
syncFunc();
console.log(3);

上述代码会先打印出1,然后调用syncFunc,syncFunc里面while循环会运行2秒,然后打印出2,最后打印出3。所以这里代码的执行顺序跟我们的书写顺序是一致,他是同步代码。

Event Loop的这个流程里面其实还是隐藏了一些坑的,最典型的问题就是总是先执行同步任务,然后再执行事件队列里面的回调。这个特性就直接影响了定时器的执行,我们想想上边那个2秒定时器的执行流程:

  • 主线程执行同步代码 遇到setTimeout,将它交给定时器线程
  • 定时器线程开始计时,2秒到了通知事件触发线程
  • 事件触发线程将定时器回调放入事件队列,异步流程到此结束
  • 主线程如果有空,将定时器回调拿出来执行,如果没空这个回调就一直放在队列里。

看一个综合的例子,代码如下:

const syncFunc = (startTime) => {
  const time = new Date().getTime();
  while(true) {
    if(new Date().getTime() - time > 5000) {
      break;
    }
  }
  const offset = new Date().getTime() - startTime;
  console.log(`syncFunc run, time offset: ${offset}`);
}

const asyncFunc = (startTime) => {
  setTimeout(() => {
    const offset = new Date().getTime() - startTime;
    console.log(`asyncFunc run, time offset: ${offset}`);
  }, 2000);
}

const startTime = new Date().getTime();

asyncFunc(startTime);

syncFunc(startTime);

执行结果如下:
在这里插入图片描述
通过结果可以看出,虽然我们先调用的asyncFunc,虽然asyncFunc写的是2秒后执行,但是syncFunc的执行时间太长,达到了5秒,asyncFunc虽然在2秒的时候就已经进入了事件队列,但是主线程一直在执行同步代码,一直没空,所以也要等到5秒后,同步代码执行完毕才有机会执行这个定时器回调。所以再次强调,写代码时一定不要长时间占用主线程。

事件队列里面的事件可以分两类:宏任务和微任务。

微任务拥有更高的优先级,当事件循环遍历队列时,先检查微任务队列,如果里面有任务,就全部拿来执行,执行完之后再执行一个宏任务。执行每个宏任务之前都要检查下微任务队列是否有任务,如果有,优先执行微任务队列。所以完整的流程图如下:

上图需要注意以下几点:

  • 一个Event Loop可以有一个或多个事件队列,但是只有一个微任务队列。
  • 微任务队列全部执行完会重新渲染一次
  • 每个宏任务执行完都会重新渲染一次
  • requestAnimationFrame处于渲染阶段,不在微任务队列,也不在宏任务队列

在本次事件循环中,产生的新的微任务和宏任务何时执行?

  • 新的微任务:在当前微任务执行期间,如果产生新的微任务,这些新微任务会被添加到微任务队列,并会在当前微任务队列完成后立即执行
  • 新的宏任务:新的宏任务会被添加到任务队列的末尾,并会在下一轮事件循环中执行

常见宏任务有:

script (可以理解为外层同步代码)
setTimeout/setInterval
setImmediate(Node.js)
I/O
UI事件
postMessage

常见微任务有:

Promise
process.nextTick(Node.js)
Object.observe
MutaionObserver

  • 34
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值