我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情
macrotask和microtask
依旧是更新Promise系列,最近又看了一些文章,对EventLoop和浏览器运行处理代码的机制有了更多的了解,因此特来补充。
前要
众所周知javascript是单线程的语言,它执行任务时一定是一个一个的执行,对于一段程序,有些代码(任务)它并不能第一时间执行结束,最经典就是异步任务,我们需要将这些代码存起来,过一会再执行,所以js运行时是有主线程和调用栈的。主线程负责执行代码,调用栈负责存储任务,然后将任务弹出给主线程执行。
名词
EventLoop 事件循环
macrotask 宏任务
microtask 微任务
queue 队列
promise 承诺
eventLoop
EventLoop 事件循环(事件队列),是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种执行规则,它是js编译器在编译代码时设定一些规则的机制。
javascript任务
在javascript中,任务分为同步任务与异步任务,而异步任务又分为宏任务与微任务。
宏任务有 setTimeout, setInterval, setImmediate, I/O, UI rendering
微任务有Process.nextTick(Node)、Promise(回调函数)、Object.observe(废弃)、MutationObserver
先提一下,虽然我们前面实现Promise的时候选用setTimeout作为实现异步的方法,但是实际上Promise内部并不是用setTimeout来实现的,选用setimeout是因为在前Es6中没有很好的方法,而且它在日常代码中常用,容易理解。但事实上Promise属于同步任务,而在resolve之后,会将将回调函数压入数组,执行
.then()是属于异步任务,而且是微任务,并不是setTimeout的宏任务。all,reject,finally方法也都是微任务。
调用栈
调用栈顾名思义,它是一个栈的结构
栈的特点是先进后出,也就是先进入栈的任务会往栈内压,而需要执行任务时,会弹出栈顶的任务拿来执行,也就是最近一次刚刚压入栈的任务。与之相反的是队列,它的特点是先进后出,队列嘛,很形象就是有一个排队做核酸的队伍,如果需要入队,那么会往队尾入队,而执行任务(捅喉咙)时会挑选队头的任务来执行,达到先进后出的效果。
调用栈又包含两个东西,宏任务队列和微任务队列(对于宏任务和微任务),具体的细节不太清楚,但是我们可以理解为调用栈附带两个队列。遇到异步任务时就会把任务存到队列里。
eventLoop实例
依旧是那段简短又经典的代码
console.log("first");
setTimeout(() => {
console.log("second");
});
Promise.resolve("third").then((res) => {
console.log(res);
});
console.log("four");
相信了解过eventLoop的小伙伴都能得出答案 first->four->third->second
这次让我们详细解读这其中的执行过程,加深理解。当然,为了体现他是一个栈的结构,也能够体现出栈外还有两个队列,我们需要让代码稍微复杂一点
console.log(1);
setTimeout(() => {
console.log(2);
});
new Promise((resolve) => {
console.log("resolve 前");
resolve(3);
console.log("resolve 后");
});
process.nextTick(() => {
console.log(4);
});
setTimeout(() => {
console.log(5);
});
Promise.resolve(6).then((res) => console.log(res));
process.nextTick(() => {
console.log(7);
});
console.log(8);
顺序为1 resolve前 resolve后 8 4 7 3 6 2 5
简单分析一下
- 进入事件循环
- 遇到log(1),是同步任务,放入调用栈栈顶,然后弹出任务,执行,输出1
- 遇到setTimeout 2,属于宏任务,入队宏任务队列
- 遇到Promise 里面同步log 直接入栈执行,输出 resolve前/后
- 遇到Promise 3,.then()执行的回调属于微任务,入队微任务队列
- 遇到nextTick 4,node提供的异步方法,他是一个单独的,会在微任务前面执行,可以说他有着一个单独的队列
- 再次遇到setTimeout 5,入队宏任务队列队尾,也就是在setTimeout后面
- Promise 6也入队微任务队尾,
- nextTick 7也进入单独队列队尾
- 遇到log 8同步任务,直接入栈调用栈,弹出给主线程执行 输出8
- 这时发现后面没有别的任务了,那么就会寻找nextTick队列,因为它会在微任务前面执行,所以队列依次出队执行,输出4 7
- 调用栈又空了,就会寻找微任务队列,里面Promise的回调依次出队执行,输出3 6
- 再到宏任务出队执行,输出 2 5
总结
简单来说,jvascript执行代码按照下面顺序
- 每次代码执行时(任何一次任务前),都会有一个寻找的过程,有同步执行同步,异步入队
- 然后同步任务执行完,再寻找微任务队列执行
- 微任务队列空的就会执行宏任务队列
比如说当执行宏任务时,宏任务执行中新增了一个微任务,那么会再次按照同步,微任务,宏任务的顺序寻找下一个该执行的任务,所以以下代码的输出顺序是 2 time_resolve 5 time_Resolve2,
setTimeout(() => {
// 新增微任务
Promise.resolve("time_resolve").then((Res) => {
console.log(Res);
});
//先执行同步
console.log(2);
});
setTimeout(() => {
console.log(5);
Promise.resolve("time_Resolve2").then((res) => {
console.log(res);
});
});
即宏任务中间增加了微任务调用栈又去寻找同步和微任务去了,同样微任务中,例如下面代码也是会优先执行输出 1 3 ,再输出3(虽然这是看起来再正常不过的事情,你也得知道这也是有一个寻找,入栈,出栈的过程的)
Promise.resolve(1).then((res) => {
console.log(res); //进入微任务,多了一个同步任务,寻找,找到同步
//同步任务入栈,执行,出栈
Promise.resolve(2).then((res) => console.log(res));
});
我觉得这和java很类似,执行任务的顺序是在不断的改变的,也就是是js和java一样有运行时状态,它处于一个不断变化的状态中,因为它需要不停的寻找下一个执行的任务,也不断的有新的同步任务,异步任务需要被执行。
推荐文章
结语
写这次文章也是希望能够帮助你我各位在写代码时考虑的更全面,也能够写出出错率更低的代码。写完代码的看结果的时候也能够理解它为什么是这样运行的。
本次的文章到这里就结束啦!♥♥♥读者大大们认为写的不错的话点个赞再走哦 ♥♥♥ 我们一起学习!一起进步!