吊打面试官之一文吃透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世界拿到这个任务一看很简单啊:首先建一条流水线(一个单线程),然后依次处理这三个任务,最后执行完后撤掉流水线(线程退出)就行了。
现在咱们的事件循环系统很容易就能处理这几个任务了,可以得出:
-
单线程解决了处理任务的问题:如果有一些确定好的任务,可以使用一个单线程来 按照顺序处理这些任务。
但是有一些问题:
-
但并不是所有的任务都是在执行之前统一安排好的,很多时候,新的任务是在线程运行过程中产生的 -
在线程执行过程中,想加入一个新任务,但是现在这个线程执行完当前记录的任务就直接退出了
世界循环运转
要想解决上面的问题,就需要引入循环机制,让线程持续运转,再来任务就能执行啦
转换成代码就像这样
function MainThread() {
while(true){
······
}
}
现在的JS的事件循环系统就能持续运转起来啦:
-
循环机制解决了不能循环执行的问题:引入了循环机制,通过一个 while
循环语句,线程会一直循环执行
不过又有其他问题出现了:
-
别的线程要交给我这个主线程任务,并且还可能短时间内交给很多的任务。这时候该如何优化来处理这种情况呢?
任务放入队列
交给主线程的这些任务,肯定得按一定顺序执行,并且还要得主线程空闲才能做这些任务,所以就需要先将这些任务按顺序存起来,等着主线程有空后一个个执行。
但是如何按顺序存储这些任务呢?
很容易想到用队列,因为这种情况符合队列“先进先出”的特点,也就是说 要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。
有了队列之后,主线程就可以从消息队列中读取一个任务,然后执行该任务,主线程就这样一直循环往下执行,因此只要消息队列中有任务,主线程就会去执行。
我们要注意的是:
-
javascript V8引擎是在 渲染进程的 主线程上工作的
结果如下图所示:
其实渲染进程会有一个IO线程:IO线程负责和其它进程IPC通信,接收其他进程传进来的消息,如图所示:
咱们现在知道页面主线程是如何接收外部任务了:
-
如果其他进程想要发送任务给页面主线程,那么先通过 IPC 把任务发送给渲染进程的 IO 线程,IO 线程再把任务发送给页面主线程
到现在,其实已经完成chromium内核
基本的事件循环系统了:
-
JavaScript V8引擎
在 渲染进程的 主线程上工作 -
主线程有 循环机制,能在线程运行过程中,能接收并执行新的任务 -
交给主线程执行的任务会先放入 任务队列中,等待主线程空闲后依次调用 -
渲染进程会有一个IO线程: IO线程负责和其它进程IPC通信,接收其他进程传进来的消息
完善运转规则
现在已经知道:页面线程所有执行的任务都来自于任务队列。任务队列是“先进先出”的,也就是说放入队列中的任务,需要等待前面的任务被执行完,才会被执行。
这就导致两个问题了:
-
如何处理高优先级的任务? -
如何处理执行时间长的任务?
如何解决这两个问题呢?
处理高优先级的任务-微任务
以监听dom变化为例,如果dom变化则触发任务回调,但是如果将这个任务回调放到队列尾部,等到轮到它出队列,可能已经过去一段时间了,影响了监听的实时性。并且如果变化很频繁的话,往队列中插入了这么多的任务,必然也降低了效率。
所以需要一种既能兼顾实时性,又能兼顾效率的方法。
解决方案V8引擎
已经给出了:在每个任务内部,开辟一个属于该任务的队列,把需要兼顾实时性和效率的任务,先放到这个任务内部的队列中等待执行,等到当前任务快执行完准备退出前,执行该任务内部的队列。咱们把放入到这个特殊队列中的任务称为微任务。
这样既不会影响当前的任务又不会降低多少实时性。
如图所示以任务1
放为例:
可以总结一下:
-
任务队列中的任务都是 宏观任务 -
每个宏观任务都有一个自己的微观任务队列 -
微任务在当前宏任务中的 JavaScript
快执行完成时,也就在V8引擎
准备退出全局执行上下文并清空调用栈的时候,V8引擎
会 检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。 -
V8引擎
一直循环执行微任务队列中的任务,直到队列为空才算执行结束。也就是说在执行微任务 过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。
我们来看看微任务怎么产生?在现代浏览器里面,产生微任务只有两种方式。
-
第一种方式是使用 MutationObserver
监控某个DOM节点,然后再通过JavaScript
来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。 -
第二种方式是使用 Promise
,当调用Promise.resolve()
或者Promise.reject()
的时候,也会产生微任务。
而常见的宏任务又有哪些呢?
-
定时器类: setTimeout、setInterval、setImmediate
-
I/O操作:比如读写文件 -
消息通道: MessageChannel
并且我们要知道:
-
宿主(如浏览器)发起的任务称为宏观任务 -
JavaScript 引擎发起的任务称为微观任务
处理执行时间长的任务-回调
★要知道
”排版引擎 Blink
和JavaScript引擎 V8
都工作在渲染进程的主线程上并且是互斥的。
在单线程中,每次只能执行一个任务,而其他任务就都处于等待状态。如果其中一个任务执行时间过久,那么下一个任务就要等待很长时间。
如果页面上有动画,当有一个JavaScript
任务运行时间较长的时候(比如大于16.7ms),主线程无法交给排版引擎 Blink
来工作,动画也就无法渲染来,造成卡顿的效果。这当然是非常糟糕的用户体验。想要避免这种问题,就需要用到回调来解决。
从底层看setTimeout实现
到现在已经知道了,JS世界是由事件循环和任务队列来驱动的。
setTimeout
大家都很熟悉,它是一个定时器,用来指定某个函数在多少毫秒后执行。那浏览器是怎么实现setTimeout
的呢?
要搞清楚浏览器是怎么实现setTimeout
就先要弄明白两个问题:
-
setTimeout
任务存到哪了? -
setTimeout
到时间后怎么触发? -
取消 setTimeout
是如何实现的?
setTimeout
任务存到哪了
首先要清楚,任务队列不止有一个,Chrome还维护着一个延迟任务队列,这个队列维护了需要延迟执行的任务,所以当你通过Javascript
调用setTimeout
时,渲染进程会将该定时器的回调任务添加到延迟任务队列中。
回调任务的信息包含:回调函数、当前发起时间、延迟执行时间
具体我画了个图:
setTimeout
到时间后怎么触发
当主线程执行完任务队列中的一个任务之后,主线程会对延迟任务队列中的任务,通过当前发起时间和延迟执行时间计算出已经到期的任务,然后依次的执行这些到期的任务,等到期的任务全部执行完后,主线程就进入到下一次循环中。具体呢我也画了个图:
★ps:为了讲清楚,画配图真的好累哦
以上是关于吊打面试官之一文吃透JS事件循环EventLoop的主要内容,如果未能解决你的问题,请参考以下文章