JavaScript异步(四)事件循环机制

本文最后更新于:8 个月前

什么是「JavaScript事件循环机制」?JavaScript中异步任务分为哪两种?事件循环机制对代码执行顺序如何影响?

JavaScript异步(四)事件循环机制

一、「事件循环机制」简介

JavaScript 是单线程的编程语言,但是在遇到异步事件的时候,js线程并没有阻塞,还会继续执行。依靠的就是JS的事件循环机制。同时它也是JS并发模型的基础。是用来协调各种事件、用户交互、脚本执行、UI 渲染、网络请求等的一种机制。

JavaScript在执行的时候有一个「执行上下文栈」,在代码执行过程中,通过将不同函数的执行上下文压入栈中来保障代码有序执行。在执行同步代码过程中,如遇到异步事件,JS引擎会将异步事件交给「WebAPI」去完成。当异步事件完成后,分配一个线程去通知主线程“刚才的异步任务完成了”(实质上是把回调函数放入CallbackQueue「任务队列」中)。当「执行上下文栈」空了时,就把CallbackQueue「任务队列」中的一个任务放入栈中,然后重复执行上述过程。

二、CallbackQueue任务队列

在JavaScript中,异步任务分为两种:宏任务、微任务。执行顺序:微任务->宏任务。

当「执行上下文栈」的事件执行完后,首先检查微任务队列是否有任务,若有就移入执行上下文栈执行。当微任务栈空了后,再检查宏任务队列是否有任务,如果有,就搬入一个任务到栈中执行,然后先检查此时的微任务栈并进行处理,然后再继续执行宏任务栈中剩余的任务。直到两个队列都为空时结束。

2.1、宏任务

宏任务包括:执行全局代码、解析HTML、创建主文档对象、更改当前URL、定时器事件、以及各种事件(页面加载、输入、网络加载)。从浏览器的角度来说,宏任务代表一个个独立的工作单元。

常见的被加入「宏任务队列」的操作:

  • 定时器事件:setTimeout()、setInterval()
  • DOM事件
  • UI渲染
  • AJAX,网络异步请求

2.2、微任务

微任务需要尽可能快地、通过异步方式执行,同时不能产生全新的微任务。微任务可以使得在重新渲染UI前执行指定的行为,避免不必要的重绘。

常见的被加入「微任务队列」的操作:

  • queueMircotask
  • MutationObserver(变异观察者)
  • Promise.resolve()

三、浏览器中的JS线程

3.1、进程 vs 线程

  • 进程:计算机中正在运行的程序。一个进程中可以有多个线程
  • 线程:操作系统运算调度的最小单位。每一个进程中都会至少创建一个线程用来执行程序中的代码(叫主线程)

计算机如果只有一个CPU核心,但是却要多个进程同时工作,该如何实现呢?

答案是将CPU计算时间进行「切片」,让不同进程的线程轮流获得「时间片」,这样就可以让用户感觉起来像是多个进程在同步工作。

3.2 、JS线程

JavaScript是单线程的,它的容器是「浏览器进程」或「Node进程」

浏览器进程中有一个单独运行JavaScript代码的线程,我们在此称其为「JS线程」。但是JS线程同一时间只能完成一个任务,如果遇上了非常耗时的操作,那么就会阻塞线程,使其它JS代码无法响应。

事实上,耗时的操作都不是由JS线程执行的。浏览器进程除了JS线程外,还有其它线程,如「渲染线程」、「定时触发器进程」等等

3.3、扩展:浏览器进程线程介绍

浏览器进程包括:

  • Browser进程,负责协调主控,只有一个。主要作用:(1)管理其它进程;(2)负责浏览器用户交互、如前进后退;(3)文件下载等等
  • GPU进程,只有一个,可禁用掉,用于3D绘制
  • 第三方插件进程
  • Renderer进程,通常所说的浏览器内核,一个标签页对应一个Renderer进程,包含我们经常接触到的「JS线程」、「GUI渲染线程」等。

