从Promise链理解Event-Loop

面试题

new Promise(resolve => {
  setTimeout(()=>{
    console.log(666);
    new Promise(resolve => {
     resolve();
    })
    .then(() => {console.log(777);})
  })
  resolve();
 })
 .then(() => {
	    new Promise(resolve => {
	      resolve();
	    })
	   .then(() => {console.log(111);})
	   .then(() => {console.log(222);});
 })
 .then(() => {
	   new Promise((resolve) => {
	     resolve()
	   })
	   .then(() => {
		     new Promise((resolve) => {
		       resolve()
		     })
		    .then(() => {console.log(444)})
	  })
	  .then(() => {
	    console.log(555);
	  })
})
.then(() => {
  console.log(333);
})  

答案

111
222
333
444
555
666
777

如果你没有得出正确的结果,有必要继续往下看.

为了能正确解答上题,需要对宏任务、微任务以及Event-Loop深入理解.



知识点

宏任务

浏览器执行代码的过程中,JS引擎会将大部分代码进行分类,分别分到这两个队列中–宏任务(macrotask ) 和 微任务(microtask ) .

常见的宏任务:script(整体代码), XHR回调,setTimeout, setInterval, setImmediate(node独有), I/O.

上面的描述仍然有些生涩,下面借助案例深入理解.

app.js

    setTimeout(()=>{ //宏任务2
      console.log(2);
    },0)
    setTimeout(()=>{  //宏任务3
      console.log(3); 
    },0)
    console.log(1);

执行结果: 1 – 2 – 3

  • 浏览器开始运行 app.js 时启动了第一个宏任务(宏任务1,指向app.js整体代码)并开始执行.
  • 在执行宏任务1途中遇到了第一个定时器,浏览器便会启用一个新线程去跑定时器的逻辑,而当前的js线程不会停直接跳过定时器继续往下执行.当定时器的那条线程跑完后,它的回调函数被添加到宏任务队列等待,这就是宏任务2.
  • 而js线程这边又遇到了定时器又开启一条线程跑定时器的逻辑,js线程跳过这段继续往下执行.当定时器线程跑完后,它的回调函数被添加到宏任务队列等待,这就形成了宏任务3,宏任务3排在宏任务2的后面.
  • js线程走到最后输出了1,此时宏任务1就结束了.浏览器此刻就会去宏任务队列中寻找,排在最前面的是宏任务2,执行输出2.宏任务2结束又执行宏任务3输出3.

浏览器里面包含有很多个线程,js是单线程,它跑在js引擎中.而定时器,ajax请求都是在不同的线程里执行.比如定时器线程执行完毕,它会将回调函数放到宏任务队列中.ajax请求也如此,它会单独使用一个线程发起ajax请求,请求成功后将回调函数放入宏任务队列中.这里需要格外指出,ajax请求和定时器本身的延迟功能都不能算是宏任务,当ajax请求以及定时器都执行完毕后,它们的回调函数才会放入宏任务队列里等待执行.

微任务

微任务是宏任务的组成部分,微任务与宏任务是包含关系,并非前后并列.如果要谈微任务,需要指出它属于哪个宏任务才有意义.

常见的宏任务:process.nextTick(nodejs端),Promise等.

app.js

	console.log(1);
	new Promise((resolve)=>{
		resolve();
    }).then(()=>{
		console.log(2)
    })
	console.log(3)

执行结果: 1 – 3 – 2

  • 运行 app.js 脚本文件启动宏任务1,第一行代码执行输出1.
  • 碰到Promise,将then的回调函数放入宏任务1的微任务队列中等待,线程继续往下.
  • 代码跑到最后一行输出3.此时同步代码执行完毕,开始检查当前宏任务中的微任务队列.
  • 运行微任务队列中的第一个then回调函数输出2.再检查微任务队列,没有发现其他任务.
  • 微任务队列执行完毕,宏任务1执行完毕.

宏任务由宿主环境开启,与此相对应,微任务是 js 引擎从代码层面开启的.

如果还对宏任务和微任务的关系模棱两可,下面从 Event-Loop 角度详细阐述.

Event-Loop

Event-Loop
从上图可知,宏任务形成了一个拥有先后顺序的队列.每个宏任务中分为同步代码和微任务队列.

  • 假设js当前的线程执行宏任务1,先执行宏任务1中的同步代码.
  • 如果碰到Promise或者process.nextTick,就把它们的回调放入当前宏任务1的微任务队列中.
  • 如果碰到setTimeout, setInterval之类就会另外开启线程去跑相应的逻辑,而js线程跳过这段继续往下执行.另起的线程执行完毕后再在当前宏任务1的队列后面创建新的宏任务并将定时器的回调函数放入其中.
  • 同步代码执行完,开始执行宏任务1的微任务队列,直到微任务队列的所有任务都执行完.
  • 微任务队列的所有任务执行完毕,宏任务1再看没有其他代码了,当前的事件循环结束.js线程开始执行下一个宏任务,直到所有宏任务执行完毕.如此整体便构成了事件循环机制.


延伸

dom操作属于宏任务还是微任务

 console.log(1);
 document.getElementById("div").style.color = "red";
 console.log(2);

在实践中发现,当上面代码执行到第三行时,控制台输出了1并且dom结构发生了变化.但要引起注意,dom树更新不代表浏览器已经渲染完了,浏览器只会在微任务队列全部执行完毕以后才开始渲染页面.

dom操作它既不能算宏任务也不能算微任务,它应该归于同步代码执行的范畴.

requestAnimationFrame属于宏任务还是微任务

setTimeout(() => {
  console.log("11111")
}, 0)
requestAnimationFrame(() => {
   console.log("22222")
})
new Promise(resolve => {
  console.log('promise');
  resolve();
})
.then(() => {console.log('then')})

执行结果: promise – then – 22222 – 11111

很多人会把 requestAnimationFrame 归结到宏任务中,因为发现它会在微任务队列完成后执行.

但实际上 requestAnimationFrame 它既不能算宏任务,也并非是微任务.它的执行时机是在当前宏任务范围内,执行完同步代码和微任务队列后再执行.它仍然属于宏任务范围内,但是是在微任务队列执行完毕后才执行.

Promise的运行机制

包裹函数是同步代码

 new Promise((resolve)=>{
    console.log(1);
	resolve();
  }).then(()=>{
    console.log(2);
 })

new Promise里面的包裹的函数,也就是输出1的那段代码是同步执行的.而then包裹的函数才会被加载到微任务队列中等待执行.


Promise链条如果没有return

new Promise((resolve)=>{
    console.log(1)
	resolve();
}).then(()=>{
    console.log(2);
}).then(()=>{
    console.log(3);
}).then(()=>{
    console.log(4);
})

执行结果: 1 – 2 – 3 – 4

在平时开发中,在Promise链中通常会返回一个新的Promise做异步操作返回相应的值.如下.

new Promise((resolve)=>{
    console.log(1)
	resolve();
}).then(()=>{
     return new Promise((resolve)=>{
       resolve(2)
     })
}).then((n)=>{
    console.log(n);
})

执行结果: 1 – 2

但上述代码中,then函数的回调里没有返回任何东西.但是后续then包含的回调函数仍然会依次执行,返回 1 – 2 – 3 – 4.并且它可以在末尾无限接then函数,这些函数也都会依次执行.


多个then函数执行次序

new Promise((resolve)=>{   // 1
    console.log("a")         // 2         
	resolve();                // 3
}).then(()=>{               // 4
    console.log("b");       // 5
}).then(()=>{               // 6
    console.log("c");       // 7
})                          // 8
console.log("d")          // 9

执行结果: a – d – b – c

  • 1,2,3行为同步执行的代码,一气呵成输出 a.
  • 此时线程走到第4行碰到then函数的回调,将其放入微任务的队列等待.
  • 线程继续往后走直接跳到了第9行输出了 d,为什么会忽略第6行的then直接跳到第9行呢?因为第4行的then函数回调执行完毕后才会开始执行第6行的代码.(如果不理解为什么此刻会忽略掉第6行代码可以查阅一下函数柯里化的概念).
  • 同步代码执行完毕,开始执行微任务队列.此时微任务队列里面只包含了一个then的回调函数,执行输出b.
  • 4,5行执行完毕后,开始执行第6行代码.发现了then函数回调,将其放入微任务队列中.此时第一个微任务执行完了,将其清空.
  • 微任务队列中还有一个刚放进去的微任务,执行输出 c.清除此微任务,至此微任务队列为空,全部任务执行完毕.


解题

有了以上知识的储备再回到本文最初的面试题,这道题就可以轻松解决了.(为了方便阐述,加入右边行号)

new Promise(resolve => {            // 1
  setTimeout(()=>{                       // 2
      console.log(666);                   // 3
      new Promise(resolve => {     // 4
        resolve();                              
      })                                          
      .then(() => {console.log(777);})   // 7
  })                                               
  resolve();                                 // 9
 })                                           // 10
 .then(() => {                                // 11
	     new Promise(resolve => {        // 12
	       resolve();                              // 13
	     })
	     .then(() => {console.log(111);})    // 15
	     .then(() => {console.log(222);});   // 16
 })                                                 // 17
 .then(() => {                               // 18
	     new Promise((resolve) => {       // 19
	       resolve()
	     })
	    .then(() => {                               // 22
		     new Promise((resolve) => {        // 23
		       resolve()
		     })
		    .then(() => {console.log(444)})       // 26
	     })
	    .then(() => {                                   // 28
	       console.log(555);                   // 29
	    })
})
.then(() => {                       // 32
  console.log(333);
})  
  • 线程执行第一行代码,同步执行Promise包裹的函数.
  • 在第二行发现定时器,启动一个宏任务,将定时器的回调放入宏任务队列等待,线程直接跳到第9行执行
  • 第9行执行完开始执行第11行代码发现then函数,放入当前微任务队列中.线程往后再没有可以执行的代码了,于是开始执行微任务队列.
  • 执行微任务队列进入第12行代码,运行到第15行代码时发现then函数放入微任务队列等待.随后线程直接跳到第18行,碰到then函数放到微队列中.后续没有可执行的代码了,再开始执行微任务队列的第一个任务也就是第15行代码输出111.
  • 15行执行完执行到16行碰到then回调放入微任务队列等待.随后线程跳到18行的微任务开始执行,一直执行到22行碰到then函数又放入微任务队列等待.此时线程继续往下跳到第32行碰到then函数放入微任务队列等待.后续没有可执行的代码了,再开始执行微任务队列的第一个任务.
  • 线程跳到第16行执行微任务输出 222,随后又跳到22行执行下一个微任务,在26行处碰到then函数放入微任务队列等待.线程继续执行下一个微任务跳到32行输出 333.至此这一轮的三个微任务全部执行完毕清空,又开始执行微任务队列的第一个任务,线程跳到第26行输出 444.
  • 线程执行到28行碰到then函数回调放入微任务队列等待.后续没有可执行的代码了,再开始执行微任务队列的第一个任务即29行代码输出 555.
  • 所有微任务执行完毕,当前宏任务结束.线程开始执行下一个宏任务,线程跳到第三行输出 666.
  • 线程继续往后第7行碰到then回调放入微任务队列,后续没有可执行的代码了,再开始执行微任务队列的第一个任务输出 777.第二个宏任务执行完毕.

综上所述:输出分别为 111 – 222 – 333 – 444 – 555 – 666 – 777

猜你喜欢

转载自blog.csdn.net/brokenkay/article/details/110261000