吊打面试官之一文吃透JS事件循环EventLoop

Posted 胡哥有话说

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了吊打面试官之一文吃透JS事件循环EventLoop相关的知识,希望对你有一定的参考价值。


本文由 dellyoung 独家授权发布,如果觉得文章有帮助,欢迎点击阅读原文给作者点个赞~

前言

「 本文共 8606 字,预计阅读全文需要 28 分钟 」

本文将从万物初始讲起JS世界的运转规则,也就是事件循环,在这个过程中你就能明白为什么需要这些规则。有了规则JS世界才能稳稳的运转起来,所以这些规则非常重要,但是你真的了解它们了吗?

阅读本文前可以思考下面几个问题:

  • 你理解中的事件循环是怎样的?
  • 有宏任务了,为什么还要有微任务,它们又有什么关系?
  • promise非常重要,你可以手撕 promise/A+规范了吗?
  • async/await底层实现原理是什么?

本文将会由浅入深的解答这些问题

深入理解JS系列

  • 第一节:深入理解JS的深拷贝
  • 第二节:深入理解JS的原型和原型链
  • 第三节:深入理解JS的事件循环

万物初始

本文基于chromium内核讲解

刚开始让万物运转是件挺容易的事情,毕竟刚开始嘛,也没什么复杂事,比如有如下一系列任务:

  • 任务1:1 + 2
  • 任务2:3 / 4
  • 任务3:打印出 任务1 和 任务2 结果

把任务转换成JS代码长这样:

