JavaScript -- EventLoop浏览器/宏任务/微任务知识点汇总

EventLoop

首先第一段示例代码

console.log('script start');

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

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

打印顺序为:

script start
script end
promise1
promise
setTimeout

具体为什么会打印出这个顺序,下面在具体解释。
我们具体看一下js的执行流程:
在这里插入图片描述

张倩qianniuerlv-2 JS事件循环机制(event loop)之宏任务/微任务解读

  1. 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数
  2. 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  3. 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  4. 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

在js引擎中,存在一个叫monitoring process的进程,这个进程会不断的检查主线程的执行情况,一旦为空,就会去Event Quene检查有哪些待执行的函数。

什么是宏任务/微任务:

在ES6 规范中是这样定义的,microtask 称为 jobs,macrotask 称为 task
宏任务是由宿主发起的,而微任务由JavaScript自身发起。

  1. 宏任务有哪些:
  • script的全部代码
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI-Render
  • postMessage
  • MessageChannel
    (对于普通的使用我们大部分关注和注意的应该是Script全部代码、setTimeout、setInterval)
  1. 微任务有哪些:
  • Process.nextTick(node.js独有)
  • Promise
  • MutaionObserver
  • 以Promise为基础开发的其它技术(async/await等)

执行顺序

首先我们都知道JS是一个单线程的运行机制,在同步操作的情况下我们的代码是一个从上往下的运行过程.上面写到了Script的代码是宏任务,那么我们代码的执行一开始可视作一个宏任务的执行,而每一个宏任务里都定义了一个微任务的队列,每当当前宏任务执行完成之后便会去检查微任务队列里是否有微任务,如果有,就执行,直到微任务队列执行完成,这时便回到宏任务队列去执行下一个宏任务,如此循环便是浏览器的EventLoop
宏任务队列执行当前宏任务 --> 当前宏任务执行完成查找当前宏任务中微任务队列 --> 微任务队列执行完成(微任务队列为空) --> 在宏任务队列执行下一个宏任务
以上便是简单理解浏览器的EventLoop
在这里插入图片描述
这张图的意思就是:

  1. 存在微任务的话,那么就执行所有的微任务
  2. 微任务都执行完之后,执行第一个宏任务,
  3. 循环 1, 2

从参考博主的博客里看到这段,这边不得不提一句,我也是看了这为博主的博客才理清楚了微任务和宏任务的概念。博主的链接会在文章末给出。

一个掘金的老哥(ssssyoki)的文章摘要:
那么如此看来我给的答案还是对的。但是js异步有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入eventqueue,然后在执行微任务,将微任务放入eventqueue最骚的是,这两个queue不是一个queue。当你往外拿的时候先从微任务里拿这个回掉函数,然后再从宏任务的queue上拿宏任务的回掉函数。
我当时看到这我就服了还有这种骚操作。

这边我们可以看出,微任务和宏任务是同属于两个不同的队列的!!!

宏任务一般包括:整体代码script,setTimeout,setInterval、setImmediate。
微任务一般包括:原生Promise(有些实现的promise将then方法放到了宏任务中)、process.nextTick、Object.observe(已废弃)、 MutationObserver

接下来看一个案例:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

第一轮循环:

首先打印 1 下面是setTimeout是异步任务且是宏任务,加入宏任务暂且记为 setTimeout1 下面是 process 微任务
加入微任务队列 记为 process1 下面是 new Promise 里面直接 resolve(7) 所以打印 7 后面的then是微任务
记为 then1 setTimeout 宏任务 记为 setTimeout2 第一轮循环打印出的是 1 7
当前宏任务队列:setTimeout1, setTimeout2 当前微任务队列:process1, then1,

第二轮循环:

执行所有微任务 执行process1,打印出 6 执行then1 打印出8 微任务都执行结束了,开始执行第一个宏任务 执行
setTimeout1 也就是 第 3 - 14 行 首先打印出 2 遇到 process 微任务 记为 process2 new
Promise中resolve 打印出 4 then 微任务 记为 then2 第二轮循环结束,当前打印出来的是 1 7 6 8 2 4
当前宏任务队列:setTimeout2 当前微任务队列:process2, then2

第三轮循环:

执行所有的微任务 执行 process2 打印出 3 执行 then2 打印出 5 执行第一个宏任务,也就是执行 setTimeout2
对应代码中的 25 - 36 行 首先打印出 9 process 微任务 记为 process3 new Promise执行resolve
打印出 11 then 微任务 记为 then3 当前打印顺序为:1 7 6 8 2 4 3 5 9 11 当前宏任务队列为空
当前微任务队列:process3,then3

