面试题
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.then的回调等.
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之类就会另外开启线程去跑相应的逻辑,而js线程跳过这段继续往下执行.另起的线程执行完毕后再在当前宏任务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
}) // 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
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!