【JS】Event loop、宏队列与微队列

JS 是门非阻塞单线程语言,因为在最初 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,我们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另一个线程中删除节点),当然可以引入读写锁解决这个问题。参考 好文参考2
所以需要对异步代码进行特殊的处理才行

浏览器里的event loop

浏览器的event loop是用的v8引擎的event loop,所以勉强可以说浏览器也有event loop
先放张自己做的图
在这里插入图片描述

macrotask(宏任务)

在浏览器端,其可以理解为该任务执行完后,在下一个macrotask执行开始前,浏览器可以进行页面渲染。(对应了上一篇文章讲的script代码执行完成后,在执行下一段script代码前,页面渲染了之前的dom链接
触发macrotask任务的操作包括:

  • dom事件回调,
  • ajax回调,
  • 定时器回调,
  • script(整体代码),
  • setTimeout、setInterval、setImmediate
  • I/O、UI交互事件
  • postMessage、MessageChannel

microtask(微任务)

可以理解为在macrotask任务执行后,页面渲染前立即执行的任务。
触发microtask任务的操作包括:

  • Promise回调:Promise.then
  • Mutation回调:MutationObserver
  • process.nextTick(Node环境)

参考

JS中用来存储待执行回调函数的队列包含2个不同特定的列队

  • 宏列队:用来保存待执行的宏任务(回调),比如:定时器回调/DOM事件回调/ajax回调
  • 微列队:用来保存待执行的微任务(回调),比如:promise的回调/MutationObserver的回调

Event loop的执行顺序

  1. 执行同步代码,这属于宏任务 ,遇到异步代码,需要判断是放入宏任务队列还是微任务队列
  2. 执行栈为空,查询是否有微任务需要执行
  3. 执行所有微任务
  4. 必要的话渲染 UI
  5. 然后开始下一轮 Eventloop,执行宏任务中的异步代码(取出宏队列中排队的宏任务执行)

例子1

console.log('script start')

setTimeout(function() {
  console.log('setTimeout')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })

console.log('script end')
// script start => Promise => script end => promise1 => promise2 => setTimeout

结果:
在这里插入图片描述
分析:
以上代码执行步骤如下

  1. 第一个宏任务:执行所有同步代码
  2. 打印输出script start
  3. 遇到setTimeout,这算异步,而且是宏任务,所以把setTimeout放入宏队列排队
  4. 遇到Promise,但是Promise的参数里面的函数不是异步的,所以会打印输出’Promise’,然后把Promise的后续回调then放入微队列排队
  5. Promise的第二个回调,继续放入微队列排队
  6. 打印输出script end
  7. 这一个宏任务完成,依次执行当前微队列里面的微任务
  8. 微任务1,打印输出promise1
  9. 微任务2,打印输出promise2
  10. 微任务执行完毕,进入宏队列取出下一个宏任务
  11. 第二个宏任务开始执行
  12. 输出setTimeout

最终答案:script start => Promise => script end => promise1 => promise2 => setTimeout

例子2

setTimeout(function(){console.log(1)},0);
new Promise(function(resolve){
    console.log(2)
    for( var i=0 ; i<10000 ; i++ ){
        i==9999 && resolve()
    }
    console.log(3)
}).then(function(){
    console.log(4)
});
console.log(5);
// 这的问题是,为什么答案是 2 3 5 4 1
// 而不是 2 3 5 1 4

在这里插入图片描述
分析:

  1. 第一个宏任务:执行所有同步代码
  2. 遇到setTimeout,是宏任务,把setTimeout放入宏队列排队
  3. 遇到Promise,但是Promise的参数里面的函数不是异步的,所以会打印输出’2’,然后打印’3’,然后把Promise的后续回调then放入微队列排队
  4. 打印输出5
  5. 这一个宏任务完成,依次执行当前微队列里面的微任务
  6. 微任务,打印输出4
  7. 微任务执行完毕,进入宏队列取出下一个宏任务
  8. 第二个宏任务开始执行
  9. 输出1

例子3

  • 不管一个微任务中有多少微任务,都会放到一个环节里面执行,不会放到下一次loop里的微任务里
    比如,一个Promise的回调里,连续链式调用then,一直回调,那么这一堆回调都会在该次同步任务执行完后,全部依次执行,不会是依次loop只执行一个微任务,
  • 如果是多个微任务的then调用,则是互相交替进行,因为注册then回调的时候是交替的,每个then里面如果return 了 一个新的promise ,比如 return Promise.resolve(4),就会阻塞该promise微任务队列两次,另一个微任务连续执行,参考,答案写的很详细了
  • 但是,宏任务不会放到一次loop里,每个宏任务都会依次在下一次的loop中执行
        console.log('a')
        setTimeout(() => {
            console.log('b')
            setTimeout(() => {
                console.log('bbbb')
            }, 0)
        }, 0)
        console.log('c')
        Promise.resolve().then(() => {
            console.log('d')
            setTimeout(() => {
                console.log('j')
                Promise.resolve().then(() => {
                    console.log('kj')
                })
            }, 0)
            Promise.resolve().then(() => {
                console.log('k')
            })
        }).then(() => {
            console.log('e')
        })
        console.log('f')
        setTimeout(() => {
            console.log('q')
            setTimeout(() => {
                console.log('qqq')
            }, 0)
        }, 0)

在这里插入图片描述

例子4

目前本人想到的有两种情况

  • 当同步代码的运行时长超过异步代码的延时时间的执行顺序
  • 当两个异步代码(setTimeout)的延时不同,比如后写的setTimeout的延时却小于前面的延时时的执行顺序

同步时长超过异步延时

        setTimeout(() => {
            console.log('我是异步');
        }, 10);

        console.log('woshitongbu1');
        for (let i = 0; i < 490000001; i++) {

        }
        console.log('woshitongbu');

在这里插入图片描述

宏任务一定会等同步全部结束后才开始执行
就算10ms的时间小于循环走到490000001,也会等循环结束后并且执行完所有同步代码后才开始执行,
也不会一到该行代码就开始计时,然后49000001循环结束后,因为10ms早就到了,就立马输出是所有同步代码都结束后,该宏任务了,才开始计时

总之就是异步一定是等同步结束后才开始执行,并且是从此时开始计时,执行异步回调

当两个异步代码(setTimeout)的延时不同,比如后写的setTimeout的延时却小于前面的延时时的执行顺序

        setTimeout(() => {
            console.log('我是异步');
        }, 2000);

        console.log('woshitongbu1');
        for (let i = 0; i < 490000001; i++) {
            if (i == 490000000){
                setTimeout(() => {
                    console.log('我还是异步');
                }, 1);
            }
        }
        console.log('woshitongbu');

在这里插入图片描述

当有两个宏任务时,延时怎么算,根据event-loop的意思是,算是两个宏队列,
可以看到,我把第二个加入宏队列的setTimeout的延迟设置为1ms,第一个为2000ms

如果说,是第一个宏队列任务执行完回调后再进入第二个,那么 ‘我还是异步’ 会晚于第一个输出,
但如果宏任务只是将回调函数放入执行主队列中,等到时间到了就执行,那么就会是跟设置的延时顺序相同

问题在于,宏任务的执行,是等待回调执行完毕才进行下一项宏任务,
还是说这个异步,在开始计时的时候瞬间就完成了,再后面回调执行,而不是等回调也执行完了,再去下一个宏队列,所以分不出是谁先谁后
可以尝试把延迟取消,多次测试看看有没有调换顺序

当前一个异步的延时太小,会跟预判不同,因为计时不是那么精确,而且代码的执行也需要时间

        setTimeout(() => {
            console.log('我是异步');
        },1050);

        console.log('woshitongbu1');
        for (let i = 0; i < 490000001; i++) {
            if (i == 490000000){
                setTimeout(() => {
                    console.log('我还是异步');
                });
            }
        }
        console.log('woshitongbu');

在这里插入图片描述

setTimeout启动也需要一定的时间,所以当延时太小的时候,可能和预期的不一样
比如上面,预期应该是 先 ‘我还是异步’ 再 ‘我是异步’
因为后面的延时为0,前面的延时为1050ms,
但实际上是先走延时为1050ms的

因为第一个宏队列的走完后,把输出 ‘我是异步’ 的代码放到后面1050ms后执行,这个时候去执行下一个宏队列里面
然后准备把 ‘我还是异步’ 放到后面马上执行

但是这段时间已经花去了1050ms,所以会先执行 输出 ‘我是异步’
然后执行 输出 ‘我还是异步’

总结

宏队列的异步 比如setTimeout ,会在所有同步代码执行完毕后执行setTimeout这段代码,并且把回调函数,放入js执行代码的主线程中,可能是延时2000ms,也可能是0ms,然后继续执行主线程,有可能还有下一个宏任务,但是此时并未到达刚刚延时的回调函数执行时间,就继续执行下一个setTimeout,把下一个回调放入下一个延时的时间,后面就等着各个回调以时间顺序输出即可

注意,有可能上一个回调的延时很小,比如10ms,在下一个setTimeout执行并设置下一个回调的时候已经到了10ms,也许这第二个时间延迟更小,比如1ms,但是这第二个延时是基于第二个setTimeout执行的时候的时间,所以在IDE中可能人为判断10ms<1ms,所以该1ms那个回调先输出,但实际上,10ms会插队进去先输出。

有async和await的时候

  • 有async的如果没有await,就看做普通的同步函数
  • 但是如果没有await,也有return的时候;会把return 当做一个then的promise微任务加入微队列
  • 有await的时候,await 的那个函数会被当做new Promise的动作,然后那个函数相对于是Promise的excutor,是同步的,但是排在后的代码,相对于是then里的微任务异步代码,
  • 如果既有await,又有return,那么await和return之间的会放在一个then里面,return放在下一个then里面,链式调用,

比如,以下两个代码例子,来自于「月野猫_」的原创文章

async  function async1() {
    console.log(1)
    await waitHandle()
    console.log(2)
}
async1()
function waitHandle(){console.log(3)}

就相对于

function async1(){
    console.log(1)
    return new Promise((res) => {
        console.log(3)
    }).then((res) => {
        //await执行完毕后才会执行后面的操作
        console.log(2)
    })
}
————————————————
版权声明:本文为CSDN博主「月野猫_」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37741317/article/details/106339447

而且结合这道题可知,如果有上下两段都在进行promise异步任务,那么是相互一前一后交替进行的,
async里面有await又有return,相当于在里面链式调用了两次then,就得和下面另一个promise的来交替进行

如下:

    async function async1() {
      console.log("async1_start_2");
      await async2();
      console.log("async1_end_6");
      return 'async_return_8';
    }

    async function async2() {
      console.log("async2_3");
    }

    console.log("script_start_1");

    setTimeout(function () {
      console.log("setTimeout_9");
    }, 0);

    async1().then(function (message) { console.log(message) });

    new Promise(function (resolve) {
      console.log("promise_4");
      resolve();
    }).then(function () {
      console.log("promise_7");
    });

    console.log("script_end_5");

输出
    script_start_1
    async1_start_2
    async2_3
    promise_4
    script_end_5

    async1_end_6

	
    promise_7
    //因为async会返回一个promise对象,所以我认为是会推迟的,所以就排在了7后面
    async_return_8


    setTimeout_9

其中的

    async function async1() {
      console.log("async1_start_2");
      await async2();
      console.log("async1_end_6");
      return 'async_return_8';
    }

就相对于

    async function async1() {
      console.log("async1_start_2");
      await async2();
      console.log("async1_end_6");
      
      return 'async_return_8';
    }
    return new Promise((resolve,reject)=>{
    	async2()
    }).then((res) => {
        //await执行完毕后才会执行后面的操作
        //这里会执行return之前的所有代码,这例子里面只有一行输出
        console.log("async1_end_6");
        
    }).then(res)=>{
    	reslove('async_return_8')
    })

