事件循环,宏任务、微任务

148 阅读6分钟

事件循环(Event Loop)

执行栈(call stack)

首先我们知道,js 是一门单线程的脚本语言,同一时间,只能执行一个任务。而 js 的执行机制便是,把一个个任务轧入执行栈,后进先执行,执行后出栈。我们以下面这一段代码来举例说明。

1. function f1(){
2.     console.log("I am f1");
3. }
4. function f2(){
5.     f1();
6.     console.log("I am f2");
7. }
8. function f3(){
9.     f2();
10.    console.log("I am f3");
11.}
12. f3()

第 12 行之前的代码均为函数的定义,所以实际执行任务的代码从第 12 行开始,f3() 轧入执行栈;

  • f3() 调用了 f2() ,所以 f2() 入栈;
  • 而执行 f2() 又要执行 f1() ,所以又把 f1() 轧入执行栈;
  • 全部入栈后,开始执行;
  • 栈顶 console.log("I am f1")先执行,之后是console.log("I am f2")console.log("I am f3")
  • 最后执行栈为空,任务全部完成。

为什么要事件循环

如果总是按照同步的方式去执行任务,可能会有问题,比如,程序要发送一个 AJAX 请求并用回调函数处理结果,除此之外还要执行其他任务。当发送了 AJAX 请求之后程序需要等待 AJAX 返回的结果,并根据结果进行后续的操作,完成后才能往下执行其他任务。如果此时 AJAX 隔较长时间才返回结果,就会导致程序的执行效率低下。也就是说,我们更希望看到的是,程序发出 AJAX 请求,之后继续完成其他任务,直到 AJAX 请求返回结果了,程序再回过头来处理这个结果。而事件轮询就帮我们实现了这样的效果。

任务队列(task queue)

所以以上任务在浏览器里运行时的实际情况是这样的,执行 AJAX 请求后,程序就把它放一边,继续往下执行任务栈里的其他任务,直到 AJAX 请求收到结果,触发回调函数后,回调函数会被放进任务队列,而事件轮询所作的便是:每隔一段很短很短的时间便查看执行栈是否为空,如果是,就去任务队列里取一个任务出来,放到执行栈中执行,不断地重复以上过程。我们以以下代码为例。

事件循环机制

1. console.log("1");
2. setTimeout(()=>{console.log("2")},0);
3. setTimeout(()=>{console.log("3")},0);
4. console.log("4");
  • 第一行,console.log("1") 放入执行栈执行,打印 1 ;
  • 第二行,异步操作,放一边,未来某个时间(这里是 0 秒后)把回调函数 ()=>{console.log("2")}放入任务队列 ;
  • 第三行,同上,任务队列中回调函数 ()=>{console.log("3")}()=>{console.log("2")}之后 ;
  • 第四行,console.log("4") 放入执行栈执行,打印 4 ;
  • 事件轮询一直在进行,但之后当第四行执行完,执行栈为空时,它才会去任务队列中拿任务到执行栈中执行;
  • 根据队列先进先出的规则,先把回调函数 ()=>{console.log("2")}放入执行栈;
  • 执行了函数,打印了 2,执行栈又变空,事件轮询又检测到执行栈为空,又去任务队列里拿任务过来执行;
  • 把任务队列里的回调函数 ()=>{console.log("3")}拿到执行栈中,执行,执行栈又为空;
  • 执行栈为空,任务队列为空,结束。

宏任务(macrotask)、微任务(microtask)

什么是宏任务、微任务

任务队列有两种,一种是宏任务队列,一种是微任务队列,也就是说,调用了异步函数,回调函数会在未来某个时刻被放入任务队列,但有些回调函数是被放进了宏任务队列,而有些回调函数是被放入了微任务队列,这二者的执行优先级有所区别。 我们先搞清楚哪些任务会进入宏任务队列,哪些会进入微任务队列

  • 宏任务包括 script脚本, setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering
  • 微任务包括 process.nextTick,Promise,MutationObserver,(注意process.nextTick是在Node环境中具有的)

如何执行

  • 首先,记得我们刚刚说到的事件循环,宏任务队列和微任务队列都是任务队列,事件轮询机制会不断地查看执行栈,当执行栈为空时,会从任务队列里拿一个任务到执行栈中执行;
  • 但,微任务队列的优先级要高于宏任务队列,也就是说,每次执行栈为空时,优先把微任务队列里的任务一个个拿到执行栈中执行,直到微任务队列为空了,再去宏任务队列里拿任务到执行栈中执行;
  • 而执行宏任务的过程中,可能又产生了新的微任务,所以下一回事件轮询,又得先看看微任务队列有没有元素,如果有,则执行微任务,直至微任务队列为空,再执行宏任务队列中的任务;
  • 如此反复,直至结束。 我们以一段代码举例说明。
1. console.log("1");
2. setTimeout(()=>{
3.     console.log("2");
4.     Promise.resolve().then(()=>{   //Promise.resolve()返回一个状态为 resolved 的 Promise 对象
5.         console.log("3");
6.         })
7.     }
8. ,0)
9. new Promise( resolve =>{
10.     console.log("4");
11.     resolve();
12. }).then(()=>{
13.     console.log("5");
14. })
15. console.log("6");
  • 第一行,console.log("1")放入执行栈,打印 1;
  • 第二行,异步,setTimeout,回调函数放入宏任务队列;
  • 第九行,new 一个对象的过程是同步的,console.log("4")放入执行栈,打印 4;
  • 接着是 Promise 对象的 resolve() ,异步,回调函数放入微任务队列;
  • 再接着是 console.log("1"),放入执行栈,打印 6;
  • 执行栈空了,这时开始从任务队列里拿任务到执行栈执行,微任务队列优先级高于宏任务队列,微任务队列里这会之后 Promise 对象的 resolve() 的回调函数(第 11 行),把它拿到执行栈中,执行,打印 5;
  • 执行栈又为空,从任务队列中拿任务过来,微任务队列空了,那就从宏任务队列拿;
  • 这会的宏任务队列有第 2 行的 setTimeOut 的回调函数,把它拿到执行栈中,执行,console.log("2"),打印 2;
  • 接着,Promise.resolve,异步,将回调函数放入微任务队列,本宏任务(宏任务队列中的一个元素)执行结束;
  • 执行栈又空了,去任务队列拿任务过来执行,先看微任务队列,有第四行 Promise 对象的回调函数console.log("3"),打印 3;
  • 微任务队列空了,看宏任务队列,宏任务队列也空,任务都完成了。

总结

  • 执行栈执行代码过程中,遇到异步调用,把回调函数放入任务队列,待执行栈为空时,从任务队列任务逐个拿到执行栈执行;
  • 任务队列分为宏任务队列,微任务队列,每回事件轮询优先从微任务队列拿任务到执行栈执行,微任务队列为空时,再考虑宏任务(执行完一个宏任务,先看下微任务队列有没有元素,没有的话才能执行下一个宏任务);
  • 每次进入执行栈的是一个任务,只有这人任务执行完毕时,才会从任务队列拿下一个任务进入执行栈。

如有错误,欢迎指正