Rederer进程(常说的浏览器内核)的线程包括:

  • GUI渲染线程,作用:解析HTML,构建DOM树、CSS树、Renderer树,计算布局和绘制
  • JS引擎线程,作用:执行JavaScript脚本,与GUI渲染线程互斥
  • 事件触发线程,作用:控制DOM事件循环,「事件队列」就在这个线程中
  • 定时器触发线程,setTimeout和setInterval所在的线程
  • 异步http请求线程

参考:[前端进阶] - 搞懂浏览器进程和线程 - 掘金 (juejin.cn)

四、事件循环实例分析

以下部分实例需要对Promise知识较为熟悉,建议先熟悉Promise基础之后再进行分析。

3.1、简单的宏任务和微任务

注意:new Promise(…)中的代码为同步代码,then()后面的代码才是异步代码(微任务)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
console.log("script start");

setTimeout(function () {
console.log("timeout1");
}, 10);

new Promise((resolve) => {
console.log("promise1");
resolve();
console.log('promise2')
setTimeout(() => console.log("timeout2"), 10);
}).then(function () {
console.log("then1");
}).then(function () {
console.log("then2");
});

console.log("script end");
//输出结果
//script start promise1 promise2 script end then1 then2 timeout1 timeout2

3.2、resolve()将微任务加入队列

**resolve()才是把微任务加入队列的操作,而不是then()**,then()方法只是用来声明onResolved、onRejected处理程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//resolve才是把then加入微任务队列的操作,仅仅声明then不是
new Promise((resolve)=>{
setTimeout(()=>{
console.log('2')
resolve(4)
console.log('3')
},0)
console.log('1')
}).then((data)=>{
console.log(data)
}).then(()=>{
console.log('5')
})
//输出结果
//1 2 3 4 5


//resolve是一个函数,只接受一个参数,(在resolve中传入函数并没有作用)
//它将参数包装进Promise.result,并返回这个Promise实例
Promise.resolve(()=>{
console.log('1')
setTimeout(()=>{console.log(2)},0)
console.log('3')
}).then(()=>{
console.log('4')
}).then(()=>{
console.log('5')
})
//输出结果
//4 5
//上面的代码并没有输出1,3,resolve()的正确用法如下:
Promise.resolve(1).then((data)=>{
console.log(data)
return 2
}).then((data)=>{
console.log(data)
})
//输出结果: 1 2

3.3、包含async和await的情况

async 和 await 其实就是 Generator 和 Promise 的语法糖。async 函数和普通 函数没有什么不同,他只是表示这个函数里有异步操作的方法,并返回一个 Promise 对象

1
2
3
4
5
6
7
8
9
10
11
//async表示该函数内部有异步操作,并(自动)返回一个Promise对象
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
// Promise 写法
async function async1() {
console.log("async1 start");
Promise.resolve(async2()).then(() => console.log("async1 end"));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end"); //这两行代码实际上等于:Promise.resolve(async2()).then(()=>{console.log('async1 end')})
}
async function async2() {
console.log("async2")
}
async1();
setTimeout(() => {
console.log("timeout");
}, 0);
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
//输出 async1 start async2 promise1 script end async1 end promise2 timeout

1、当前微任务:[],宏任务:[<script>]

  • 输出async1 start
  • 执行Promise.resolve(async2()),同时resolve()把then后面的处理程序加入微任务队列
  • 遇到timeout,加入宏队列
  • new Promise(executor)executor函数里面的代码属于同步代码,立即执行,所以输出promise1,执行resolve(),将then(promise2)加入微队列
  • 输出script end

2、当前微任务:[‘async1 end’, ‘promise2’],宏任务:[‘timeout’]

  • 输出async1 end
  • 输出promise2

3、当前微任务:[],宏任务:[timeout]

  • 输出timeout

3.4、Promise.reject的情况