Node.JS里的event loop

Node 中的 Event loop
2021.6.27记,–关于node的loop,建议也看一下方老师的视频,很清楚bilibili饥人谷视频

Node 中的 Event loop 和浏览器中的不相同。
Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

timer

timers 阶段会执行 setTimeout 和 setInterval
一个 timer 指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的事务而延迟
I/O

I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调
poll

poll 阶段很重要,这一阶段中,系统会做两件事情

执行到点的定时器
执行 poll 队列中的事件
并且当 poll 中没有定时器的情况下,会发现以下两件事情

如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制
如果 poll 队列为空,会有两件事发生
如果有 setImmediate 需要执行,poll 阶段会停止并且进入到 check 阶段执行 setImmediate
如果没有 setImmediate 需要执行,会等待回调被加入到队列中并立即执行回调
如果有别的定时器需要被执行,会回到 timer 阶段执行回调。
check

check 阶段执行 setImmediate
close callbacks

close callbacks 阶段执行 close 事件
并且在 Node 中,有些情况下的定时器执行顺序是随机的

setTimeout(() => {
    console.log('setTimeout');
}, 0);
setImmediate(() => {
    console.log('setImmediate');
})

// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 否则会执行 setTimeout
上面介绍的都是 macrotask 的执行情况,microtask 会在以上每个阶段完成后立即执行

