# JavaScript 中的 Event Loop 事件循环、微任务与宏任务
TIP
本节开始学习 JavaScript 中的 Event Loop 事件循环、微任务与宏任务
# 一、单线程的 JavaScript
TIP
我们都知道 JS 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那 JS 为什么要设置成单线程的呢 ?为什么不设置成多线程呢 ?
- JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,JS 的主要用途是与用户交互,以及操作 DOM。这就决定了他只能是单线程的,否则会带来很多复杂的同步问题。
- 假设 JS 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准 ?
所以,为了避免复杂性,从一从一诞生,JavaScript 就是单线程,浏览器中的 JS 执行和 DOM 渲染共用一个线程。
# 二、同步任务与异步任务
TIP
JS 是单线程的,那就意味着所有的任务需要排队,前一个任务结束,才能执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
如果因为计算量大,CPU 忙不过来,倒也算了,但是很多时候 CPU 是闲着的,因为 IO 设备(输入输出设备)很慢(比如受网络的影响,外部请求加载一张图片等会很慢),就不得不等着结果出来,再往下执行。这样就造成了 CPU 资源的浪费,因为 CPU 是闲着的,但后面还有很多任务要做又不能做,这样代码的执行效率就变得很低了,因为某个任务过长,就会造成主线程阻塞。
JS 的语言设计者也意识到了,这时主线程完全可以不管 IO 设备,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 设备返回了结果,再回过头,把挂起的任务继续执行下去。
于是,JS 中把任务分成两种:同步任务(synchronous)和异步任务(asynchronous)
- 同步任务:是指在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
- 异步任务:是指不进入主线程,而进入“任务队列”(task queue)的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行
JS 中那些任务是属于同步任务,那些属性异步任务呢 ?
同步任务 | 异步任务 |
---|---|
大部分代码都是同步任务 | 事件、setTimeout、setInterval、requestAnimationFrame、Ajax、fetch、MutationObserver、Promise 的 then 和 catch 方法、async 函数 |
你可能有这样的疑问:
JS 是单线程的,那他的异步任务是谁来负责的,如何被加入到任务队列,这就需要了解浏览器的进程与线程。
# 三、浏览器进程与线程
TIP
首先我们要知道 JS 是单线程的,所谓的单线程是指用来执行 JS 代码的线程只有一个。
但浏览器是多线程的,所以 JS 执行时遇到异步任务,如是 http 请求,这些请求是由浏览器的相关线程来完成的,等请求有结果时,再把需要 JS 线程来执行的任务(通常以回调函数的形式)加入到任务队列等着 JS 主线程空闲时来执行。
浏览器是多进程和多线程的,那进程和线程是什么关系呢 ?
分类 | 说明 |
---|---|
进程 | 是操作系统资源分配的最小单元。一个进程拥有的资源有⾃⼰的堆、栈、虚存空间(页表)、文件描述符等信息(可以把他理解为一个独立运行的程序) |
线程 | 是操作系统能够进行运算调度的最小单元。它被包含在进程中,是进程中实际运行的单位。一个进程中可以并发多个线程,每个线程执行不同的任务 |
生活类比 - 解释进程与线程
如果把进程看作一个部门,一个部门都需要完成指定的任务,那就会为完成这些任务配套相关的资源。
那线程就相当于这个部门的人,他们共享这个部门的资源,然后每个人又有自己不同的事情要做,多个人(多个线程)之间相互配合,然后一起把这个任务完成。
温馨提示:
- 线程共享进程资源,包括内存空间和操作系统的权限
- 进程中的任意一个线程执行出错,都会导致整个进程的崩溃
- 进程和进程之间也是可以互相通信,就好比部门和部门之间也是可以互相通信的一样
仅仅打开一个网页,就需要具有以下 4 个进程
进程 | 功能 |
---|---|
浏览器进程 | 主要负责界面显示、用户交互、子进程管理、同时提供存储等功能 |
渲染进程 | 核心任务是将 HTML、CSS 和 JS 转换为用户可以与之交互的网页,提成版引擎 Blink 和 JS 的 V8 引擎都是在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。 |
GPU 进程 | 负责整个浏览器界面的渲染,早期主要是为了实现 3D CSS 效果 |
网络进程 | 主要负责页面的网络资源加载 |
渲染进程中的线程 | 功能 |
---|---|
JS 引擎线程 | JS 引擎线程也称为 JS 内核,负责处理 Javascript 脚本程序,解析 Javascript 脚本,运行代码; JS 引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个 Tab 页中无论什么时候都只有一个 JS 引擎线程在运行 JS 程序 |
HTTP 请求线程 | XMLHttpRequest 连接后通过浏览器新开一个线程请求;检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待 JS 引擎空闲后执行 |
定时器触发线程 | 主要负责负 setTimeout,setInterval 定时器计时的,计时完毕后,将事件添加到处理队列的队尾,等待 JS 引擎空闲去处理 |
事件触发线程 | 用来控制事件循环,当 JS 引擎执行到点击事件,异步事件等等,都会将对应的任务添加到事件线程中,当事件符合触发条件时,会将事件添加到处理队列的队尾,等待 JS 引擎空闲后去执行(主要负责将准备好的事件交给 JS 引擎去执行) |
GUI 线程 | 负责渲染浏览器页面,解析 HTML、CSS、构建 DOM 树,构建 CSSOM 树,构造渲染树和绘制页面。当界面需要重绘或某种操作引发回流时,该线程就会执行 不过要特别注意: GUI 线程和 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到 JS 引擎空闲时立即被执行 |
# 四、同步与异步执行顺序
TIP
首先 JS 会将所有同步任务执行完再去执行异步任务,如果在执行同步任务的过程中遇到了异步任务,会先把他放到 “任务队列” 中等着,等同步的代码全部执行完,再到任务队列取出异步任务,进入主线程并执行。
异步任务的执行顺序是先加入队列的先拿出来执行
console.log(1);
console.log(2);
setTimeout(function () {
console.log("定时器1000");
}, 1000);
console.log(3);
setTimeout(function () {
console.log("定时器0");
}, 0);
console.log(4);
......
// 最后执行结果 1,2,3,4 定时器0 定时器1000
# 五、宏任务与微任务
TIP
JS 中的任务分为同步与异步,其中异步任务又分为两种:
- 宏任务(Macro-take)
- 微任务(Micro-take)
宏任务 | 微任务 |
---|---|
script 标签(JS 整体代码)、setTimeout、setInterval、Ajax、DOM 事件 等 | Promise 的 then 和 catch 方法、MutaionObserver、async/await 等 |
任务队列的执行过程:
- 1、刚开始,调用栈空。微任务队列空,宏任务队列里有且只有一个 Script 脚本(整体 JS 代码)。这时首先执行的就是这个宏任务。(所以一开始程序执行时是没有微任务的)
- 2、整体代码作为宏任务进入调用栈,先执行同步代码,在执行的过程中遇到宏任务或微任务,就将他们加入分别加入到宏任务队列或微任务队列。
- 3、上一步的同步代码执行完后出栈,接着从微任务队列中取出微任务(先添加到微任务队列的先执行)并执行,在执行微任务过程中产生新的微任务,会添加到微任务队列,等微任务中的任务全部完成后,并不会马上执行宏任务,而是会进行 DOM 渲染
- 4、开始 DOM 渲染,把内容呈现在页面中,DOM 渲染结束。
- 5、接着从宏任务队列中取出宏任务(先加入到宏任务队列的先执行),并压入栈中执行。在执行宏任务时,也可能会产生新的宏任务和微任务。其执行过程重复上面操作。
以上不断重复的过程就叫做 Event Loop(事件循环)
注意事项:
微任务是在下一轮 DOM 渲染之前执行,宏任务是在这之后执行。也就是说微任务与宏任务之间隔着一个 DOM 渲染。
所谓 DOM 渲染是指把内容绘制到页面上。
接下来我们通过相关的代码来分析整个代码执行的顺序
# 1、代码分析 一
<div>正文内容</div>
<script>
console.log("同步开始---");
const div = document.createElement("div");
div.innerHTML = "新加的内容";
document.body.appendChild(div);
const list = document.querySelectorAll("div");
console.log("div的个数----", list.length);
setTimeout(() => {
console.log("timeout中代码");
alert("阻塞 timeout");
});
console.log("同步进行中----");
Promise.resolve().then(() => {
console.log("Promise的then方法中代码");
alert("阻塞 Promise");
});
console.log("同步结束----");
</script>
# 2、代码分析 二
console.log("同步1");
setTimeout(function fn1() {
console.log("定时器为宏任务");
}, 0);
new Promise((resolve, reject) => {
console.log("同步2");
resolve("a");
})
.then(function fn2() {
console.log("then方法为微任务1");
})
.then(function fn3() {
console.log("then方法为微任务2");
});
console.log("同步3");
分析上面代码的执行步骤:
1、宏任务:执行整个代码(<script>
标签中的代码)
- (1):先执行同步任务
console.log("同步1")
,输出"同步1"
- (2):遇到 setTimeout,加入到宏任务队列
- (3):遇到 Promise 的构造函数,属于同步任务,输出
"同步2"
- (4):遇到 Promise 的 then 方法,加入微任务队列(1 个 then,加入微任务队列)
- (5):接着执行后面的同步代码
console.log("同步3");
输出 "同步 3"
2、微任务:执行微任务对列(promise 的 then 方法中的回调)
- (1): 从微任务队列中取出第一个任务(第一个 then 的回调)执行,输出:
"then方法为微任务1"
这个 then 方法执行后又产生了一个微任务,加入到了微任务队列。 - (2):从微任务队列中取出刚加的微任务,并执行,输出
"then方法为微任务2"
3、执行渲染操作,更新界面
4、宏任务:取出宏任务队列中的任务(setTimeout 的回调函数 fn1
)并执行,最后输出 "定时器为宏任务"
# 3、代码分析 三
setTimeout(() => {
console.log("ok");
});
new Promise((resolve, reject) => {
console.log(1);
resolve();
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(4);
})
.then(() => {
console.log(5);
});
new Promise((resolve, reject) => {
console.log(10);
resolve();
})
.then(() => {
console.log(20);
})
.then(() => {
console.log(30);
})
.then(() => {
console.log(40);
})
.then(() => {
console.log(50);
});
// 执行结果:1 10 2 20 3 30 4 40 5 50 ok
分析上面代码的执行步骤:
宏任务:执行整个代码(<script>
标签中的代码)
- (1):遇到异步的宏任务,添加到宏任务队列。然后接着向下执行代码
- (2):遇到同步任务,
new Promise(...)
,则打印 1 ,然后 Promise 的状态改变,向微任务队列中添加第 1 个微任务() => { console.log(2); }
- (3):接着遇到同步任务,
new Promise(...)
,则打印 10,然后然后 Promise 的状态改变,向微任务队列中添加第 2 个微任务() => { console.log(20);}
- (4):同步任务执行完,开始从微任务队列中取出第 1 个微任务执行,打印 2,然后返回成功的 Promise 对象,向微任务队列中添加第 3 个微任务
() => { console.log(3);}
,出栈。 - (5):接着从微任务队列中取出第 2 个微任务执行,打印 20,然后然后返回成功的 Promise 对象,向微任务队列中添加第 4 个微任务
() => { console.log(30);}
,出栈。 - (6):接下来重复上面的步骤 3 和 4,不断取出对应的微任务执行,在执行的过程中又产生新的微任务。等所有微任务全部执行完,最后去宏任务队列取出宏任务,并执行,所以最后输出
“ok"
# 4、代码分析 四
这是一道经典的面试题,熟称让人失眠的一道面试题
const p2 = Promise.resolve()
.then(() => {
console.log(0);
// 慢两拍
return Promise.resolve(4);
})
.then((data) => {
console.log(data);
});
Promise.resolve()
.then(() => {
console.log(1);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(5);
})
.then(() => {
console.log(6);
});
大厂最新技术学习分享群
微信扫一扫进群,获取资料
X