js-单线程?异步?事件循环?一次性搞懂

161 阅读6分钟

什么是进程?什么是线程?

  1. 进程:程序运行的专属内存空间,进程之间相互独立,即使要通信,也要经过双方同意。例如qq和微信运行时在内存中占用的空间分别是qq进程和微信进程
  2. 线程:线程是进程的组成部分,一个进程至少有一个线程,进程开启后会有一个主线程。

浏览器的进程模型

浏览器是一个多进程多线程的应用程序

为了减少互相影响和连环崩溃的几率,浏览器启动后,会自动启动多个进程,例如:

  1. 浏览器进程:负责界面展示、用户交互、子进程管理等
  2. 网络进程:负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务
  3. 渲染进程:渲染进程启动后会开启一个渲染主线程,负责执行html、css、js代码,默认情况下,每个标签页都会开启一个新的渲染进程,保证不同标签页之间不会相互影响

image.png

渲染主线程如何工作

渲染主线程是浏览器中最繁忙的线程,它的任务包括但不限于:

  1. 解析html
  2. 解析css
  3. 计算样式
  4. 布局
  5. 处理图层
  6. 每秒把页面画60次
  7. 执行全局js代码
  8. 执行事件回调函数
  9. 执行计时器回调函数 ……

为什么浏览器的渲染进程不启用多个线程来处理这些任务?为什么js设计成单线程的?

  1. 早期的浏览器功能较为简单,js设计成单线程模式能够快速开发,更易于维护
  2. 线程之间的通信较为复杂,造成性能开销和死锁、竟态问题。比如a线程在操作xx节点,b线程要删除xx节点,那此时交互的处理就会变得极为复杂

单线程的问题?

  1. 主线程正在执行一个函数,此时用户点击了按钮,主线程是应该停下来执行用户的操作还是继续执行
  2. 主线程正在执行一个函数,此时定时器到达了时间,主线程是应该执行定时器的回调还是继续执行
  3. 用户点击按钮的同时,定时器到达了时间,先处理哪个呢

主线程承担着渲染页面的任务,一旦被某个操作打断,等这个操作完成后再往下执行,这个等待的过程就是阻塞

如何避免阻塞?--异步

由于js的单线程的,这个线程承担着渲染页面、执行js的工作,如果使用同步的方式,极有可能导致主线程产生阻塞,从而导致消息队列中的其他任务无法执行。

主线程不得不等待当前任务执行完再去执行下一个任务,白白地消耗时间,页面也无法及时更新,造成页面卡死的现象。

所以浏览器采用异步的方式来避免这种情况,比如计时器、网络请求、用户操作,这些任务就是异步任务,它们可以等待同步任务执行完再执行,最大限度保证单线程的流畅运行。

同样都是异步任务,哪个先执行?哪个后执行?--消息队列

首先,任务没有优先级,在消息队列中先进先出

消息队列具有优先级

W3C的最新解释:

  1. 每个任务都有任务类型,同一个类型的任务必须在一个队列中,不同类型的任务可以分属于不同的队列。在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行
  2. 浏览器必须准备好一个微队列,微队列中的任务优先于其他任务

随着浏览器的复杂度急剧提升,W3C不再使用宏任务的说法,在目前Chrome的实现中,包含以下队列:

  1. 延时队列:setTimeout、setInterval,优先级中
  2. 交互队列:addEventListener,优先级高
  3. 微队列:promise.then,优先级最高

所以,任务的执行顺序为:同步任务-->微队列-->交互队列-->延时队列

这么多任务,如何有条不紊的在单线程中执行?--事件循环

事件循环又叫消息循环,是浏览器渲染主线程的工作方式。在chrome源码中,初始化时开启一个永不结束的for循环,每次循环从消息队列中取出第一个任务执行,其他线程只需要在合适的时候将任务添加到消息队列的末尾。过去把消息队列分为宏队列和微队列,这种说法目前已经无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。 根据W3C的官方解释,每个任务都有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同的消息队列优先级不同,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务优先级最高。

计时器准确性问题

计时器无法做到准确计时,原因:

  1. 计算中没有原子钟,无法做到精确计时
  2. 操作系统的计时函数本身存在少量偏差,js计时器最终调用的是操作系统的函数,继承了这些偏差
  3. 按照w3c的标准,浏览器实现计时器,如果嵌套超过5层,会有4ms的最少时间,这样在计时时间少于4ms时又带来了偏差
  4. 受事件循环的影响,计时器的回调只能在主线程闲暇时执行,又带来了偏差

js模拟多线程

h5提出的worker可以模拟多线程,但是其他线程都是由主线程控制的,并且不能获取和操作DOM,所以worker的本质还是单线程的

1、先看一段比较耗时的操作,在主线程下的耗时表现

      function fb(n) {
        if (n === 1 || n === 2) return 1
        return fb(n - 1) + fb(n - 2)
      }

      console.time('fb执行时间1')
      fb(40)
      console.timeEnd('fb执行时间1')

      console.time('fb执行时间2')
      fb(40)
      console.timeEnd('fb执行时间2')

      console.time('fb执行时间3')
      fb(40)
      console.timeEnd('fb执行时间3')

动图.gif

2、使用worker

  1. 同级新增worker1.js、worker2.js、worker3.js,将耗时操作分离出去
function fb(n) {
  if (n === 1 || n === 2) return 1
  return fb(n - 1) + fb(n - 2)
}

console.time('fb执行时间worker1')
const result = fb(40)
console.timeEnd('fb执行时间worker1')

self.postMessage('worker1干完活了')
  1. 主线程下接收worker的计算结果
      const worker1 = new Worker('worker1.js')
      const worker2 = new Worker('worker2.js')
      const worker3 = new Worker('worker3.js')

      worker1.onmessage = (e) => {
        console.log(e.data)
      }
      worker2.onmessage = (e) => {
        console.log(e.data)
      }
      worker3.onmessage = (e) => {
        console.log(e.data)
      }

动图.gif

可见,三次计算的耗时和一次计算耗时差不多,当这种计算情况越多,使用worker的效果越明显。



js线程

js是单线程的,但是可以通过轮转时间片模拟多线程:

  1. 任务1 任务2
  2. 切分任务1 任务2
  3. 随机排列这些任务片段,组成队列
  4. 按照队列顺序将任务片段送进js线程
  5. js线程执行一个一个的任务片段 blog.csdn.net/fuhanghang/…

www.jianshu.com/p/8821c6432…

www.jianshu.com/p/b2a15fda9…