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