第四轮循环:

执行所有的微任务 执行process3 打印出 10 执行then3 打印出 12 代码执行结束: 最终打印顺序为:1 7 6 8 2 4
3 5 9 11 10 12 (请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)

但是我的输出顺序是:1 7 8 6 2 4 3 5 9 11 10 12
我的node版本是14.17.5
不同的输出结果,可能是因为单独的微任务和在宏任务内的微任务,执行顺序有差别,具体原因有待进一步考究。

据说:Node版本是Node11.0.0以后的,Node的事件循环机制的差异已经把和浏览器的差异抹平了
博主的也是node版本,所以可能是版本不同,导致执行顺序有差别,按照以前的版本

NodeJS引擎中:

先执行script中的所有同步代码,过程中把所有异步任务压进它们各自的队列(假设维护有process.nextTick队列、promise.then队列、setTimeout队列、setImmediate队列等4个队列)
按照优先级(process.nextTick > promise.then > setTimeout >
setImmediate),选定一个 不为空
的任务队列,按先进先出的顺序,依次执行所有任务,执行过程中新产生的异步任务继续压进各自的队列尾,直到被选定的任务队列清空。 重复2…
也就是说,NodeJS引擎中,每清空一个任务队列后,都会重新按照优先级来选择一个任务队列来清空,直到所有任务队列被清空。
———————————————— 版权声明:本文为CSDN博主「蜗牛1T」的原创文章,遵循CC 4.0
BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/happyqyt/article/details/90644667

不同任务队列的优先级

实际上,对于任务队列的优先级的定义,Promise/A+ 规范中有作详细的解释。

图灵社区 : 阅读 : 【翻译】Promises/A+规范 : https://www.ituring.com.cn/article/66566

接下来看一个优先级案例

(其中setImmediate()和process.nextTick()是node的语句)

setTimeout(function(){
    console.log(0);
},0);
setImmediate(function(){
    console.log(1);
},0);
setTimeout(function(){
    console.log(2);
},0);
process.nextTick(function(){
    console.log(9);
});
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});
console.log(6);
process.nextTick(function(){
    console.log(7);
});
console.log(8);
// 3
// 4
// 6
// 8
// 5
// 9  我加的
// 7
// 1
// 0  我加的
// 2

// ————————————————
// 版权声明:本文为CSDN博主「蜗牛1T」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
// 原文链接:https://blog.csdn.net/happyqyt/article/details/90644667
//博主输出顺序是3 4 6 8 7 5 2 1

而博主原文是:

其中3 4 6 8是同步输出的。 因为注册顺序:1 > 2 > 5 > 7,而输出顺序是7 > 5 > 2 > 1。

所以可以很容易得到,优先级 :process.nextTick > promise.then > setTimeout > setImmediate。

process.nextTick()属于idle观察者,setImmediate()属于check观察者.在每一轮循环检查中,idle观察者先于I/O观察者,I/O观察者先于check观察者.

而实际上,上述的Promises规范早已提到异步队列优先级规定的详细定义和解释了,并不需要我们一个一个去测试。

w

web引擎和node引擎

浏览器JS引擎中:

macrotask(按优先级顺序排列): script(你的全部JS代码,“同步代码”), setTimeout, setInterval,
setImmediate, I/O,UI rendering
microtask(按优先级顺序排列):process.nextTick,Promises(这里指浏览器原生实现的 Promise),
Object.observe, MutationObserver JS引擎首先从macrotask
queue中取出第一个任务,执行完毕后,将microtask queue中的所有任务取出,按顺序全部执行; 然后再从macrotask
queue(宏任务队列)中取下一个,执行完毕后,再次将microtask queue(微任务队列)中的全部取出;
循环往复,直到两个queue中的任务都取完。 所以,浏览器环境中,js执行任务的流程是这样的:

第一个事件循环,先执行script中的所有同步代码(即 macrotask 中的第一项任务) 再取出 microtask
中的全部任务执行(先清空process.nextTick队列,再清空promise.then队列) 下一个事件循环,再回到
macrotask 取其中的下一项任务 再重复2 反复执行事件循环…

NodeJS引擎中:

