Js中的Event Loop&任务队列

前言

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脚本会放在,我们都知道这是因为防止我们操作不了dom等所以放在body体内,但是今天我们看一段代码,你就会知道除了这个原因还有额外的原因,这里今天我们不探讨scritpt属性asyncdefer,因为这两个属性会影响js脚本执行的顺序。




    

    
    
    Document
    



    Event Loop 测试



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

你可能感兴趣的:(Js中的Event Loop&任务队列)