事件循环详解

我认为要理解事件循环,首先要理解什么是进程,什么是线程。

进程和线程

什么是进程?

百科解释: 进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。

说实话这种解释我是看不懂。一番查阅后,找了一个很不错的介绍:

程序运行需要有它自己专属的内存空间,可以把这块内存空间简单的理解为进程

"专属的内存空间"就可以理解为独立。

可以通过浏览器f12,查看任务管理器来看下开启了哪些进程

每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。

什么是线程?

百科解释: 线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

线程是最小的执行单元,而进程由至少一个线程组成。启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。说的在透彻一点,运行代码的「人」称之为「线程」。

一个进程可以包含多个线程,但是一个进程至少要有一个线程,所以在进程开启后会自动创建一个线程来运行代码,该线程称之为主线程。

浏览器包含哪些进程和线程

  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能
  • 渲染进程 。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
  • 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

我们这里主要来看渲染进程,渲染进程启动后,会开启一个渲染主线程,主线程主要负责执行HTML、CSS、JS代码。

渲染主线程

每个渲染进程都有一个主线程,并且主线程非常繁忙,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。那么主线程如何调度任务呢?

排队

将这些任务按照顺序加入消息队列中,并且按照"先进先出"一个个执行。

我们来分析一下执行步骤:

  1. 渲染主线程会进入到一个无线循环中
  2. 如果添加一个任务,那么则将添加的任务添加到消息队列的末尾
  3. 每一次循环都会检查消息队列中是否有任务存在,如果存在,那么渲染主线程会从消息队列的头部中读取任务,并执行任务,执行完毕后,则进入下一次循环;如果不存在,则主线程进入休眠状态。

这里添加任务并不是只有渲染线程的任务,其他所有线程(包括其他进程中的线程)可以随时向消息队列中添加任务。在添加新任务的时候,如果主线程是处于休眠状态,那么则将主线程唤醒以继续循环读取并执行任务。

异步

我们知道,所有的任务都是在单线程执行的,而且每次只能执行一个任务,那么其他的任务就都处于一个等待的状态。而有些任务在代码执行的过程中,是无法立即处理的,那么这样就会导致一个很严重的问题。如果浏览器主线程等待这些无法立即处理的任务的时机达到,就会导致主线程长期处于一个等待的状态,也就是我们常说的"阻塞",就会导致浏览器卡死。

从图中可以看到,当计时开始的时候,渲染主线程一直处于等待的状态。

由于渲染主线程承担着极其重要的工作,所以绝对不能阻塞,浏览器选择了异步来解决这个问题

使用异步,当计时开始的时候,渲染主线程将任务交给计时线程去处理,随后自身立即结束任务的执行,转而执行后续代码,当计时线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。

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

优先级

消息队列是"先进先出"的特性,那么放入队列中的任务,要等待前面的任务执行完毕之后,才能被执行。但是如果有的任务比较着急执行呢?比如我们常说的微任务比宏任务先执行。这个是因为任务有优先级吗?

其实并不是,任务没有优先级,任务就是需要排队,在队列中先进先出,但是消息队列有优先级。

根据w3c最新解释 html.spec.whatwg.org/multipage/w...

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

和我们关系比较大的,在chrome浏览器中,至少要包含以下队列:

  • 延时队列 --------- 优先级(中)
  • 交互队列 --------- 优先级(高)
  • 微队列 --------- 优先级(最高)

现代浏览器中,产生微任务有两种方式。

  • 第一种方式是使用 MutationObserver 监控某个 DOM 节点,然后再通过 JavaScript 来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
  • 第二种方式是使用 Promise,当调用 Promise.resolve() 或者 Promise.reject() 的时候,也会产生微任务。

下面我们用一个题目来演示一下

js 复制代码
Promise.resolve().then(() => {
  console.log('Promise1');
  setTimeout(() => {
    console.log('setTimeout2');
  }, 0);
});
setTimeout(() => {
  console.log('setTimeout1');
  Promise.resolve().then(() => {
    console.log('Promise2');
  });
}, 0);

碰到这种题目,首先立刻画个图

首先渲染主线程开始执行js代码

  1. 当碰到promise.resolve.then(fn),立即把一个函数添加到微队列, 这里我们称为Promise1任务
  2. 遇到setTimeout,然后交给计时线程计时,
  3. 在0s之后 将回调添加到延时队列,这里我们称为setTimeout1任务 此时全局js同步代码执行完毕。

而微队列优先级最高,则先执行Promise1任务,那么则输出Promise1,接着将遇到setTimeout,然后交给计时线程计时,在0s之后 将回调添加到延时队列, 这里我们称为setTimeout2任务

Promise1任务执行完毕之后,微队列中已经没有任务了,只有延时队列有任务,那么先执行setTimeout1任务。那么先输出setTimeout1 ,然后碰到promise.resolve.then(fn),立即把一个函数添加到微队列, 这里我们称为Promise2任务。

此时微队列中有任务了,由于他的优先级最高,那么先执行Promise2任务,则输出Promise2 ,Promise2任务执行完毕之后,微队列中已经没有任务了,只有延时队列有任务,那么先执行setTimeout2任务。那么先输出setTimeout2, 所以最后的结果为:Promise1、setTimeout1、Promise2、setTimeout2

你明白了吗?欢迎评论区留言

参考:极客时间------《浏览器工作原理与实践》

相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax