美文网首页
Js中的Event Loop&任务队列

Js中的Event Loop&任务队列

作者: 十年之后_b94a | 来源:发表于2021-12-31 17:55 被阅读0次

    前言

    Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
    以下内容仅为我个人理解,如有言误请及时通知我。

    任务

    用个现实的例子我们俩比喻js中的任务,比如一个人一天,要打扫卫生,吃饭,上厕所,工作等。。。但是这些事情不可能同时进行,同时吃饭&上厕所🐶,所以我们就要一个顺序,做完某件事接着做另一件事,所以我们规划出一个任务队列,在js中同理

    JavaScript中,任务被分为两种,一种宏任务,一种叫微任务。

    宏任务
    script全部代码、setTimeoutsetIntervalsetImmediateI/OUI Rendering

    微任务
    Process.nextTick(Node独有)、PromiseObject.observe(废弃)、MutationObserver

    执行顺序

    Javascript 有一个主线程和 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。执行顺序当主线程的任务执行完成之后,他会往微任务中拿取任务直到微任务队列中没有任务了,再往宏任务队列中拿取任务,这就是一次事件轮询。在任务队列中执行的顺序就是先进先出的原则。请记住这句话,当遇到该问题之后 就不会做了。

    案例

    我这里的案例是往上一次的代码中增加代码。

    基础案例

    console.log('start')
    setTimeout(()=>{
      console.log('time')
    })
    Promise.resolve().then(()=>{
      console.log('promise')
    })
    console.log("end")
    

    当理解了我们的上述的执行原则,我们就很简单的就能说出答案
    start =>end=>promise=>time

    image.png

    脑海中自然的能想到是这样的图绘,当主线程执行完结束之后,就回去微任务中找任务队列,当微任务队列中执行完之后在执行宏任务队列,此时就完成一次事件轮询了。

    案例升级 -> 微任务中继续执行微任务

    console.log('start')
    setTimeout(()=>{
      console.log('time')
    })
    Promise.resolve().then(()=>{
      console.log('promise');
      // 不同点
      Promise.resolve().then(()=>{
        console.log('子promise');
      })
    })
    console.log("end")
    

    想想此时的执行顺序会是什么?记住我们那句话,直到微任务队列中没有了任务再继续执宏任务。所以此时顺序则是:start =>end=>promise=>子promise=>time

    image.png

    案例升级 -> 在宏任务中执行微任务

    console.log('start')
    setTimeout(()=>{
      console.log('time')
      // 不同点
      Promise.resolve().then(()=>{
        console.log('promise - time');
      })
    })
    Promise.resolve().then(()=>{
      console.log('promise');
      Promise.resolve().then(()=>{
        console.log('子promise');
      })
    })
    console.log("end")
    

    分析,主线程的两个打印执行完成之后,微任务宏任务队列中各有一个任务,然后执行微任务打印promise,打印完之后,发现一个微任务,那么往微任务队列中增加任务,继续执行微任务打印子promise 然后在执行宏任务 打印 time,继续添加任务至微任务队列中,然后继续执行微任务打印promise - time
    所以执行顺序:start =>end=>promise=>子promise=>time=>promise - time

    image.png

    案例升级->在微任务中执行宏任务

    console.log('start')
    setTimeout(()=>{
      console.log('time')
      Promise.resolve().then(()=>{
        console.log('promise - time');
      })
    })
    Promise.resolve().then(()=>{
      console.log('promise');
      Promise.resolve().then(()=>{
        console.log('子promise');
      })
      // 不同点
      setTimeout(()=>{
         console.log('子setTimeout');
      })
    })
    console.log("end")
    

    接下来我们分析该案例:首先毫无疑问主线程的console先执行,此时我们在看微任务队列和宏任务队列中分别各有一个任务,那么先执行宏任务队列的任务,执行到打印promise,发现有一个微任务,那么继续放入队列中,在发现一个宏任务那么继续放入宏任务队列中,现在微任务队列中还有一个任务,则直接打子promise,此时微任务队列中无任务了,那么转而执行宏任务:首先要知道 现在宏任务队列中有两个任务,保持先进先出原则,那就是先打印time然后发现有一个微任务,放入队列,然后继续轮询,发现微任务队列中有任务,则继续打印promise - time,执行完成之后,此时微任务队列中清空,但是此时宏任务还有一个任务等待执行,所以继续执行宏任务打印子settimeout
    执行顺序:start=>end=>promie=>子promise=>time=>promise - time=>子settimeout

    image.png

    案例再升级 -> 微任务并列执行

    console.log('start')
    setTimeout(() => {
        console.log('time')
        Promise.resolve().then(() => {
            console.log('promise - time');
        })
    })
    Promise.resolve().then(() => {
        console.log('promise1');
        Promise.resolve().then(() => {
            console.log('子promise1');
        })
        setTimeout(() => {
            console.log('子setTimeout1');
        })
    })
    // 不同点
    Promise.resolve().then(() => {
        console.log('promise2');
        Promise.resolve().then(() => {
            console.log('子promise2');
        })
        setTimeout(() => {
            console.log('子setTimeout2');
        })
    })
    console.log("end")
    

    这里分析我们就说白话了,直接画图:看看各个任务队列中的顺序


    image.png 执行结果 image.png

    案例 promise链式调用

    console.log('start')
    
    
    setTimeout(() => {
        console.log('time')
        Promise.resolve().then(() => {
            console.log('promise - time');
        })
    })
    Promise.resolve().then(() => {
        console.log('promise1');
    }).then(() => {
        console.log('promise2');
        Promise.resolve().then(() => {
            console.log('promise2 --- 1');
        })
    }).then(() => {
        console.log('promise3');
    }).then(() => {
        console.log('promise4');
    })
    
    Promise.resolve().then(() => {
        console.log('promise1 - next');
    }).then(() => {
        console.log('promise2 - next');
    }).then(() => {
        console.log('promise3 - next');
    }).then(() => {
        console.log('promise4 - next');
    })
    
    console.log("end")
    

    promise的链式调用的执行顺序是上一次的then 执行完毕之后在继续执行新一次的then调用,所以在微任务队列中的任务顺序一定要清晰。

    image.png

    定时器模块

    什么是定时器模块?定义一个定时器任务,那么该任务是什么时机放入宏任务队列中的?要知道定时器是有一个间隔时间设置的,众所周知,时间间隔设置的越低,该任务最先执行,所以说当一个定时器设置的时间间隔到了之后,再把任务推进宏任务队列,然后再按照先进先出的原则,执行任务。

    看看这一个案例:如果主线程中定义一个定时器,并设置时间为2秒,但是在主线程任务执行完毕远超2秒,那我们想象,当主线程任务执行完毕,是会等待2秒之后执行定时器中的任务,还是说立即就执行了定时器的任务?看代码

    setTimeout(()=>{
      console.log('time')
    },2000)
    // 假设这个for循环执行时间超过2秒(因电脑配置不同,这段程序执行的时间并不一致)
    for(let i = 0 ;i <10000;i++){
      console.log('');
    }
    

    仔细观察结果:我们会发现当主线程for循环执行完成之后,并没有等待2秒,而是立马执行了定时器任务;也就是说,当程序运行时,settimeout会被放入定时起模块,并且开始计时,当时间一到就推送至宏任务队列,但是并不影响主线程任务执行,当主线程、微任务执行完毕之后,就会执行宏任务队列了。

    setTimeout(()=>{
      console.log('time - 10')
    },10)
    setTimeout(()=>{
      console.log('time - 9')
    },9)
    for(let i = 0 ;i <10000;i++){
      console.log('');
    }
    

    此时我们绘制运行图:

    image.png
    此时运行图还没有执行主线程任务,定时器模块中有两个,time-10先进入,但是它的时间间隔大于后面那个定时器任务,所以time-9先进入宏任务队列中。

    Promise微任务处理逻辑

    关于promise我们知道,在promise的构造函数体中 这一部分代码时同步代码,也就是在主程序运行的代码,而then调用则在构造函数体中返回状态(resolve,reject)之后在执行。

    console.log('start');
    setTimeout(()=>{
      console.log('time')
    
      new Promise((resolve)=>{
        console.log('promise - time')
        resolve();
       }).then(()=>{
        console.log('then - time')
      })
    })
    
    new Promise((resolve)=>{
      console.log('promise')
      resolve();
    }).then(()=>{
      console.log('then')
    })
    console.log('end')
    

    相信通过上面的一些案例,这里的执行顺序你应该心里很明白。主线程的任务不用说值的注意的是promise的同步代码也是同步执行的。
    执行顺序:start->promise->end->then->time->promise - time-> then-time

    Dom渲染任务

    虽然我们在上诉大篇幅讲了主线程、微任务、宏任务,但是浏览器内核本事是多线程的 image.png

    ,那我们来探讨下Dom渲染这个任务是发生在哪个环节?

    在讲解这个Dom渲染时,可以网上翻翻宏任务中有哪几种类型?我们这一节关注:script全部代码、 UI Rendering,我们知道按照我们的习惯一般script脚本会放在</body>,我们都知道这是因为防止我们操作不了dom等所以放在body体内,但是今天我们看一段代码,你就会知道除了这个原因还有额外的原因,这里今天我们不探讨scritpt属性asyncdefer,因为这两个属性会影响js脚本执行的顺序。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
    
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <script>
          //主要是这里代码。不管是因为你引入外部的js脚本还是直接写入代码 执行的效果都是一样的
            for (let i = 0; i < 1000000; i++) {
                console.log(' ');
            }
        </script>
    </head>
    
    <body>
        Event Loop 测试
    </body>
    
    </html>
    

    我们仔细看页面的渲染,会发现网页渲染会有一段较长时间的空白,之后在加载出文字。这就意味着js脚本会影响浏览器渲染dom的时机。这是为什么呢?因为我们上面的js的任务没干完,所以Dom渲染的任务,就会排在上一个宏任务的后面,所以我们一般把js脚本放在body体内,虽然页面的icon一直在转,但是dom渲染的任务已经执行完毕了。

    相关文章

      网友评论

          本文标题:Js中的Event Loop&任务队列

          本文链接:https://www.haomeiwen.com/subject/vlxzcltx.html