浏览器里面事件循环探秘

想弄清楚这个问题 ,需要了解操作系统,进程,线程,内存 浏览器等前置知识,所以先说一下,有一个整体的把控。

我们在电脑上写软件

这句话有几个主体: 人 , 物(电脑), 软件。

关于物- 电脑还可以细分为, 苹果系统电脑或者windows电脑

在计算机,软件行业 可以抽象为:

底层是硬件 - 电脑 中间层是 - 操作系统 上层是 - 应用程序

为什么要分层?

  • 可以将复杂的问题 大化小 ,小问题更好解决
  • 可以方便维护自己的那一层
  • 可以方便复用,上层依赖下层, 下层没有变化的时候 可以改上层的实现,反之亦然。

可以没有那么多层吗? 比如没有操作系统这一层

比如 可以 软件 直接访问硬件吗? 单片机不需要并发的场景 可以

什么是操作系统?

  • 软件和硬件之间的一层抽象,是一个软件,向上提供系统调用, 向下可以管理不同的硬件资源。

主要解决什么问题?

  • 解决并发,多个软件需要同时使用系统的资源的问题

为了解决这个问题,操作系统做了什么工作?

  • 进行抽象
  • 虚拟化
  • 抽象出文件系统
  • 映射 物理地址和真实地址
  • 实现了冯 诺伊曼的 理论
  • 抽象出了 进程的概念

mac可以通过activity monitor 查看

里面有一些属性: 包括 进程名字 cpu占用率 pid 进程ID 唯一的, linux上 杀掉进程一般就拿这个id来进行下一步的处理。

什么是进程?

  • 我们写的代码都保存在电脑的硬盘里面, 只有运行的时候, 加载到内存里面的,才是活的,
  • 这种对活的的内容的抽象 叫进程。 我自己的理解。 可能不那么准确

进程也有自己的生命周期: 类似人的生老病死 不同的生命周期做正确的事情

什么是线程?

线程是依附进程存在的

进程和线程之间的关系是?

因为线程和进程是 总 分的关系 所以可以是一对一存在 或者是 一对多存在。

一个进程至少需要有一个线程吗?

一个进程中什么情况需要多线程?

为什么说内存很宝贵?

软件运行的时候 操作系统做了什么?

有一句很重要的话: 操作系统创建一个进程来承载该软件的执行 , 并且分批内存空间等操作

打王者荣耀(一个软件)的时候, 会把QQ搞崩溃吗?

  • 不会, 有了上面的结论, 就可以将两个运行的软件抽象成两个进程了。
  • 因为上面说了操作系统保证进程之间是隔离的, 没有机会操作到其他软件的控制的内存。

那进程之间有办法通信吗? 想取一些数据

  • 可以 双方都同意就行, 通过IPC等方式实现

什么是浏览器?

  • 浏览器就是一个软件
  • 浏览器运行在操作系统上

浏览器有哪些能力?

可以在地址栏里面输入要访问的地址, 一般叫URL ,就是一个有特殊格式的字符串 可以浏览网页 可以添加浏览器插件 可以前进, 后退 可以刷新 可以查看访问历史 可以开一个新的tab看新的内容 可以进行SEO 可以将喜欢的内容添加到书签

最主要的就是:

将用户请求的web资源从服务器获取到并且在浏览器里面显示出来, 给用户看, 这些web资源剋有是不同的形式, 比如:

  • pdf
  • html
  • video
  • forms
  • picture 等等

下面的图片更加形象:

一个经典问题:这里面每个环节都可以深入

这里提到的OS 就是操作系统

浏览器有哪些进程?和线程?

  • 现在的浏览器很复杂的, 开始浏览器设计的比较简单,但是后面发现崩溃比较频繁, 就做了多进程, 内存隔离, 避免互相干扰。所以现在的浏览器是多进程的

我们再加深理解一遍其中的几个进程:

主进程 Main Process : 浏览器启动就创建, 负责管理和协调其他进程的工作, 主进程通常负责管理窗口、标签页、菜单、扩展插件等,以及处理用户输入和导航请求。

网络进程 Network Process: 负责网络请求和响应, 包括加载网页内容,下载文件,处理websocket连接等, 网络进程通常与渲染进程分离,防止网络请求的阻塞影响网页的渲染和用户的交互。

那我们常说的八股文 构建cssOM htmltree 发生在哪?

这就要提到渲染进程了

渲染进程 Renderer Process : 每一个标签页通常有一个独立的渲染进程,负责加载、解析和渲染网页内容。渲染进程会执行html css 和js代码 !!!! js代码是在渲染进程执行的 并且和渲染html css互斥 ,并且将渲染结果显示在用户界面上, 提供沙盒来保证每一个渲染进程的安全性。避免恶意代码对系统的攻击。

问 浏览器渲染进程 里面有多少线程?

Renderer process (渲染进程)里面包含:

  1. Main Thread (主线程):解析html css 构建DOM tree 和 CSSOM tree, 计算网页布局, 以及执行js代码,主线程还负责处理用户输入事件和页面内容。每秒把页面画60次(图A)
  2. Rendering Thread (渲染线程): 将布局和样式信息转化为图像,绘制在屏幕上
  3. Network Thread (网络线程) :处理网络的请求和响应
  4. JavaScript Thread (js引擎线程): 执行js代码,并且处理js相关的任务 比如定时器,事件处理等,
  5. Timer Thread(定时器线程): 管理定时器的执行

图A

js为什么是单线程的?

因为容易实现 不用担心死锁的问题

不要并发

那有些异步代码 怎么实现? 比如ajax 请求 promise 定时器 用户点击事件?

  • 可以调度 ! 设置优先级

渲染主线程大概会怎么处理呢?

属于异步的放在队列里面 等待主线程同步代码执行完毕, 再来异步队列里面检查, 是否队列里面有值 。 有就取出来执行。

为什么用队列?

因为渲染主线程和其他线程处理任务的速度和时间不一致, 需要一个缓冲区, 并且是 first in first out 的 ,符合这个特点的数据结构是队列。

渲染主线程具体怎么实现的呢?

源码(c++)是这样的

cpp 复制代码
run函数里面 

for( ; ; ) {

}

就是一个死循环

所以可以了解到 :

  1. 最开始的时候,渲染主线程会进入一个无限循环。
  2. 每一次循环,都会检查消息队列里面是否有任务, 如果有的话, 就取出第一个任务执行,执行完以后,进入下一次循环,如果没有则进入休眠状态
  3. 其他所有线程(包括其他进程的线程) 可以随时向消息队列的尾部添加任务。在新增加任务的时候, 如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务。

什么是异步?

  • 不能马上执行的

前端有哪些操作属于异步的?

  • 定时器 setTimeout setInterval
  • 网络请求 ajax
  • 异步处理promise
  • 用户事件 addEventListener('eventName', callback) click doubleclick mousemove mouseup ...

为什么要做成异步?

  • 渲染主线程不止要执行js代码, 还要渲染
  • 做成异步以后, 把需要处理的异步任务交给对应的处理的人, 等处理好了我再接着处理,不用在一个地方一直等待。
  • 这样主线程提高了生产效率,可以处理更多的事情

所以怎么理解js的异步? 总结一下

js是一门单线程的语言(一定要说 不说单线程就解释不了阻塞带来的问题,解释不了阻塞带来的问题就解释不了异步 ),这是因为他运行在浏览器的渲染主线程中,而渲染主线程只有一个。并不是浏览器是单线程,前面说了。 而是js执行在渲染主线程上的!js执行在渲染主线程上的!

渲染主线程不仅要执行js,还承担很多工作: 比如 页面渲染 解析html 解析css 执行计时器回调等

如果使用同步的方式,有极有可能导致主线程的阻塞,