setTimeout(()=>{
    console.log('timer1')

    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)

setTimeout(()=>{
    console.log('timer2')

    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

// 以上代码在浏览器和 node 中打印情况是不同的
// 浏览器中一定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2
Node 中的 process.nextTick 会先于其他 microtask 执行

setTimeout(() => {
 console.log("timer1");

 Promise.resolve().then(function() {
   console.log("promise1");
 });
}, 0);

process.nextTick(() => {
 console.log("nextTick");
});
// nextTick, timer1, promise1

参考阮一峰老师的帖子
在这里插入图片描述

值得注意的点是,比如文件读取的任务

const fs = require('fs');

const timeoutScheduled = Date.now();

// 异步任务一:100ms 后执行的定时器
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms`);
}, 100);

// 异步任务二:文件读取后,有一个需要持续运行 200ms 的回调函数
fs.readFile('test.js', () => {
  const startCallback = Date.now();
  while (Date.now() - startCallback < 200) {
    // 什么也不做
  }
});

在首轮loop中,进入到I/O callback的时候,并不会执行callback,首轮是会进行文件的读取,把该回调函数放到第二轮的I/O callback里面运行,在第一轮的poll中就能将文件读取完毕
然后在第二轮里面,进入timers能运行setTimeout的回调,然后进入 I/O callbacks 阶段,执行fs.readFile的回调函数。这个回调函数需要 200ms,也就是说,在它执行到一半的时候,100ms 的定时器就会到期。但是,必须等到这个回调函数执行完,才会离开这个阶段。再进入下一个loop的timers执行,(但是评论区说如果poll没有任务,会回到当前的timers去执行,还有的说fs读取后的回调是放在poll里面进行的)

fs.readFile('test.js', () => {
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
});

对于这个我也有下面的疑问
在这里插入图片描述
这是别人的评论,但是和帖子前面讲的

  • Node 规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。

相违背
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值