• JS中的事件循环eventloop


    1.什么是事件循环eventloop

    1.1 同步编程

    我们先不着急明白事件循环是什么。先从它的起源入手。大家都知道JavaScript是同步的,也就是单线程,原因是因为如果不使用单线程,在操作DOM时可能会出现一些问题,比如我们有两个事件,一个是删除div,一个是添加div,他们的执行顺序不同,导致的结果也将截然不同。比如当前有div1和div2,如果先执行删除后添加,那么得到的就是div1和div3,但是如果是先执行添加后删除,那么得到的还是div1和div2。为了避免这种逻辑上的混乱,因此规定JavaScript是单线程的。

    1.2 异步编程

    但是如果JavaScript如果只是单线程的话,那也会有问题。比如我们执行多个任务,其中一个任务执行需要的时间很长,比如读取大文件或巨量的数据,此时就会造成阻塞,导致后面的任务无法执行,必须等待,造成程序假死(实际在执行,只是其中一个任务耗时特别长),用户体验极差。于是JavaScript就有了异步任务的概念。
    所谓的异步任务,和同步任务不同,同步任务是一个任务和它的回调函数完成了,才执行下一个任务。但是异步任务却是,一个任务执行了,还没执行回调函数,就直接开始执行下一个任务。

    举个不是很恰当的例子,就是一堆人在食堂排队买饭吃,第一个人买完饭(第一个任务执行了),但是还没开始吃(回调函数还没执行);第二个人就开始买饭(第二个任务就执行了),还没开始吃,第三个人就又开始买饭了(第三个任务就执行了)。

    1.3 宏任务(macro task)

    整体代码的script(外层的同步代码)、setTimeout、setInterval、DOM监听、UI渲染相关事件、ajax等。

    1.4 微任务(micro task)

    promise、async await、process.nextTick等就是微任务。

    1.5 事件循环eventloop

    说了这么多,那事件循环究竟是什么呢?事件循环,简单理解就是代码的执行流程。而理解事件循环就是理解所谓的同步代码、异步代码或者说宏任务、微任务的执行的先后顺序。

    2.事件循环的执行顺序

    2.1 执行顺序

    先执行同步代码(主任务中的宏任务),遇到异步宏任务将异步宏任务放进宏任务队列,遇到异步微任务放进微任务队列。所有同步代码执行后再将异步微任务调入主线程中执行,执行完毕后,再将宏任务队列中的宏任务调入到队列中执行。

    2.2 练习

    上面这么说可能有点抽象,让我们做几个练习理解下。

    2.2.1 同步、异步微任务、异步宏任务的执行顺序

    Promise.resolve().then(()=>{
      console.log('1')  
      setTimeout(()=>{
        console.log('2')
      },0)
    });
    setTimeout(()=>{
      console.log('3')
      Promise.resolve().then(()=>{
        console.log('4')    
      })
    },0);
    console.log('5');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    让我们分析下这道题目中的代码执行顺序,为了方便,我给每个任务都给个编号:
    1、先从最外层开始看,先找同步代码:
    任务(1)

    console.log('5');
    
    • 1

    2、再看异步代码,先看异步微任务:
    任务(2)

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

    3、最后看异步宏任务:
    任务(3)

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

    此时我们的任务列表可以分为3条,最外层同步代码:(1);异步微任务:(2);异步宏任务:(3)
    4、此时按照顺序执行(1)(2),输出 5 1 又遇到宏任务
    任务(4)

    setTimeout(()=>{
        console.log('2')
    },0)
    
    • 1
    • 2
    • 3

    再次放进宏任务队列中,此时任务列表的已经没有了最外层同步代码和异步微任务,只剩下异步宏任务:(3)(4)
    5、按照顺序执行(3),输出3时,又遇到异步微任务
    任务(5)

    Promise.resolve().then(()=>{
        console.log('4')    
      })
    
    • 1
    • 2
    • 3

    放进微任务队列中,此时任务列表变成了,异步微任务:(5);异步宏任务:(4)
    6、此时按照顺序执行就直接输出了 4 2

    所以最终执行输出的结果就是

    5
    1
    3
    4
    2
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果已经熟练的同学就可以采用直接在代码中标序号的方式进行排列,比如:

    Promise.resolve().then(()=>{	// (2)
      console.log('1')  // (2-1)
      setTimeout(()=>{	// (4)
        console.log('2')	// (4-1)
      },0)
    });
    setTimeout(()=>{	// (3)
      console.log('3')	// (3-1)
      Promise.resolve().then(()=>{	// (3-2)
        console.log('4')	//	(3-2-1)  
      })
    },0);
    console.log('5');	// (1)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    输出:

    5
    1
    3
    4
    5
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.2.2 setTimeout练习题

    可能有的同学上面那道题还没理解,那我们先从一道相对简单的题目开始:

    // 任务A
    setTimeout(()=>{
        console.log(1)
    },20)
    
    // 任务B
    setTimeout(()=>{
        console.log(2)
    },0)
    
    // 任务C
    setTimeout(()=>{
        console.log(3)
    },10)
    
    // 任务D
    setTimeout(()=>{
        console.log(4)
    },10)
    
    // 任务E
    console.log(5)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    按照队列顺序排列,最外层同步任务:E;异步宏任务:A、B、C、D。
    虽然任务的执行顺序如上,但是setTimeout具有延时效果,因此真正的输出情况应该是:

    4
    2
    3
    4
    1
    
    • 1
    • 2
    • 3
    • 4
    • 5

    同学们可以随意打乱顺序或者输出数字,对自己进行二次测试,看看自己是否掌握。

    2.2.3 Promise练习题

    console.log(1)
    new Promise((resolve,reject)=>{
        console.log(2)
        resolve()
    }).then(res=>{
       console.log(3) 
    })
    console.log(4)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    值得注意的是:上面一个promise中,console.log(2)属于宏任务,console.log(3) 才属于微任务。

    第一轮是先执行同步代码(最外层的宏任务),先输出1,代码执行到promise时立即输出2,resolve将.then()中的代码放入到了微任务中,宏任务继续输出4,最后执行微任务输出3
    所以最终的输出是:

    1
    2
    4
    3
    
    • 1
    • 2
    • 3
    • 4

    2.2.4 setTimeout和promise的执行顺序

    setTimeout(()=>{
        console.log(1)
    },0)
    new Promise((resolve,reject)=>{
        console.log(2)
        resolve()
    }).then(()=>{
        console.log(3)
    }).then(()=>{
        console.log(4)
    })
    
    console.log(5)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    先执行同步代码(主栈中的宏任务),遇到setTimeout放入宏任务列表,执行到promise,马上输出2,resolve()将then()中的两部分代码丢入到微任务中,输出5,第一轮宏任务结束,开始执行第一轮留下的微任务,输出34,第一轮循环结束。开始第二轮宏任务setTimeout输出1

    2
    5
    3
    4
    1
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.2.5 setTimout和promise执行顺序变种题

    setTimeout(()=>{
        console.log(1)
    },0)
    new Promise((resolve,reject)=>{
        console.log(2)
        for(let i = 0;i < 1000;i++){
            if(i === 10){
               console.log(10)
            }
            i === 999 && resolve()
        }
        console.log(3)
    }).then(()=>{
        console.log(4)
    })
    console.log(5)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    其实解题思路和一样,先执行同步代码(主栈中的宏任务),setTimeout放进宏任务列表中,遇到promise直接输出2,循环时输出10,resolve()将.then()中的代码放进微任务列表中,输出3,再输出5,第一轮宏任务结束,开始执行遗留下的微任务,直接输出4。第一轮事件循环结束,开始第二轮宏任务,setTimout输出1

    2
    10
    3
    5
    4
    1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在chrome的控制台console中输出结果的话,会发现每相邻的两个事件循环之间都会有一个undefined进行隔离
    在这里插入图片描述

    2.2.6 setTimeout和promise相互嵌套

    console.log(1)
    setTimeout(()=>{
        console.log(2)
        Promise.resolve().then(()=>{
            console.log(3)
        })
    },0)
    
    new Promise((resolve,reject)=>{
        console.log(4)
        setTimeout(()=>{
            console.log(5)
            resolve(6)
        },0)
    }).then(res=>{
        console.log(7)
        setTimeout(()=>{
            console.log(res)
        })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    老规矩,先执行同步代码(主栈的宏任务),输出1,遇到setTimeout

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

    放入宏任务列表中,遇到promise直接输出4,又遇到setTimeout

    setTimeout(()=>{
      console.log(5)
        resolve(6)
    },0)
    
    • 1
    • 2
    • 3
    • 4

    放进宏任务列表中,第一轮宏任务结束,且没有微任务,结束第一轮事件循环。
    开始第二轮宏任务,setTimeout输出2,resolve将console.log(3)放进微任务中,第二轮宏任务结束,执行第二轮微任务,输出3,结束第二轮事件循环。
    开始第三轮宏任务,执行下一个setTimeout输出5,同时resolve(6)将

    res=>{
     console.log(7)
     setTimeout(()=>{
         console.log(res)
     })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    放进微任务列表中,第三轮宏任务结束,执行第三轮微任务,输出7,将

    setTimeout(()=>{
         console.log(res)
     })
    
    • 1
    • 2
    • 3

    放进宏任务列表中,第三轮事件循环结束。
    开始第四轮宏任务,直接输出res,就是6
    所以输出顺序为:

    1
    4
    2
    3
    5
    7
    6
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.2.7 复合练习

    async function async1(){
        console.log(1)
        await async2()
        console.log(2)
    }
    
    async function async2(){
        new Promise((resolve)=>{
            console.log(3)
            resolve()
        }).then(()=>{
            console.log(4)
        })
    }
    
    console.log(5)
    
    setTimeout(()=>{
        console.log(6)
    },0)
    
    async1()
    
    new Promise((resolve)=>{
        console.log(7)
        resolve()
    }).then(()=>{
        console.log(8)
    })
    
    console.log(9)
    
    • 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

    第一轮宏任务(同步代码)开始,先输出5,遇到

    setTimeout(()=>{
        console.log(6)
    },0)
    
    • 1
    • 2
    • 3

    放进宏任务中,执行async1,输出1,执行async2,输出3,将

    console.log(4)
    
    • 1

    console.log(2)
    
    • 1

    放入微任务中,遇到promise,输出7,将

    console.log(8)
    
    • 1

    放入微任务中,输出9,第一轮宏任务结束。开始遗留的微任务,输出428,第一轮事件循环结束。
    开始第二轮事件循环,开始第二轮宏任务,输出6,事件循环结束。

    5
    1
    3
    7
    9
    4
    2
    8
    6
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    复合练习

    async function async1() {
        console.log('5');
        await async2();
        console.log('6');
    }
    
    async function async2() {
        console.log('7');
    }
    
    setTimeout(()=>{
        console.log(1)
    })
    
    new Promise(function(resolve){
        console.log(2);
        resolve();
    }).then(function(){
        console.log(3)
    });
    
    console.log(4);
    
    async1();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    执行第一轮宏任务(同步代码),

    setTimeout(()=>{
        console.log(1)
    })
    
    • 1
    • 2
    • 3

    放入宏任务列表,遇到promise,输出2console.log(3)放入微任务,输出4,执行async1,输出5,执行async2,输出7console.log('6')放入微任务中,第一轮宏任务结束,开始执行遗留的微任务,输出37。第一次事件循环结束,第二次宏任务开始,输出1
    所以,执行顺序为:

    2
    4
    5
    7
    3
    6
    1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    总结

    其实事件循环本身并不难,总结一下:
    (·)先执行主栈中的宏任务(同步代码),遇到setTimeout之类的宏任务就放进下个循环事件的宏任务列表中,遇见promise或者async await(其实就是对promise的封装)之类的微任务,就放进当前循环的微任务列表中。
    (2)宏任务执行完毕后,执行当前循环中的微任务。完成当前事件循环中的所有微任务后,当前事件循环结束。
    (3)开启下一轮循环后,重复上诉操作,注意每个setTimeout本身是一个宏任务,而非多个setTimeout为一个宏任务。
    (4)当然,在我们的日常工作中基本上都使用了async await的操作将异步变成同步的写法,大家很容易忘记这部分的知识点。不过在我们的工作中,有些仍然会遇到直接使用promise或者多个setTimeout的情况,这时候代码的执行顺序常常使我们困惑,因此熟悉事件循环还是有一定必要的。

  • 相关阅读:
    Java IO其它字符流简介说明
    一文读懂原子操作、内存屏障、锁(偏向锁、轻量级锁、重量级锁、自旋锁)、Disruptor、Go Context之上半部分
    【C】语言文件操作(二)
    6.串口、时钟
    AM@导数求导法则
    并发编程(三)原子性(1)
    Redis高级篇——持久化
    allegro中shape的一些基本操作(三)——挖空铜皮(shape)、删除孤岛
    WhatsApp账号被封?看看是不是你的原因!
    【论文速读】| GPTFUZZER:利用自动生成的越狱提示对大型语言模型进行红队测试
  • 原文地址:https://blog.csdn.net/qq_39055970/article/details/126502302