先执行script中的所有同步代码,过程中把所有异步任务压进它们各自的队列(假设维护有process.nextTick队列、promise.then队列、setTimeout队列、setImmediate队列等4个队列)
按照优先级(process.nextTick > promise.then > setTimeout >
setImmediate),选定一个 不为空
的任务队列,按先进先出的顺序,依次执行所有任务,执行过程中新产生的异步任务继续压进各自的队列尾,直到被选定的任务队列清空。 重复2…
也就是说,NodeJS引擎中,每清空一个任务队列后,都会重新按照优先级来选择一个任务队列来清空,直到所有任务队列被清空。

多次验证发现,在我的node环境下(V14.17.5)执行顺序是Promise.then的优先级大于process.nextTick,但是在宏任务中的process.nextTick > promise.then

接下来看一个增加微任务promise的示例

console.log('script-start')

setTimeout(() => {
    console.log('settimeout');  
});

new Promise((resolve, reject) => {
    console.log('promise1');  
    resolve();
}).then(() => {
    console.log('promise1-then');  
});

console.log('script-end');  

//打印顺序为    script-start  
//			   promise1
//			   script-end
//			   promise1-then  
//			   settimeout

分析: 首先进入代码执行,外层同步代码为第一个宏任务执行,从上往下

  1. 执行 console.log(‘script-start’) 打印第一个log script-start
  2. 遇到定时器setTimeout,属于宏任务,将定时器里的任务放到宏任务队列
  3. 遇到Promise,首先都知道Promise创建便执行,于是执行console.log(‘promise1’) 打印第二个log promise1
  4. 由于Promise的回调是微任务,所以将Promise的回调也就是then中的代码放到微任务队列
  5. 继续往下执行 console.log(‘script-end’) 打印第三个log script-end
  6. 此时宏任务执行完成,查找微任务队列,发现刚刚放进微任务的promise的回调
  7. 执行promise.then中的代码 执行console.log(‘promise1-then’) 打印第四个log promise1-then
  8. 再查找微任务队列,为空了,则回到宏任务队列查找下一个宏任务,为刚刚碰到的定时器setTimeout
  9. 执行宏任务setTimeout console.log(‘settimeout’) 打印第五个log settimeout
    ———————————————— 版权声明:本文为CSDN博主「Skime Ma」的原创文章,遵循CC 4.0
    BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/weixin_44622984/article/details/113885482

增加async/await的示例

console.log('script-start')
	
	async function a() {
      console.log('a-start');  
      await b();
      console.log('a-end');  
    }

    async function b() {
      console.log('b');  
    }

	a()
	
	setTimeout(() => {
	    console.log('settimeout');  
	});
	
	new Promise((resolve, reject) => {
	    console.log('promise1');  
	    resolve();
	}).then(() => {
	    console.log('promise1-then');  
	});
	
	console.log('script-end');  

  	//打印顺序为    script-start 
  	//             a-start 
  	//			   b
  	//			   promise1
	//			   script-end
	//             a-end
	//			   promise1-then  
	//			   settimeout

分析: 首先进入代码执行,外层同步代码为第一个宏任务执行,从上往下

  1. 执行 console.log(‘script-start’) 打印第一个log script-start
  2. 碰到async a 以及 async b的函数声明,但是没有执行,所以继续往下
  3. 遇到a(),执行async a进入a函数执行,遇到console.log(‘a-start’) 打印第二个log a-start
  4. 碰到await b() 先进入b函数执行, 遇到console.log(‘b’) 打印第三个log b
  5. b()执行完成回到a()函数中,这时候await下面还有语句,怎么办呢? 答案是将下面的语句作为一个微任务,放进微任务队列里
  6. 然后跳出a()函数 继续往下执行
  7. 遇到定时器setTimeout,属于宏任务,将定时器里的任务放到宏任务队列
  8. 遇到Promise,执行console.log(‘promise1’) 打印第四个log promise1
  9. 将Promise的回调也就是then中的代码放到微任务队列
  10. 继续往下执行 console.log(‘script-end’) 打印第五个log script-end
  11. 此时宏任务执行完成,查找微任务队列,此时微任务队列里第一个微任务便是我们放进去的async里await下面的代码,也就是 console.log(‘a-end’) 执行打印第六个log a-end
  12. 再继续执行微任务,也就是执行promise.then中的代码 执行console.log(‘promise1-then’) 打印第七个log promise1-then
  13. 再查找微任务队列,为空了,则回到宏任务队列查找下一个宏任务,为刚刚碰到的定时器setTimeout
  14. 执行宏任务setTimeout console.log(‘settimeout’) 打印第八个log settimeout
    ————————————————
    版权声明:本文为CSDN博主「Skime Ma」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/weixin_44622984/article/details/113885482

