目录
事件循环:引入
一、浏览器的进程模型
1.1、什么是进程(Process)
1.2、什么是线程(Thread)
1.3、进程与线程之间的关系联系与区别
二、浏览器有哪些进程和线程
2.1、浏览器的主要进程
①浏览器进程
②网络进程
③渲染进程
2.2、渲染主线程的工作原理
渲染主线程的消息队列
三、事件循环
3.1、什么是事件循环(Event Loop)
3.2、渲染主线程的事件循环如何确定任务的优先级?
3.3、事件循环的执行示例
示例一
示例二
测试题:如下代码块执行后输出顺序是什么?
四、相关问题
4.1、为什么要使用事件循环
4.2、如何理解JS的异步
4.3、JS中计时器能精确计时吗,为什么?
五、总结与相关资源
事件循环(消息循环):引入
事件循环是浏览器的核心内容。
与计时器、Promise、ajax、node等技术有关。
要想说清楚事件循环,必须先聊进程与线程。
一、浏览器的进程模型
1.1、什么是进程(Process)
我们先看看定义:
- 进程是程序的执行实例。它是操作系统进行资源分配和调度的一个独立单位。
- 进程拥有独立的内存空间,可以拥有或分配不同的资源如CPU时间、文件、消息队列等。
- 进程可以创建子进程,形成进程树结构。
对于coder来说,说到实例肯定不陌生,一个程序的运行就至少需要产生一个实例,实例负责给程勋运行提供运行所需的资源。
简单的说,程序运行需要它专属的内存空间(RAM和虚拟内存),这部分内存空间可以简单的理解为该程序对应的进程。
每个应用至少有一个进程,且相互独立,即使要通信,也要双方同意。
1.2、什么是线程(Thread)
先看定义:
- 线程是程序执行的逻辑单元,是程序中一个单一的顺序控制流程。
- 在一个进程中可以包含多个线程,它们共享进程的资源,如内存空间,但每个线程有自己的线程栈和程序计数器。
简单的说,线程是进程的执行者。一个进程可以有多个线程,线程之间资源共享, 通信简单,独立执行,开销较小。
一个进程至少有一个线程,所以进程开启后就会自动创建一个线程来运行代码,该线程称之为主线程。
如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程。重要的事情要多次重复,这些线程资源共享, 通信简单,独立执行,开销较小(线程相比于进程)!
1.3、进程与线程之间的关系联系与区别
综上所述,二者之间的联系与区别就很明确了:
- 进程是程序某一部分或整体的运行实例,每个程序运行都至少需要一个进程。(但不一定只有一个,为了保证程序的稳定性,往往会有多个进程,一个进程崩溃不会导致整个程序崩溃)
- 线程是进程的执行者,每个进程都至少包含一个线程(即主线程)。
- 线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中。因此同一进程内的线程可以共享进程的资源,如全局变量、文件句柄等。
- 与创建新进程相比,创建线程的开销较小,因为线程可以复用进程的资源。
- 由于线程共享同一地址空间,线程间的通信更简单,不需要复杂的进程间通信机制。
关系示意图:
二、浏览器有哪些进程和线程
首先,浏览器是一个多进程多线程的应用程序。
2.1、浏览器的主要进程
浏览器内部工作极其复杂,为了避免相互影响,为了减少连环崩溃的几率,当启动浏览器后,它会自动启动多个进程。一般主要有浏览器进程、网络进程、渲染进程。
可以在浏览器的任务管理器中查看当前的所有进程。
①浏览器进程
主要负责界⾯显示、⽤户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。
②网络进程
负责加载网络资源。网络进程内部会启动多个线程来处理不同的⽹络任务。
③渲染进程
渲染进程启动后,会开启⼀个渲染主线程,主线程负责执行 HTML、CSS、JS 代码。
默认情况下,浏览器会为每个标签页开启⼀个新的渲染进程,以保证不同的标签页之间不相互影响。
2.2、渲染主线程的工作原理
渲染主线程是浏览器中最繁忙的线程,也是前端开发中提高运行效率需要着重关注的线程,需要它处理的任务包括不限于:解析HTML、解析CSS、计算样式、布局、处理图层、每秒60次渲染,执行全局JS代码、执行事件处理函数、执行计时器回调函数等。
那渲染主线程如何执行和调度这些任务呢?总要有个章法去有序执行这些步骤,同时兼顾这些步骤的因果顺序和中途插入的步骤。
比如任务之间存在因果顺序:不解析HTML、CSS,就没办法执行布局任务。
又比如任务之间会有插入情况:执行JS函数的过程中,用户点击了某个按钮或者计时器到了时间需要执行回调函数。
这里就引入了一个概念:
渲染主线程的消息队列
- 在最开始的时候,渲染主线程会进入一个无限循环
- 每一次循环会检查消息队列中是否有任务存在。如果有,就取出第一个任务执行,执行完一个后进入下一次循环;如果没有,则进入休眠状态。
- 其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务
简而言之,渲染主线程的消息队列就是渲染主线程的任务管家,负责给渲染主线程要执行的任务进行排序、管理、调度。渲染主线程只需要一直检查消息队列里面有没有任务,按序执行即可,但消息队列要考虑的可就多了(bushi)
这样一来,就可以让每个任务有条不紊的、持续的进行下去了。现在就能引出本文的核心内容:事件循环。
三、事件循环
3.1、什么是事件循环(Event Loop)
又称消息循环(Message Loop),在有些情景也叫 Run Loop。
一言以蔽之:事件循环就是渲染主线程不断循环不断从消息队列中读取事件并执行的过程。
也可以说:事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。(但并非只有浏览器的渲染主线程会进行事件循环,有时候网络线程也会)
不是所有的线程都有事件循环,但是渲染主线程一般都有。
3.2、渲染主线程的事件循环如何确定任务的优先级?
首先,任务本身没有优先级,消息队列遵守先进先出的规则。
但是消息队列有优先级。消息队列一般至少由三个队列:微队列、交互队列、延时队列构成,其分类和优先级规则如下:
微队列 > 交互队列 > 延时队列
- 微队列:用户存放需要最快执行的任务(一般由Promise和MutationObserver生成),优先级「最高」
- 交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
- 延时队列:用于存放计时器到达后的回调任务,优先级「中」
这里的优先级是指事件循环过程中,高优先级的队列会“插队”放入队列。比如现在队列中微队列和延时队列各有一个事件,先读取微队列中的任务,执行后又产生了一个微队列任务和一个交互队列任务,那么下一个执行的是新产生的微队列任务,然后是新产生的交互队列任务,最后才是一开始的延时队列任务。
如下图所示,消息队列大概是个这样的模型,只有微队列完全空掉才会执行交互队列中的任务,在同一类型的队列中才严格遵守“先进先出”的队列规则:
3.3、事件循环的执行示例
请问如下几个例子的输出顺序是什么?
示例一
setTimeout(function () {
console.log(1);
}, 0);
function delay(duration) {
var start = Date.now();
while (Date.now() - start < duration) {}
}
delay(3000);
console.log(2);
// 输出顺序为 2 、 1
(点击代码详情查看答案)
解析:整体先作为一个任务①顺序执行。setTimeout生成一个新任务②,放到延时队列中(虽然计时为0,但是任务①还没执行完毕,所以哪怕计时到了也只能在队列等候执行)。delay函数将渲染主线程阻塞3秒,然后输出2,任务①执行完毕,通过事件循环执行任务②,输出1。
示例二
function a() {
console.log(1);
Promise.resolve().then(function () {
console.log(2);
});
}
setTimeout(function () {
console.log(3);
}, 0);
Promise.resolve().then(a);
console.log(5);
// 输出顺序为: 5 、 1 、 2 、 3
(点击代码详情查看答案)
解析:整体作为任务①执行。setTimeout生成一个新任务②,放到延时队列中。Promise生成一个新任务③(执行a函数),放到微队列中。然后输出5,任务①执行完毕。
此时消息队列中微队列有任务③,优先执行,先输出1,然后Promise生成一个新任务④,放到微队列中,任务③执行完毕。
此时微队列又有任务④,优先执行,输出2。任务④执行完毕。
此时消息队列中微队列和交互队列为空,执行延时队列中的任务②,输出3,任务②执行完毕。
即输出结果为:5 1 2 3。
测试题:如下代码块执行后输出顺序是什么?
function a() {
console.log(1);
Promise.resolve().then(function () {
console.log(2);
});
}
setTimeout(function () {
console.log(3);
Promise.resolve().then(a)
}, 0);
Promise.resolve().then(function(){
console.log(4);
});
console.log(5);
// 输出顺序为: 5 、 4 、 3 、 1 、 2
四、相关问题
4.1、为什么要使用事件循环
在本文2.2中提到“让每个任务有条不紊的、持续的进行下去”。那么为什么不使用事件循环就会出现问题?为什么“执行JS函数的过程中,用户点击了某个按钮或者计时器到了时间需要执行回调函数”就会有矛盾?这两个任务又没有因果关系,直接一起执行不行吗?
事实上,JS是一门单线程语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。也就是说,JS函数不能多个一起进行,哪怕两个任务相互独立,也要有个规定来调度任务,有序执行。所以必须要有一个像事件循环一样的逻辑来管理、调度任务。
4.2、如何理解JS的异步
代码在执行过程中,会遇到一些无法立即处理的任务,比如:
- 计时完成后需要执行的任务 —— setTimeout、setInterval
- 网络通信完成后需要执行的任务 —— XHR、Fetch
- 用户操作后需要执行的任务 —— addEventListener
如果让渲染主线程等待这些任务的时机达到,就会导致渲染主线程长期处于“阻塞”的状态,从而让用户感觉浏览器“卡死”,让用户的体验变差。
因此,浏览器使用异步来解决这个问题。
具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。
从而最大程度的保证单线程的流畅运行。
4.3、JS中计时器能精确计时吗,为什么?
不可以。原因如下:
从硬件角度来说:JS计时器是调用了操作系统中的计时函数,该函数本身就有少量偏差,硬件精度有限。
从语法标准上说:W3C标准中建议浏览器的计时器嵌套层级超过5层,则存在至少4ms的最少事件,这样也会带来偏差。
// 例如嵌套的层数小于等于5层,那么就会按照设置的时间执行。
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
// 假如嵌套的层数大于5层,即使设置了0毫秒的间隔,浏览器也会确保至少有4毫秒的延迟,以避免潜在的性能问题,即:
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
// 实际执行效果:
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {
setTimeout(function () {}, 4);
}, 4);
}, 4);
}, 4);
}, 4);
}, 4);
从事件循环的逻辑上讲,计时器的回调函数只能在主线程空闲时进行,并不一定能在计时完成后立马开始执行逻辑。
综上所述,JS中计时器做不到精确计时。
五、总结与相关资源
度一教育的袁进老师谈到他的理解:单线程是异步产生的原因,事件循环是异步的实现方式。
本质是因为渲染进程因为计算机图形学的限制,只能是单线程。所以需要“异步”这个技术思想来解决页面阻塞的问题,而“事件循环”是实现“异步”这个技术思想的最主要的技术手段。
但事件循环并不是全部的技术手段,比如Promise,虽然受事件循环管理,但是如果没有事件循环,单一Promise依然能实现异步不是吗?
博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
更多优质内容,请关注:
JS底层逻辑:
路由通配符,小小的字符有大大的作用,你真的熟悉吗?
管理数据必备!侦听器watch用法详解
什么是深拷贝?深拷贝和浅拷贝有什么区别
JS语法篇:
你真的会使用Vue3的onMounted钩子函数吗?Vue3中onMounted的用法详解
对象数据的读取,看这一篇就够了!
通过array.every()实现数据验证、权限检查和一致性检查,array.some与array.every的区别
通过array.some()实现权限检查、表单验证、库存管理、内容审查和数据处理
通过array.map()实现数据转换、创建派生数组、异步数据流处理、搜索和过滤等需求
通过array.reduce()实现数据汇总、条件筛选和映射、对象属性的扁平化、转换数据格式等
通过array.filter()实现数组的数据筛选、数据清洗和链式调用
巧妙算法与窍门:
多维数组操作,不要再用遍历循环foreach了,来试试数组展平的小妙招!
别再用双层遍历循环来做新旧数组对比,寻找新增元素了!
shpfile转GeoJSON且控制转化精度;如何获取GeoJSON?GeoJson结构详解
Mapbox添加行政区矢量图层、分级设色图层、自定义鼠标悬浮框、添加天地图底图等
Element plus拓展:
通过el-tree自定义渲染网页版工作目录,实现鼠标悬浮显示完整名称等
el-table实现动态数据的实时排序,一篇文章讲清楚elementui的表格排序功能
el-table中如何添加渐变色带、多色色带