我认为要理解事件循环,首先要理解什么是进程,什么是线程。
进程和线程
什么是进程?
百科解释: 进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。
说实话这种解释我是看不懂。一番查阅后,找了一个很不错的介绍:
程序运行需要有它自己专属的内存空间,可以把这块内存空间简单的理解为进程
"专属的内存空间"就可以理解为独立。
可以通过浏览器f12,查看任务管理器来看下开启了哪些进程
每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。
什么是线程?
百科解释: 线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程是最小的执行单元,而进程由至少一个线程组成。启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。说的在透彻一点,运行代码的「人」称之为「线程」。
一个进程可以包含多个线程,但是一个进程至少要有一个线程,所以在进程开启后会自动创建一个线程来运行代码,该线程称之为主线程。
浏览器包含哪些进程和线程
- 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能
- 渲染进程 。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
- 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
我们这里主要来看渲染进程,渲染进程启动后,会开启一个渲染主线程,主线程主要负责执行HTML、CSS、JS代码。
渲染主线程
每个渲染进程都有一个主线程,并且主线程非常繁忙,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。那么主线程如何调度任务呢?
排队
将这些任务按照顺序加入消息队列中,并且按照"先进先出"一个个执行。
我们来分析一下执行步骤:
- 渲染主线程会进入到一个无线循环中
- 如果添加一个任务,那么则将添加的任务添加到消息队列的末尾
- 每一次循环都会检查消息队列中是否有任务存在,如果存在,那么渲染主线程会从消息队列的头部中读取任务,并执行任务,执行完毕后,则进入下一次循环;如果不存在,则主线程进入休眠状态。
这里添加任务并不是只有渲染线程的任务,其他所有线程(包括其他进程中的线程)可以随时向消息队列中添加任务。在添加新任务的时候,如果主线程是处于休眠状态,那么则将主线程唤醒以继续循环读取并执行任务。
异步
我们知道,所有的任务都是在单线程执行的,而且每次只能执行一个任务,那么其他的任务就都处于一个等待的状态。而有些任务在代码执行的过程中,是无法立即处理的,那么这样就会导致一个很严重的问题。如果浏览器主线程等待这些无法立即处理的任务的时机达到,就会导致主线程长期处于一个等待的状态,也就是我们常说的"阻塞",就会导致浏览器卡死。
从图中可以看到,当计时开始的时候,渲染主线程一直处于等待的状态。
由于渲染主线程承担着极其重要的工作,所以绝对不能阻塞,浏览器选择了异步来解决这个问题
使用异步,当计时开始的时候,渲染主线程将任务交给计时线程去处理,随后自身立即结束任务的执行,转而执行后续代码,当计时线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。
在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。
优先级
消息队列是"先进先出"的特性,那么放入队列中的任务,要等待前面的任务执行完毕之后,才能被执行。但是如果有的任务比较着急执行呢?比如我们常说的微任务比宏任务先执行。这个是因为任务有优先级吗?
其实并不是,任务没有优先级,任务就是需要排队,在队列中先进先出,但是消息队列有优先级。
根据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代码
- 当碰到
promise.resolve.then(fn)
,立即把一个函数添加到微队列, 这里我们称为Promise1任务 - 遇到setTimeout,然后交给计时线程计时,
- 在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
你明白了吗?欢迎评论区留言
参考:极客时间------《浏览器工作原理与实践》