理解js单线程异步与事件循环机制

前言

       这几天在学习过程中遇到一些问题,在找相关资料的情况下了解到js单线程和事件循环机制的一些知识,通过看一些大牛的文章,感觉又学习到了很多,对js有了更深的理解,这里来记录一下我学习完之后的一些理解。

js单线程

我们都知道js实际上是单线程的,但是很多时候很多操作其实是异步的,那么这是怎么实现的呢,js单线程该如何实现异步。其实虽然js是单线程的,但是浏览器并不是单线程的,浏览器是多线程的。

那么浏览器内核一般都有哪些常驻现成:

  • GUI渲染线程

    • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
    • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
    • 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  • JS引擎线程

    • 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
    • JS引擎线程负责解析Javascript脚本,运行代码。
    • JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
    • 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  • 事件触发线程

    • 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
    • 当JS引擎执行代码块时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
    • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
    • 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
  • 定时触发器线程

    • 传说中的setInterval与setTimeout所在线程
    • 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
    • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
    • 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
  • 异步http请求线程

    • 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
    • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

再补充一张图:

(本文图片来源及上述描述均转自这位大牛

通过以上可以看出虽然js本身是单线程的,但是浏览器内核中却有这么多的线程协助,那么自然可以实现异步。

事件循环机制

既然前面说到js是存在异步任务的,并且浏览器内核中存在这么多的线程,那么js的事件循环机制究竟是怎么样的呢。

js中存在同步任务以及异步任务

  • 同步任务在主线程中执行,形成一个任务栈 (进行最终处理的只有任务栈)
  • 浏览器内核中存在的事件触发线程,这个线程控制一个 任务队列,当这些异步任务(比如像点击事件之类的)产生了结果(那些回调函数之类的),事件触发线程就会将这些结果放到 任务队列当中

那么执行栈与任务队列之间有什么关系呢:

  • 所有的处理最终都是在任务栈中进行的,这是js的主线程。
  • 所有的异步结果也就是任务队列中的任务最终也是要交给任务栈进行处理的。
  • 当主线程空闲的时候就会主动去读取任务队列,并且执行任务队列中的可执行的任务。如此循环

这张图片很形象:

大概理解可以是在主线程中的执行栈的运行过程中会根据页面中的内容会调用到一些异步任务,那么这些异步任务产生的结果就会有事件触发线程之类的线程提交到任务队列,然后当执行栈空闲的时候就会执行这些任务,这样形成一个循环,这就是我理解的事件循环机制。

定时器

这里单独说一下定时器,其实我之所以会找这方面的资料也是因为我在使用定时器的时候遇到一些问题没有想明白,那么定时器其实是很特别的,因为定时器有一个独享的线程,就是定时器线程,之所以会有定时器线程是因为定时器需要一个定时的功能,很显然主线程如此的繁忙,交给主线程来定时是不够准确的,那么就由浏览器的一个单独的线程来定时,当达到定时器设定的时间时,定时器线程就会将要处理的特定的结果(也就是定时器的回调)推入到任务队列当中等待主线程执行。

在W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。这里可以看一个例子:

1
2
3
4
5
6
setTimeout(function () {
console.log('这是定时器的内容');
}, 10)
console.log('这是一句话');
console.log('这是另一句话');
console.log('这是又是一句话');

这段代码的执行结果会是什么?

1
2
3
4
//这是一句话
//这是另一句话
//这是又是一句话
//这是定时器的内容

  • 这就是这段代码的执行结果,如果在我了解事件循环机制之前我可能会认为因为定时器时10ms之后触发,所以会在最后。
  • 但是这里有一个问题,如果定时器后面的代码量非常多呢,执行的时间很长会怎么样,定时器中的内容还会是最后输出的吗。
  • 但是现在通过事件循环机制我知道,无论后面的代码执行需要再长的时间,定时器的时间再短,也都是定时器最后执行,就是因为事件循环机制,因为只有当主线程空闲的时候才会去执行任务队列中的内容。

关于定时器的使用差别

这里有一个问题,是直接使用setInterval还是利用setTimeout模拟,先来说说这二者之间的区别:

  • 在使用setTimeout模拟时,第二次以及之后再次开启的定时器是需要在上一次的setTimeout中的回调执行完毕之后才会再次开启,所以这之间是存在误差的,那么有多少的误差就取决于回调执行的时间。

  • 如果是直接使用setInterval则不会有setTimeout那样的情况,setInterval的定时器开启是非常准时的,不需要等待回调执行完毕就会准时的再次开启,那么这样会产生一些问题,如果上一次的回调还没有执行完下一次就来了怎么办,或者是即使上一次的执行完了,但是用了较多的时间,中间的时间间隔就会变得非常短,导致代码连续运行。

  • 当浏览器最小化的时候setInterval是不执行的,当窗口恢复的时候就会一次性全部执行。

所以最好的解决办法是用setTimeout模拟setInterval或者用requestAnimationFrame。

补充:JS高程中有提到,JS引擎会对setInterval进行优化,如果当前事件队列中有setInterval的回调,不会重复添加。不过,仍然是有很多问题

总结

通过看大牛的文章受益颇多,大牛文章的内容我没有全部仔细看完,只琢磨了我现在暂时想知道的东西,等有时间把大牛文章中的东西全部弄明白。

参考

撒网要见鱼:从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

-------------本文结束感谢您的阅读-------------