重点:

  • 可能在这里大家就比较疑惑了,async/await的执行到底是怎么样的?
  • 这里简单的解释一下,我理解的async/await实际是Promise的一个语法糖,或者说是以Promise为基础的新技术吧,async函数中的await实际类似于Promise中的.then,首先因为代码是从右往左执行的,所以当执行到await这一行的时候,应该先从右边执行,执行完成了之后,碰到await,而await又相当于是一个Promise的.then效果,所以将接下来要执行的下面的代码作为一个微任务放到了微任务队列中

也就是说async/await函数中:

从上往下执行 --> 碰到await语句先执行右侧代码 --> 右侧代码执行完成之后将await左侧代码及下面代码放进微任务队列 –>
跳出async函数执行外层同步代码

面试题

1

for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// console:
// 3
// 3
// 3

相当于

 for (var i = 0; i < 3; i++) {
    setTimeout(() => {
      console.log(i);
    }, 1000);
  }
  //相当于
    var i = 1
    setTimeout(() => {
        console.log(i);
        }, 1000)

    var i = 2
  setTimeout(() => {
      console.log(i);
    }, 1000)

    var i = 3
  setTimeout(() => {
      console.log(i);
    }, 1000)

此题涉及到var的全局变量定义问题,通过 var 进行的定义,它会污染全局变量,所以在 for 外层,还可以看到 i 的值。

再加上这个setTimeout中设置了1秒后打印,故会有三个一秒在任务队列中等待执行,所以不会立刻console.log出来,而是等时间到了之后,再执行结果,但是不用等待整整三秒

JavaScript 在碰到 setTimeout 的时候,会将它封印进异次元,只有等所有正常的语句(if、for……)执行完毕后,才会将它从异次元解封,输出最终结果

解决方法

for (let j = 0; j < 3; j++) {
     setTimeout(() => {
       console.log(j);
     }, 1000);
}

letfor 中形成了独特的作用域块,当前的 i 只在本轮循环中有效,然后 setTimeout 会找到本轮最接近的 i,从而作出了正确的输出。

而我们通过 var 进行的定义,它会污染全局变量,所以在 for 外层,还可以看到 i 的值。

当然,讲到这里,你可能还是不太清楚更细节的区分,亦或者面试官进一步问你 var let const 的区分了,你要怎么更好回答?

看看阮一峰大佬的 ES6 文档吧:http://es6.ruanyifeng.com/#docs/let ————————————————
版权声明:本文为CSDN博主「JavaScriptLiang」的原创文章,遵循CC 4.0
BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_41806099/article/details/103573150

2

let name;
 
setTimeout(() => {
  name = 'jsliang';
  console.log(name);
}, 1000);
 
if (name) {
  name = '梁峻荣';
  console.log(name);
}
// console: 'jsliang'

JavaScript 在碰到 setTimeout 的时候,会将它封印进异次元,只有等所有正常的语句(iffor……)执行完毕后,才会将它从异次元解封,输出最终结果。

所以setTimeout会先进去宏任务列表,等待if语句执行完毕再执行,此时if语句执行,name传进来的是undefined,所以if语句不成立,后续代码不会执行下去,然后回到setTimeout 中,等待一秒后,打印name = ‘jsliang’,所以最终结果是jsliang

如果此时把第一句let name中修改成let name = 1 ,则输出结果是

梁峻荣
jsliang

3

// 位置 1
setTimeout(function () {
  console.log('timeout1');
}, 1000);
 
// 位置 2
console.log('start');
 
// 位置 3
Promise.resolve().then(function () {
  // 位置 5
  console.log('promise1');
  // 位置 6
  Promise.resolve().then(function () {
    console.log('promise2');
  });
  // 位置 7
  setTimeout(function () {
    // 位置 8
    Promise.resolve().then(function () {
      console.log('promise3');
    });
    // 位置 9
    console.log('timeout2')
  }, 0);
});
 
// 位置 4
console.log('done');

输出结果是

start
done
promise1
promise2
timeout2
promise3
timeout1

有新的收获会持续更新。

引用文章:
https://blog.csdn.net/weixin_41806099/article/details/103573150
https://juejin.cn/post/6844903512845860872#heading-3
https://juejin.cn/post/6844903638238756878
https://blog.csdn.net/weixin_44622984/article/details/113885482
https://blog.csdn.net/happyqyt/article/details/90644667
https://blog.csdn.net/qq_38606793/article/details/97368842

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值