为什么说可能, 你不用到异步的代码, 就没有阻塞。如果有异步代码,消息队列里面的内容就会很多等待执行的任务。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新, 给用户造成卡死现象 所以, 浏览器采用异步的方式来避免。

具体怎么做的呢?

当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续的代码。当其他线程完成时,将事先传递的回调函数包装成任务 (不是加回调函数本身,是结构体的一部分)整体被叫做任务加入到队列的,加入到消息队列的末尾排队(回调函数加入队列的时机是其他线程完成时才加入回调等待主线程调度执行。

在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。

js执行在渲染进程的渲染主线程中, 渲染主线程不仅只做执行js一件事,用同步就要阻塞,不阻塞的话浏览器就采用异步,并搭配消息队列,缓存回掉函数构成的任务,使得主线程永不阻塞,流畅执行, 等主线程同步任务都执行完成, 再从消息队列里面取出任务,清空消息队列。

为什么js会阻碍渲染?

  • 因为js执行和渲染都是主线程上完成的, 执行其中一个,另一个就要等。
js 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>前端关宇</h1>
    <button>change</button>
    <script>
      //异步代码上的体现有
      //1 事件监听
      //2 ajax 请求
      //3 promise / settimeout
      var h1 = document.querySelector("h1"),
        btn = document.querySelector("button");

      //duration 指定死循环的时间
      function delay(duration) {
        var startTime = Date.now();
        while (Date.now() - startTime < duration) {}
      }

      //这里属于 事件监听 1 是异步
      btn.onclick = function () {
        h1.textContent = "我在异步队列里面出队列 被主线程执行才会看到!!!";

        //js的执行 会影响上面的绘制任务, 如果一直都执行不完, 绘制任务不会从消息队列里面取出执行,
        //所以后来 框架react  出了 fiber 不让js执行太久
        delay(3000);
      };

      //代码执行过程:

      //1 代码从上到下执行 内存里面声明 h1 btn delay 发现有交互动作btn.onclik 交给交互线程处理 ,之前说了渲染线程不仅仅执行js ,还要处理html css的渲染,
      // 所以 因为html里面写了h1 里面是前端关宇 和button 里面是change, 绘制显示在页面上,本次执行完毕, 消息队列为空, 渲染线程休眠

      //2 当用户点击button按钮的时候,将回掉函数包装成任务加到消息队列里面, 渲染线程里面为空,就去消息队列里面取 ,取出来消息队列为空了,发现需要执行的是 两个内容 
      //2.1 h1.textContent = "我在异步队列里面出队列 被主线程执行才会看到!!!"; 将h1里面的内容改为这个的绘制任务, 因为绘制是一个异步任务 ,所以又被加到消息队列里面 
      //2.2 执行delay

      //3 执行delay 延迟3秒,执行完毕以后,主线程里面没有可以执行的内容, 再去 消息队列里面看是否为空,发现不为空, 有一个绘制任务, 取出来, 执行。 
      //所以绘制在js delay执行3秒后才执行。 消息队列为空, 主线程休眠。

      //4 如果delay里面的执行的时间更长, h1里面的内容就没有机会显示了。 所以要控制js的执行时间。 react里面采用fiber 实现的。

      //   每次 先执行主线程的同步代码,没有可以执行的
      //   就去消息队列里面看一下,不为空,就从消息队列里面取出来再执行,
      //   执行的过程中可能有新的任务被添加到消息队列里面,继续执行,直到清空为止。
      //   这样每次 主线程 到 消息队列  主线程 到消息队列 就构成事件循环了。
    </script>
  </body>
</html>

任务有优先级吗?

  • 没有, 放在消息队列里面 先进先出

但是消息队列是有优先级的

w3c的规定:html.spec.whatwg.org/multipage/w...

这里有更加细致的解读 github.com/fi3ework/bl...

规定了哪些是属于任务

  • 同一个类型的任务放必须在同一个队列里面, 不同类型的任务放在不同的队列里面 ,在一次事件循环的过程中,浏览器可以根据实际情况选择从不同的对立里面取出任务执行。
  • 浏览器必须准备好一个微队列, 微队列里中国呢的任务优先其他任务执行。

任务类型可以有很多种 ,源码里面定义了

每一个字段代表一个任务类型

用户交互相关的任务类型: KUserInteraction

网络相关的任务类型: KNetworking

随着浏览器复杂度急剧提升, W3C 不再使用宏队列的说法

在目前的chrome的实现中, 至少包含了下面的队列:

  • 延时队列: 用于存放计时器到达后的回掉任务, 优先级【中】
  • 交互队列: 用户存放用户操作后产生的事件处理任务,优先级【高】
  • 微队列: 用户存放需要最快执行的任务,优先级【最高】

可以看到 promise mutationObserver postMessage 都是属于microtask 放在微队列里面, 微队列在主线程 调用栈 执行完当前同步任务的时候, 优先查看微队列是否为空, 不为空就清空微队列, 如果时间还够就去其他队列里面取任务执行。

怎么把一个函数添加到微队列?

  • 添加到微队列是因为前面说的优先级最高 相比 延时队列和交互队列
js 复制代码
Promise.resolve().then(函数)

执行js代码的时候, 就是任务在下面图里面添加 清空的过程

说一下js的事件循环

事件循环又叫消息循环, 是浏览器渲染主线程的工作方式。 在chrome 的源码中,它开启一个不会结束的for循环,每次从消息队列里面取出第一个任务执行,而其他线程只需要在合适的时候任务添加到毒烈末尾即可

过去把消息队列简单分为宏队列和微队列,这种说法无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。

根据W3C官方的解释, 每一个任务有不同的类型,同类型必须放一个队列里面, 不同的任务可以属于不同的队列,任务没有优先级,不同的任务队列有不同的优先级,微队列优先级最高,其次是交互队列,然后是延时队列, 浏览器自行决定取哪一个队列的任务,但浏览器必须有一个微队列,微队列的任务一定有最高的优先级, 必须优先调度执行。

js计时器能精确计时吗?

  • 不能 ,
    1. 操作系统实现的时候,有少量偏差 ,widows 和mac实现不一样
    1. 按照w3c的标准, 浏览器实现计时器的时候, 如果嵌套层级超过5层, 则会有4毫秒的最少时间

总结

  • 想了解浏览器的事件循环要了解很多前置知识, 比如浏览器,操作系统进程,线程, 浏览器有几个进程线程
  • 什么是同步异步
  • 同步阻塞可以用异步解决
  • 异步解决需要有缓存空间,就引入消息队列
  • 消息队列可以不只有一个, 微队列被主线程取出的优先级最高
  • 代码从上到下执行
  • 主线程里面的代码在调用栈里面执行, 栈为空的时候, 优先去微队列里面取任务执行,直到微队列为空
  • promise.then 里面的回调函数会被放到微队列
  • vue nexttick里面实现就是使用微队列优先级最高的特性
  • 浏览器渲染页面大概每秒钟画60次
  • js执行和渲染都是在浏览器主线程里面执行的,是互斥的, 所以js执行时间不能太久
  • react里控制js执行太久,使用了fiber架构解决。
  • settimeout 和交互 交互优先执行, 为了更快响应用户交互
  • settimeout delay参数指定为0 , 也不一定立即执行,要看主线程是否被清空, 以及其他队列(微队列 交互队列是否为空)才能被取出执行
  • 同步异步,不熟悉可以画出 图, 主线程 微队列 交互队列 延时队列 其他线程 等模型, 然后自己模拟执行顺序。
相关推荐
桂月二二33 分钟前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062062 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb2 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角2 小时前
CSS 颜色
前端·css
浪浪山小白兔3 小时前
HTML5 新表单属性详解
前端·html·html5
lee5763 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579653 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
limit for me4 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者4 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架
qq_392794484 小时前
前端缓存策略:强缓存与协商缓存深度剖析
前端·缓存