面试题
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);})
答案
111222333444555666777
如果你没有得出正确的结果,有必要继续往下看.
为了能正确解答上题,需要对宏任务、微任务以及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途中遇到了第一个定时器,浏览器便会开启一个新的宏任务2,定时器被添加到宏任务队列等待,线程继续往下执行.•随后又遇到了定时器开启一个新的宏任务3,定时器又被添加到宏任务队列等待,宏任务3排在宏任务2的后面,线程继续往下执行.•线程走到最后输出了1,此时宏任务1就结束了.浏览器此刻就会去宏任务队列中寻找,排在最前面的是宏任务2,发现延迟时间已到允许执行便输出了2,宏任务2结束又执行宏任务3输出3.
宏任务通常是由宿主环境开启.比如在客户端,浏览器就是宿主环境.开始执行一个脚本文件,开启一个定时器任务以及ajax请求,都是浏览器在其底层完成,并非是通过js 引擎去做的这些工作.在服务器端,node就作为了宿主环境.
微任务
微任务是宏任务的组成部分,微任务与宏任务是包含关系,并非前后并列.如果要谈微任务,需要指出它属于哪个宏任务才有意义.
常见的宏任务: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
从上图可知,宏任务形成了一个拥有先后顺序的队列.每个宏任务中分为同步代码和微任务队列.
•假设js当前的线程执行宏任务1,先执行宏任务1中的同步代码.•如果碰到Promise或者process.nextTick,就把它们的回调放入当前宏任务1的微任务队列中.•如果碰到setTimeout, setInterval之类就会在当前宏任务1的队列后面开启新的宏任务将回调放入其中.•同步代码执行完,开始执行宏任务1的微任务队列,直到微任务队列的所有任务都执行完.•微任务队列的所有任务执行完毕,宏任务1再看没有其他代码了,当前的事件循环结束.js线程开始执行下一个宏任务,直到所有宏任务执行完毕.如此整体便构成了事件循环机制.
延伸
dom操作属于宏任务还是微任务
console.log(1); document.getElementById("div").style.color = "red"; console.log(2);
在实践中发现,当上面代码执行到第三行时,控制台输出了1并且页面已经完成了重绘,div的颜色变成了红色.
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}) // 8console.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