3.消息队列和事件循环

每个渲染进程都有一个主线程,并且主线程非常繁忙,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要一个系统来统筹调度这些任务,这个统筹调度系统就是消息队列和事件循环系统
浏览器页面是由消息队列和事件循环系统来驱动的。

使用单线程处理安排好的任务

把所有任务代码按照顺序写进主线程里,等线程执行时,这些任务会按照顺序在线程中依次被执行;等所有任务执行完成之后,线程会自动退出
初始线程

  • 在线程运行过程中处理新任务
    • 要想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制
      • 循环机制
        • 具体实现方式是在线程语句最后添加了一个 for 循环语句,线程会一直循环执行。
      • 引入事件
        • 可以在线程运行过程中,等待用户操作的结果,等待过程中线程处于暂停状态,一旦接收到用户输入的信息,那么线程会被激活,然后执行相应的操作,最后输出结果。
        • 可以在执行过程中接受新的任务。
      • 初始化线程 <-> (等待输入任务 -> 执行任务)

引入事件循环的线程

  • 处理其他线程发送过来的任务
    • 其他线程是如何发送消息给渲染主线程
      其他线程发消息给渲染主线程
      • 渲染主线程会频繁接收到来自于 IO 线程的一些任务,接收到这些任务之后,渲染进程就需要着手处理,比如接收到资源加载完成的消息后,渲染进程就要着手进行 DOM 解析了;接收到鼠标点击的消息后,渲染主线程就要开始执行相应的 JavaScript 脚本来处理该点击事件。
      • 一个通用模式是使用消息队列
      • 消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取
      • 有了队列之后就可以改造线程模型了

引入队列+事件循环线程
* 三个步骤:
* 添加一个消息队列 ->
* 把IO线程中产生的新任务添加进消息队列尾部 ->
* 渲染主线程循环的从消息队列头部中读取任务、执行任务。

  • 消息队列中的任务类型

    • 输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时器
    • 与页面相关的事件,如 JavaScript 执行、解析 DOM、样式计算、布局计算、CSS 动画
    • 以上这些事件都是在主线程中执行的,所以在编写 Web 应用时,还需要衡量这些事件所占用的时长,并想办法解决单个任务占用主线程过久的问题。
  • 页面使用单线程的缺点

    • 第一个问题是如何处理高优先级的任务。
      • 典型的场景
      • 监控 DOM 节点的变化情况 -> 根据这些变化来处理相应的业务逻辑
      • 通用的设计:
      • 利用 JavaScript 设计一套监听接口,当变化发生时,渲染引擎同步调用这些接口,这是一个典型的观察者模式
      • 存在的问题:
      • 为 DOM 变化非常频繁,如果每次发生变化的时候,都直接调用相应的 JavaScript 接口,那么这个当前的任务执行时间会被拉长,从而导致执行效率的下降。
      • 如果将这些 DOM 变化做成异步的消息事件,添加到消息队列的尾部,那么又会影响到监控的实时性,因为在添加到消息队列的过程中,可能前面就有很多任务在排队了。
      • 如果 DOM 发生变化,采用同步通知的方式,会影响当前任务的执行效率;如果采用异步方式,又会影响到监控的实时性。
      • 那该如何权衡效率和实时性呢?
    • 微任务
      • 通常我们把消息队列中的任务称为宏任务每个宏任务中都包含了一个微任务队列。微任务依然运行在当前宏任务的执行环境中。
      • 在执行宏任务的过程中,如果 DOM 有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此也就解决了执行效率的问题。
      • 宏任务中的主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题。
    • 第二个是如何解决单个任务执行时长过久的问题。
      • 所有的任务都是在单线程中执行的,所以每次只能执行一个任务,而其他任务就都处于等待状态。

      • 如果在执行动画过程中,其中有个 JavaScript 任务因执行时间过久,占用了动画单帧的时间,这样会给用户制造了卡顿的感觉,这当然是极不好的用户体验。针对这种情况,JavaScript 可以通过回调功能来规避这种问题,也就是让要执行的 JavaScript 任务滞后执行

  • 浏览器页面是如何运行的

    • 开发者工具,点击“Performance”标签,选择左上角的“start porfiling and load page”来记录整个页面加载过程中的事件执行情况。
    • Main 这个项目,其记录了主线程执行过程中的所有任务。图中灰色的就是一个个任务,每个任务下面还有子任务,其中的 Parse HTML 任务,是把 HTML 解析为 DOM 的任务。值得注意的是,在执行 Parse HTML 的时候,如果遇到 JavaScript 脚本,那么会暂停当前的 HTML 解析而去执行 JavaScript 脚本。
  • 动画

    • 部分css3的动画效果是在合成线程上实现的,不需要主线程介入,所以省去了重排和重绘的过程,这就大大提升了渲染效率。
    • JavaScript都是在在主线程上执行的,所以JavaScript的动画需要主线程的参与,所以效率会大打折扣。
  • 为什么说页面是单线程架构?

    • 默认情况下每个标签页都会配套一个渲染进程,而一个渲染进程里有主线程、合成线程、IO线程等。
    • 因为排版引擎 blinkJavaScript引擎 v8工作在渲染进程的主线程上并且是互斥的,他们都是在渲染进程的主线程上工作,所以同时只能执行一个。基于这点说页面是单线程架构。
    • 比如v8除了在主线程上执行JavaScript代码之外,还会在主线程上执行垃圾回收,所以执行垃圾回收时停止主线程上的所有任务,我们把垃圾回收这个特性叫着全停顿。

笔记内容来自极客时间李兵老师的《浏览器工作原理与实践》 学习收获了很多 感谢老师

原文地址:https://www.cnblogs.com/liyf-98/p/14411187.html