事件循环详解

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

进程和线程

什么是进程?

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

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

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

相关推荐
Json_1817901448017 分钟前
电商拍立淘按图搜索API接口系列,文档说明参考
前端·数据库
风尚云网40 分钟前
风尚云网前端学习:一个简易前端新手友好的HTML5页面布局与样式设计
前端·css·学习·html·html5·风尚云网
木子020443 分钟前
前端VUE项目启动方式
前端·javascript·vue.js
GISer_Jing1 小时前
React核心功能详解(一)
前端·react.js·前端框架
捂月1 小时前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
深度混淆1 小时前
实用功能,觊觎(Edge)浏览器的内置截(长)图功能
前端·edge
Smartdaili China1 小时前
如何在 Microsoft Edge 中设置代理: 快速而简单的方法
前端·爬虫·安全·microsoft·edge·社交·动态住宅代理
秦老师Q1 小时前
「Chromeg谷歌浏览器/Edge浏览器」篡改猴Tempermongkey插件的安装与使用
前端·chrome·edge
滴水可藏海1 小时前
Chrome离线安装包下载
前端·chrome
m51271 小时前
LinuxC语言
java·服务器·前端