面试必考:v8事件中的循环机制,保姆级教学

410 阅读8分钟

引言

在了解v8事件中的循环机制之前,我们先来复习一下promise,我们都知道js是一门单线程执行的一门编程语言,其中分为同步异步,所谓的同步就是:会逐行执行代码, 会对后续代码造成阻塞,直至代码接收到预期的结果之后,才会继续向下执行任务。所谓的异步就是:调用之后先不管结果,继续向下执行任务。这个时候,我们可以通过proimse来进行调整

js当中为什么要有v8事件中的循环机制?

事件循环机制之所以重要,是因为JavaScript本身是单线程的。这意味着在任何给定时间点,只能执行一个操作。然而,这并不意味着JavaScript不能处理并发或异步任务。事件循环正是为了解决这个问题,它允许JavaScript在等待某些操作(如网络请求或定时器)完成时继续执行其他代码。

首先,我们来了解一些知识点

异步任务分为:(宏任务) 和 (微任务)

宏任务 (Macrotasks)

宏任务通常指的是那些可能需要等待一些事件或者条件满足后才会执行的任务,它们是事件循环的主要部分。宏任务的例子包括:

  • setTimeout 和 setInterval:这些是基于时间的延迟执行函数。
  • setImmediate:在Node.js环境中,这个函数会在当前事件循环的末尾执行。
  • DOM 事件:例如点击、滚动等用户交互事件。
  • I/O 操作:文件系统读写、网络请求等。

微任务 (Microtasks)

微任务是指在当前宏任务执行结束后立即执行的任务,但在下一个宏任务开始之前。微任务通常与JavaScript引擎内部的事件紧密相关,因此它们可以更快地执行。微任务的例子包括:

  • Promise 的 .then() 或 .catch() 方法:当Promise的状态改变时,相关的回调函数会被加入微任务队列。

简单总结一下

微信截图_20240613230515.png

那循环机制执行的步骤是什么呢?

简单粗略来讲的话,就是这五个步骤 微信截图_20240613231541.png

  1. 开始执行:JavaScript从你的代码的第一行开始执行。

  2. 遇到异步:当你调用像setTimeout或创建一个Promise这样的异步操作时,JavaScript并不会停下来等待它们完成,而是继续执行后面的代码。

  3. 异步完成:当异步操作完成时,比如setTimeout的时间到了,它会生成一个回调函数并放到事件队列里。

  4. 执行栈清空:JavaScript继续执行,直到执行栈(当前正在执行的代码)完全清空。

  5. 检查队列:一旦执行栈清空,JavaScript会检查事件队列。如果队列中有任务,它会取出来执行。

  6. 微任务执行:在执行下一个宏任务之前,所有在当前执行周期中产生的微任务(如Promise的回调)都会被执行。

  7. 重复步骤:执行完队列中的任务后,JavaScript会再次检查队列,重复以上过程,直到队列为空。

小试牛刀

console.log(1);
new Promise((resolve, reject) => {
    console.log(2);
    resolve()
})
.then(() => {
        console.log(3);
})
.then(() => {
        console.log(4);
})
setTimeout(() => {
    console.log(5)
});
console.log(6);

第一步:执行同步代码

1.同步代码:

      console.log(1);
      
    new Promise((resolve, reject) =>{
    console.log(2);
    resolve()
    })
    
    console.log(6);

所以编译器首先会打印1,2,6

2.别忘了,这个过程中把宏任务放入宏队列中,微任务放入微队列

微任务.then() 2个

宏任务 setTimeout()

微信截图_20240613233104.png

第二步:同步执行完毕后,检查是否有异步要执行

去哪里找异步 ?去队列里找

第三步:执行所有的微任务

我们发现微队列里面有then1then2,所以执行所有的微任务,让微任务出队列

微信图片_20240613235118.jpg

因为队列是先进先出,所以先then1先出队,打印3

然后then2出队,打印4

第四步:微任务执行完毕之后,如果有需要就会渲染页面

我们发现,这里是纯js,没有涉及到渲染

第五步:执行异步宏任务,开启下一次事件循环

我们发现微队列里面有setTimeout,所以执行所有的宏任务,让宏任务出队列 然后setTimeout出队,打印5

微信图片_20240613235238.jpg

所以,这段代码最终的执行结果是1,2,6,3,4,5

检测练习

为了看你是否真正理解,可以看下面代码,打印输出为?

console.log(1);
new Promise((resolve, reject) => {
    console.log(2);
    resolve()
})
    .then(() => {
        console.log(3);
        setTimeout(() => {
            console.log(4);
        }, 0)
    })
setTimeout(() => {
    console.log(5);
    setTimeout(() => {
        console.log(6);
    }, 0)
}, 0)
console.log(7);

同样的

第一步 执行同步代码

1.找同步代码: 同步代码如下:

     console.log(1);
     
    new Promise((resolve, reject) => { 
    console.log(2);
    resolve() })
    
    console.log(7);

