前言
Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程
运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
以下内容仅为我个人理解,如有言误请及时通知我。
任务
用个现实的例子我们俩比喻js中的任务,比如一个人一天,要打扫卫生,吃饭,上厕所,工作等。。。但是这些事情不可能同时进行,同时吃饭&上厕所🐶,所以我们就要一个顺序,做完某件事接着做另一件事,所以我们规划出一个任务队列,在js中同理
在JavaScript
中,任务被分为两种,一种宏任务,一种叫微任务。
宏任务
script
全部代码、setTimeout
、setInterval
、setImmediate
、I/O
、UI Rendering
。
微任务
Process.nextTick
(Node独有)、Promise
、Object.observe
(废弃)、MutationObserver
执行顺序
Javascript
有一个主线程和 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。执行顺序当主线程的任务执行完成之后,他会往微任务中拿取任务直到微任务队列中没有任务了,再往宏任务队列中拿取任务,这就是一次事件轮询。在任务队列中执行的顺序就是先进先出的原则。请记住这句话,当遇到该问题之后 就不会做了。
案例
我这里的案例是往上一次的代码中增加代码。
基础案例
console.log('start')
setTimeout(()=>{
console.log('time')
})
Promise.resolve().then(()=>{
console.log('promise')
})
console.log("end")
当理解了我们的上述的执行原则,我们就很简单的就能说出答案
start =>end=>promise=>time
脑海中自然的能想到是这样的图绘,当主线程执行完结束之后,就回去微任务中找任务队列,当微任务队列中执行完之后在执行宏任务队列,此时就完成一次事件轮询了。
案例升级 -> 微任务中继续执行微任务
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
案例升级 -> 在宏任务中执行微任务
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
案例升级->在微任务中执行宏任务
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
案例再升级 -> 微任务并列执行
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的链式调用的执行顺序是上一次的
image.pngthen
执行完毕之后在继续执行新一次的then
调用,所以在微任务队列中的任务顺序一定要清晰。
定时器模块
什么是定时器模块?定义一个定时器任务,那么该任务是什么时机放入宏任务队列中的?要知道定时器是有一个间隔时间设置的,众所周知,时间间隔设置的越低,该任务最先执行,所以说当一个定时器设置的时间间隔到了之后,再把任务推进宏任务队列,然后再按照先进先出的原则,执行任务。
看看这一个案例:如果主线程中定义一个定时器,并设置时间为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('');
}
此时我们绘制运行图:
此时运行图还没有执行主线程任务,定时器模块中有两个,
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
属性async
和defer
,因为这两个属性会影响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渲染的任务已经执行完毕了。
网友评论