javascript代码的运行顺序是怎样的?一篇文章带你了解

138 阅读6分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情

macrotask和microtask

依旧是更新Promise系列,最近又看了一些文章,对EventLoop和浏览器运行处理代码的机制有了更多的了解,因此特来补充。

前要

众所周知javascript是单线程的语言,它执行任务时一定是一个一个的执行,对于一段程序,有些代码(任务)它并不能第一时间执行结束,最经典就是异步任务,我们需要将这些代码存起来,过一会再执行,所以js运行时是有主线程调用栈的。主线程负责执行代码,调用栈负责存储任务,然后将任务弹出给主线程执行

名词

EventLoop 事件循环

macrotask 宏任务

microtask 微任务

queue 队列

promise 承诺

eventLoop

EventLoop 事件循环(事件队列),是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种执行规则,它是js编译器在编译代码时设定一些规则的机制。

javascript任务

在javascript中,任务分为同步任务异步任务,而异步任务又分为宏任务微任务

宏任务有 setTimeoutsetIntervalsetImmediate, 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

简单分析一下

  1. 进入事件循环
  2. 遇到log(1),是同步任务,放入调用栈栈顶,然后弹出任务,执行,输出1
  3. 遇到setTimeout 2,属于宏任务,入队宏任务队列
  4. 遇到Promise 里面同步log 直接入栈执行,输出 resolve前/后
  5. 遇到Promise 3,.then()执行的回调属于微任务,入队微任务队列
  6. 遇到nextTick 4,node提供的异步方法,他是一个单独的,会在微任务前面执行,可以说他有着一个单独的队列
  7. 再次遇到setTimeout 5,入队宏任务队列队尾,也就是在setTimeout后面
  8. Promise 6也入队微任务队尾,
  9. nextTick 7也进入单独队列队尾
  10. 遇到log 8同步任务,直接入栈调用栈,弹出给主线程执行 输出8
  11. 这时发现后面没有别的任务了,那么就会寻找nextTick队列,因为它会在微任务前面执行,所以队列依次出队执行,输出4 7
  12. 调用栈又空了,就会寻找微任务队列,里面Promise的回调依次出队执行,输出3 6
  13. 再到宏任务出队执行,输出 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一样有运行时状态,它处于一个不断变化的状态中,因为它需要不停的寻找下一个执行的任务,也不断的有新的同步任务,异步任务需要被执行。

推荐文章

一次弄懂Event Loop

V8引擎简介

结语

写这次文章也是希望能够帮助你我各位在写代码时考虑的更全面,也能够写出出错率更低的代码。写完代码的看结果的时候也能够理解它为什么是这样运行的

本次的文章到这里就结束啦!♥♥♥读者大大们认为写的不错的话点个赞再走哦 ♥♥♥ 我们一起学习!一起进步!