JavaScript——事件循环机制

252 阅读6分钟

什么是事件循环机制

在JavaScript中,代码的执行通常是同步的,按照从上到下的顺序执行。然而,当涉及到异步操作时(例如网络请求、定时器、事件监听等),JavaScript的事件循环机制就会发挥作用。

事件循环机制的基本思想是,JavaScript引擎会在主线程上执行同步代码,同时维护一个消息队列(或任务队列)。当异步任务完成或者某个事件被触发时,相应的回调函数会被放入消息队列中。当主线程上的同步任务执行完成后,它会检查消息队列,如果有任务,就将任务移出队列,并在主线程上执行相应的回调函数。这个过程不断重复,就形成了一个事件循环。

相关知识点

Promise.resolve()

Promise.resolve(value) 是一个同步执行的方法,当你调用 Promise.resolve(value) 时,它会立即返回一个已解决状态(resolved)的 Promise 对象,并且该 Promise 对象的状态已经被解决,不会等待异步操作。如果 value 是一个普通的 JavaScript 对象、字符串、数字等,Promise.resolve() 会立即返回一个已解决的 Promise 对象。

await 后接的内容

await 的后面一般是接一个 Promise的表达式(可以是异步函数或者是Promise对象),但是如果后面承接的是一个基本数据类型返回值,await 就会对其进行转换。

await 后面跟着一个非 Promise 对象的普通值(例如数字、字符串、布尔值等),JavaScript 引擎会将这个值封装为一个已解决(resolved)状态的 Promise 对象。实际上,await 关键字期望接收一个 Promise 对象,但如果提供的是一个非 Promise 的值,它会被隐式地转换为一个立即解决的 Promise 对象。

例如,当你使用 await 等待一个数字时,它会被自动封装为一个 Promise 对象,并且立即解决,使得 await 可以等待这个数字:

async function example() {
    console.log('Start');
    let result = await 42; // 等待数字42
    console.log('End, Result:', result);
}

example();

在这个例子中,await 42 实际上等同于 await Promise.resolve(42)。JavaScript 引擎会将数字 42 封装为一个立即解决的 Promise 对象,因此 await 可以正常等待这个值。

这种特性使得 await 可以等待任何 JavaScript 表达式的结果,不仅仅限于 Promise 对象。当 await 后面是非 Promise 的值时,它会被隐式地转换为一个立即解决的 Promise 对象,使得异步函数能够按照期望等待这个值。

实战代码

以下都会添加代码片段, 可以简单分析一下其运行过程和结果。

简单的事件循环

执行过程

1、执行代码自上而下,首先打印结果 script start;

2、遇到执行函数 async1(),函数中存在 await,执行函数 async2() 打印结果 async2 end,并且将 await 后面的内容存放入微任务队列中。

3、继续执行定时器,放入宏任务队列中等待执行;

4、遇到 Promise 对象,首先执行 Promise 函数中的内容: 打印 Promise ,然后将 then 函数内容存放入微任务队列中去。

5、打印 script end ;

6、开始执行第一个微任务,打印 async1 end ;

7、继续执行第二个微任务,打印 promise1;

8、继续执行第三个微任务,打印 promise2;

9、微任务内容执行完毕,开始执行宏任务,打印 setTimeout;

执行结果

script start、async2 end、Promise、script end、async1 end、promise1、promise2、setTimeout

复杂一点的执行过程

执行过程

1、运行 asy1() 函数, 然后根据顺序执行上下文,首先打印结果 1;

宏任务队列:空 微任务队列:空

2、await asy2(),中断代码开始执行函数 asy2(),函数内部是 await 后面接了一个定时器(定时器一定是在下一个宏任务队列中)。所以函数中的 await 内容被存放入了微任务队列中(同时打印 2 也被推迟了),我们这里给他起一个名字叫 micTask1

宏任务队列:空 微任务队列:micTask1

3、继续执行 console.log(7) 打印结果 7

宏任务队列:空 微任务队列:micTask1

4、继续向下执行函数 asy3(),函数的内容是一个 Promise 对象的 then 函数,这种写法会把 then 函数中的内容推入到微任务队列中执行,对于此任务我们起个名字叫:micTask2

宏任务队列:空 微任务队列:micTask1 micTask2

以上是在一次事件循环中宏任务的执行过程

5、开始执行微任务 micTask1,我们发现它后面接了一个定时器,那就会把这个定时器放入到下一次的宏任务中,我们命名为 macTask1,然后执行完成,但是我们发现 await 后面还会有代码,这个时候需要放入到微任务队列中(起名为 micTask3 ),此时任务队列如下:

宏任务队列:macTask1 微任务队列:micTask1(已完成) micTask2 micTask3

6、继续执行微任务队列中的 micTask2 任务,打印结果 6 ;

宏任务队列:macTask1 微任务队列:micTask1(已完成) micTask2(已完成) micTask3

7、继续执行微任务队列中的 micTask3 任务,打印结果 2 ;

宏任务队列:macTask1 微任务队列:micTask1(已完成) micTask2(已完成) micTask3(已完成)

8、执行宏任务 macTask1,打印结果 4 3,执行完成。

宏任务队列:macTask1(已完成) 微任务队列:micTask1(已完成) micTask2(已完成) micTask3(已完成)

执行结果

1 7 6 2 4 3

反思过程

代码的过程中容易出错的点在于 6 和 2 的执行顺序,但是我们仔细回忆一下 await 的特性就会明白,await 后面接的就是 Promise ,至于之后的代码则是放在 resolve 中执行完成,故而如果出现了 await 我们要考虑将其后续内容放置在微任务队列中,而不是直接按上下文执行。

所以如果我们将函数 asy2 中的 await 去掉的话,执行顺序就会发生变化,可以自己试一下这个结果。

再复杂一点的结构

执行过程

整体的执行过程和上面那个有点区别,在于函数 asy2() 的变化导致的顺序不同。函数中存在嵌套的自执行函数, 并且都是由 await 来接的,所以顺序上会收到这个函数的影响。

1、先打印 1

2、执行函数 asy2() 发现这里都是 await 的内容,所以先打印 3,但是 console.log(4) 放在了微任务队列中执行。同时 console.log(2) 被打印

3、打印 7

4、执行函数 asy3() 发现也有一个微任务,继续放在微任务队列中。

5、执行微任务,打印 4,但是 asy2 的完成会将后续的内容推入到微任务队列中,所以并不能立即打印 2 ;

6、执行微任务,打印 6;

7、执行微任务,打印 2,结束。

执行结果

1 3 7 4 6 2