事件循环详解

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

进程和线程

什么是进程?

百科解释: 进程(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

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

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

相关推荐
安冬的码畜日常3 分钟前
【D3.js in Action 3 精译_027】3.4 让 D3 数据适应屏幕(下)—— D3 分段比例尺的用法
前端·javascript·信息可视化·数据可视化·d3.js·d3比例尺·分段比例尺
l1x1n031 分钟前
No.3 笔记 | Web安全基础:Web1.0 - 3.0 发展史
前端·http·html
昨天;明天。今天。1 小时前
案例-任务清单
前端·javascript·css
zqx_72 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己2 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称3 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色3 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2343 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河3 小时前
CSS总结
前端·css
BigYe程普4 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发