如果没有在then中指定onReject处理程序,那么rejected的Promise实例会被Promise.resolve向后继承,直到在then参数中遇到onResolved处理程序,或者遇到catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Promise.reject(1).then(()=>{
console.log(2)
}).catch((data)=>{
console.log(data)
}).then(()=>{
console.log(3)
})
Promise.resolve().then(()=>{
console.log('a')
}).then(()=>{
console.log('b')
}).then(()=>{
console.log('c')
})
//输出结果
// a 1 b 3 c

3.5、复杂综合情况

如果then参数中处理函数返回的不是普通值,而是thenable或者Promise,就会被延迟到下一次微任务执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Promise.resolve()
.then(() => {
console.log(0)
return Promise.resolve(4)
})
.then(res => {
console.log(res)
})

Promise.resolve()
.then(() => {
console.log(1)
})
.then(() => {
console.log(2)
})
.then(() => {
console.log(3)
})
.then(() => {
console.log(5)
})
.then(() => {
console.log(6)
})
// 0 1 2 3 4 5 6
// return Promise.resolve(4)被推迟到下一次微任务循环执行
// Promise.resolve(4)又将一个空的处理程序加入了微任务,
// 所以原本的then()处理程序(即console.log(res))被推迟了两次
  1. 第一轮,Promise.resolve()各自把then后的处理程序加入微任务队列。执行完第一轮后的微任务队列前后有两个元素

    • A,{console.log(0); return Promise.resolve(4)}
    • B,{console.log(1)}
  2. 第二轮,执行代码A,输出0,接着return遇到Promise,被延迟到下一次微任务执行

    然后执行代码B,输出1,隐式调用了Promise.resolve()。此时微任务队列有两个个元素:

    • C,{return Promise.resolve(4)}

    • D,{console.log(2)}

  3. 第三轮,执行代码C,把C的then处理程序(实际上为空)加入微任务队列

    然后执行代码D,输出2,把D的then处理程序加入微任务队列

    • E,{}
    • F,{console.log(3)}
  4. 第四轮,执行代码E,此处内部是如何操作的,我暂时不明白,但是结果是把A的then处理程序加入了微任务队列

    然后,执行F,把F的then处理程序加入微任务队列

    • G,{console.log(4)}
    • H,{console.log(5)}
  5. 第五轮,执行G,输出4,后面没有then处理程序了

    然后执行H,输出5,把H的then处理程序加入微任务队列

    • I,{console.log(6)}
  6. 第六轮,执行I,输出6,后面没有then处理程序了,结束。

与上例做个对比,将 return Promise.resolve(4) 改为 return 4,看看结果有何不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Promise.resolve()
.then(() => {
console.log(0)
return 4
})
.then(res => {
console.log(res)
})

Promise.resolve()
.then(() => {
console.log(1)
})
.then(() => {
console.log(2)
})
.then(() => {
console.log(3)
})
.then(() => {
console.log(5)
})
// 0 1 4 2 3 5

还是这个例子,再看一种不同的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Promise.resolve()
.then(() => {
console.log(0)
return {
then(resolve) {
resolve(4)
}
}
})
.then(res => {
console.log(res)
})

Promise.resolve()
.then(() => {
console.log(1)
})
.then(() => {
console.log(2)
})
.then(() => {
console.log(3)
})
.then(() => {
console.log(5)
})
//输出: 0 1 2 4 3 5
//解析:关于4的输出,resolve把then的输出加入到了微任务队列,下一次微任务循环时输出

参考文章

一文详解JS中的事件循环机制_脚本之家 (jb51.net)

JavaScript 高级深入浅出:async、await 与事件循环 - 掘金 (juejin.cn)


JavaScript异步(四)事件循环机制
http://timegogo.top/2023/02/10/JavaScript/JavaScript异步(四)事件循环机制/
作者
丘智聪
发布于
2023年2月10日
更新于
2023年7月16日
许可协议