所以编译器首先会打印1,2,7

2.别忘了,这个过程中把宏任务放入宏队列中,微任务放入微队列

微任务.then() 1个

宏任务 setTimeout()1个

微信截图_20240614191418.png

第二步:同步执行完毕后,检查是否有异步要执行

去哪里找异步 ?去队列里找

第三步:执行所有的微任务

我们发现微队列里面有then

所以执行所有的微任务then

   .then(() => { 
   console.log(3);
   setTimeout(() => {
   console.log(4); 
   }, 0)})

这个时候,也是先执行同步代码

同步代码

     console.log(3);

这个时候,编译器就会打印3,然后把宏任务放入宏队列中,微任务放入微队列

微信截图_20240614192812.png

执行完了之后微任务了之后,微任务then()出栈

微信截图_20240614193452.png

第四步:微任务执行完毕之后,如果有需要就会渲染页面

我们发现,这里是纯js,没有涉及到渲染

第五步:执行异步宏任务,开启下一次事件循环

因为set1(下面的那个setTimeout)先入队,说以先执行set1

setTimeout(() => { 
console.log(5);
setTimeout(() => { 
console.log(6);
   }, 0) 
}, 0)

这个时候,也是先执行同步代码

这个时候,编译器就会打印5,然后把宏任务放入宏队列中,微任务放入微队列

微信图片_20240614200228.jpg

同样的,执行完了之后微任务(set1)了之后,微任务(set1)出队

微信截图_20240614200500.png

第五步:执行异步宏任务,开启下一次事件循环

执行set2(最上面的setTimeout),所以,编译器会打印4

执行set3(最下面的setTimeout),所以,编译器会打印6

所以最终的执行结果是:1,2,7,3,5,4,6

接下来,我们来一个终极代码

首先,我们来讲一下什么async

let data = null
function getData(){
    return new Promise((resolve)=>{
        setTimeout(() => {
            data = [1, 2, 3]
            resolve()
        }, 1000);
    })
}
function another(){
     console.log(data)
}

 async function foo(){
     await getData()     //await 在谁前面,谁就要先执行
     another()
 } 
 foo()

当调用foo()时,它会等待getData中的setTimeout执行完毕,设置data的值,然后再继续执行another函数,此时data已经有值了,所以console.log(data)会输出正确的结果:[1, 2, 3]

简单的一句话就是:在xxx前面加了await,就要等待xxx执行完之后再执行下面的代码

值得注意的是:await关键字只能在async函数内部使用

await 会将后面的代码阻塞进微任务,管它是同步还是异步,把后面的执行语句阻塞进微任务

下面,我们来看一个终极代码

   console.log('script start'); 

async function async1() {
    await async2()               //await 会将后面的代码阻塞进微任务,管它是同步还是异步
    console.log('async1 end');     //把后面的执行语句阻塞进微任务
}

async function async2() {
    console.log('async2 end');
}
async1()
setTimeout(function () {
    console.log('setTimeout');
}, 0)
new Promise(function (resolve, reject) {
    console.log('promise');
    resolve()
})
    .then(() => {
        console.log('then1');
    })
    .then(() => {
        console.log('then2');
    })
console.log('script end');
  1. 宏任务开始

    • 首先输出 "script start"
  2. 执行async1

    • 调用async1函数。由于async1函数是异步的,它立即返回一个Promise并开始执行,但是遇到await async2()时会暂停当前执行流程,直到async2执行结束。async2函数执行并立即输出"async2 end"
    • async1函数继续执行,并输出"async1 end"。但是,因为await的存在,这部分代码被放入微任务队列中等待执行。
  3. 宏任务继续

    • 接下来执行setTimeout,虽然延迟时间是0毫秒,但setTimeout总是会被放到下一次事件循环的宏任务队列中执行,所以它不会立即执行。
    • 然后创建一个新的Promise并立即执行其构造函数内的代码,输出"promise"Promiseresolve方法被调用,这意味着then方法会在当前宏任务结束后,作为微任务被调度执行。
  4. 宏任务结束,微任务开始

    • 执行async1中被await阻塞的代码,输出"async1 end"
    • 然后执行Promisethen方法,依次输出"then1""then2"
  5. 宏任务再次开始

    • 输出"script end"
  6. 宏任务再次结束,检查是否有新的宏任务

    • 此时setTimeout的回调函数被调度执行,输出"setTimeout"

综上所述,最终的输出顺序应该是:

script start
async2 end
promise
script end
async1 end
then1
then2
setTimeout

总结

今天,我们讲解了关于JavaScript中事件循环机制和异步编程的相关内容。重点介绍了宏任务和微任务的概念,以及在事件循环中宏任务和微任务的执行顺序。通过具体的代码示例和分析,展示了异步任务的执行过程,包括同步代码、微任务和宏任务的执行顺序,以及await关键字在异步函数中的作用。最终,给出了一个终极代码的执行过程,进一步加深了对事件循环机制和异步编程的理解。