function MainThread({
    let a = 1 + 2;
    let b = 3 / 4;
    console.log(a + b)
}

JS世界拿到这个任务一看很简单啊:首先建一条流水线(一个单线程),然后依次处理这三个任务,最后执行完后撤掉流水线(线程退出)就行了。

吊打面试官之一文吃透JS事件循环EventLoop

现在咱们的事件循环系统很容易就能处理这几个任务了,可以得出:

  • 单线程解决了处理任务的问题:如果有一些确定好的任务,可以使用一个单线程来 按照顺序处理这些任务。

但是有一些问题:

  • 但并不是所有的任务都是在执行之前统一安排好的,很多时候,新的任务是在线程运行过程中产生的
  • 在线程执行过程中,想加入一个新任务,但是现在这个线程执行完当前记录的任务就直接退出了

世界循环运转

要想解决上面的问题,就需要引入循环机制,让线程持续运转,再来任务就能执行啦

转换成代码就像这样

function MainThread({
    while(true){
        ······
    }
}

吊打面试官之一文吃透JS事件循环EventLoop

现在的JS的事件循环系统就能持续运转起来啦:

  • 循环机制解决了不能循环执行的问题:引入了循环机制,通过一个 while 循环语句,线程会一直循环执行

不过又有其他问题出现了:

  • 别的线程要交给我这个主线程任务,并且还可能短时间内交给很多的任务。这时候该如何优化来处理这种情况呢?

任务放入队列

交给主线程的这些任务,肯定得按一定顺序执行,并且还要得主线程空闲才能做这些任务,所以就需要先将这些任务按顺序存起来,等着主线程有空后一个个执行。

但是如何按顺序存储这些任务呢?

很容易想到用队列,因为这种情况符合队列“先进先出”的特点,也就是说 要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。

吊打面试官之一文吃透JS事件循环EventLoop

有了队列之后,主线程就可以从消息队列中读取一个任务,然后执行该任务,主线程就这样一直循环往下执行,因此只要消息队列中有任务,主线程就会去执行。

我们要注意的是:

  • javascript V8引擎是在 渲染进程的 主线程上工作的

结果如下图所示:

吊打面试官之一文吃透JS事件循环EventLoop

其实渲染进程会有一个IO线程:IO线程负责和其它进程IPC通信,接收其他进程传进来的消息,如图所示:

吊打面试官之一文吃透JS事件循环EventLoop

咱们现在知道页面主线程是如何接收外部任务了:

  • 如果其他进程想要发送任务给页面主线程,那么先通过 IPC 把任务发送给渲染进程的 IO 线程,IO 线程再把任务发送给页面主线程

到现在,其实已经完成chromium内核基本的事件循环系统了:

  • JavaScript V8引擎渲染进程的 主线程上工作
  • 主线程有 循环机制,能在线程运行过程中,能接收并执行新的任务
  • 交给主线程执行的任务会先放入 任务队列中,等待主线程空闲后依次调用
  • 渲染进程会有一个IO线程: IO线程负责和其它进程IPC通信,接收其他进程传进来的消息

完善运转规则

现在已经知道:页面线程所有执行的任务都来自于任务队列。任务队列是“先进先出”的,也就是说放入队列中的任务,需要等待前面的任务被执行完,才会被执行。

这就导致两个问题了:

  • 如何处理高优先级的任务?
  • 如何处理执行时间长的任务?

如何解决这两个问题呢?

处理高优先级的任务-微任务

以监听dom变化为例,如果dom变化则触发任务回调,但是如果将这个任务回调放到队列尾部,等到轮到它出队列,可能已经过去一段时间了,影响了监听的实时性。并且如果变化很频繁的话,往队列中插入了这么多的任务,必然也降低了效率。

所以需要一种既能兼顾实时性,又能兼顾效率的方法。

解决方案V8引擎已经给出了:在每个任务内部,开辟一个属于该任务的队列,把需要兼顾实时性和效率的任务,先放到这个任务内部的队列中等待执行,等到当前任务快执行完准备退出前,执行该任务内部的队列。咱们把放入到这个特殊队列中的任务称为微任务。

这样既不会影响当前的任务又不会降低多少实时性。

如图所示以任务1放为例:

吊打面试官之一文吃透JS事件循环EventLoop

可以总结一下:

  • 任务队列中的任务都是 宏观任务
  • 每个宏观任务都有一个自己的微观任务队列
  • 微任务在当前宏任务中的 JavaScript快执行完成时,也就在 V8引擎 准备退出全局执行上下文并清空调用栈的时候, V8引擎检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。
  • V8引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束。也就是说在执行微任务 过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。

我们来看看微任务怎么产生?在现代浏览器里面,产生微任务只有两种方式。

  • 第一种方式是使用 MutationObserver监控某个DOM节点,然后再通过 JavaScript来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
  • 第二种方式是使用 Promise,当调用 Promise.resolve()或者 Promise.reject() 的时候,也会产生微任务。

而常见的宏任务又有哪些呢?

  • 定时器类: setTimeout、setInterval、setImmediate
  • I/O操作:比如读写文件
  • 消息通道: MessageChannel

并且我们要知道:

  • 宿主(如浏览器)发起的任务称为宏观任务
  • JavaScript 引擎发起的任务称为微观任务

处理执行时间长的任务-回调

要知道排版引擎 BlinkJavaScript引擎 V8都工作在渲染进程的主线程上并且是互斥的。

在单线程中,每次只能执行一个任务,而其他任务就都处于等待状态。如果其中一个任务执行时间过久,那么下一个任务就要等待很长时间。

如果页面上有动画,当有一个JavaScript任务运行时间较长的时候(比如大于16.7ms),主线程无法交给排版引擎 Blink来工作,动画也就无法渲染来,造成卡顿的效果。这当然是非常糟糕的用户体验。想要避免这种问题,就需要用到回调来解决。

从底层看setTimeout实现

到现在已经知道了,JS世界是由事件循环和任务队列来驱动的。

setTimeout大家都很熟悉,它是一个定时器,用来指定某个函数在多少毫秒后执行。那浏览器是怎么实现setTimeout的呢?

要搞清楚浏览器是怎么实现setTimeout就先要弄明白两个问题:

  • setTimeout任务存到哪了?
  • setTimeout到时间后怎么触发?
  • 取消 setTimeout是如何实现的?

setTimeout任务存到哪了

首先要清楚,任务队列不止有一个,Chrome还维护着一个延迟任务队列,这个队列维护了需要延迟执行的任务,所以当你通过Javascript调用setTimeout时,渲染进程会将该定时器的回调任务添加到延迟任务队列中。

回调任务的信息包含:回调函数、当前发起时间、延迟执行时间

具体我画了个图:

吊打面试官之一文吃透JS事件循环EventLoop

setTimeout到时间后怎么触发

当主线程执行完任务队列中的一个任务之后,主线程会对延迟任务队列中的任务,通过当前发起时间和延迟执行时间计算出已经到期的任务,然后依次的执行这些到期的任务,等到期的任务全部执行完后,主线程就进入到下一次循环中。具体呢我也画了个图:

吊打面试官之一文吃透JS事件循环EventLoop

ps:为了讲清楚,画配图真的好累哦

以上是关于吊打面试官之一文吃透JS事件循环EventLoop的主要内容,如果未能解决你的问题,请参考以下文章

吊打面试官之消息队列基础

吊打面试官之源码中用了哪些设计模式?

大厂面试系列吊打面试官之Redis高频面试题(最新版)

前端面试必问:Js事件循环机制Eventloop

搞懂js中 eventloop事件循环和Promise面试题

面试率 90% 的JS事件循环Event Loop,看这篇就够了!! !