10 个关于 Promise 和 setTimeout 知识的面试题,通过图解一次说透彻
英文 | https://javascript.plainenglish.io/6-interview-questions-that-combine-promise-and-settimeout-34c430fc297e
翻译 | 杨小爱
JS引擎中有两个任务队列:macrotask queue和microtask queue
整个脚本最初作为宏任务执行
执行时直接执行同步代码,宏任务进入宏任务队列,微任务进入微任务队列
当前宏任务完成后,检查微任务队列,依次执行所有微任务
执行浏览器 UI 线程的渲染(您可以在本文中忽略它)
如果存在任何 Web Worker 任务,则执行它(您可以在本文中忽略这一点)
检查宏任务队列,如果不为空,则返回步骤2,执行下一个宏任务。
值得注意的是第4步:当一个macrotask完成后,先依次执行其他所有microtask,然后再执行下一个macrotask。
Mircotasks 包括:MutationObserver、Promise.then() 和 Promise.catch(),其他基于 Promise 的技术如 fetch API、V8 垃圾收集过程、node 环境中的 process.nextTick()。
Marcotasks 包括:初始脚本、setTimeout、setInterval、setImmediate、I/O、UI 渲染。
好吧,如果你不完全理解这里发生了什么,让我们用例子来练习。
一共有 10 道题:前 4 道是简单的 Promise 题,帮助你理解微任务;后面 6 个问题是 Promise 和 setTimeout 的混合。
1、
让我们从一个简单的例子开始来解释微任务。
例子:
const promise1 = new Promise((resolve, reject) => {
console.log(1);
resolve('success')
});
promise1.then(() => {
console.log(3);
});
console.log(4);
分析:
首先,执行此代码的前四行,控制台会打印出1,然后promise1就会变成resolved状态。
然后,开始执行 promise1.then(() => {console.log(3);}); 片段。因为 promise1 现在处于已解决状态,所以 () => {console.log(3);} 将立即添加到微任务队列中。
但是,我们知道 () => {console.log(3);} 是一个微任务,所以它不会立即被调用。
然后,执行最后一行代码(console.log(4);),并在控制台打印 4。
至此,所有同步的代码,即当前的宏任务,都被执行了。然后 JavaScript 引擎检查微任务队列并依次执行它们。
() => {console.log(3);} 然后执行并在控制台中打印 4。
结果如下:
2、
例子
const promise1 = new Promise((resolve, reject) => {
console.log(1);
});
promise1.then(() => {
console.log(3);
});
console.log(4);
分析:
这个例子和上一个非常相似,只是在这个例子中,promise1 会一直处于挂起状态,所以 () => {console.log(3);} 不会被执行,控制台也不会输出3。
结果:
3、
例子
const promise1 = new Promise((resolve, reject) => {
console.log(1)
resolve('resolve1')
})
const promise2 = promise1.then(res => {
console.log(res)
})
console.log('promise1:', promise1);
console.log('promise2:', promise2);
仔细考虑控制台打印结果的顺序和每个 Promise 的状态。
分析:
首先,前四行代码和之前一样,在控制台打印1,promise1的状态是resolved。
然后,执行 const promise2 = promise1.then(...),res => {console.log(res)} 被添加到微任务队列中。同时,promise1.then() 将返回一个新的待处理的 promise 对象。
然后,执行console.log('promise1:', promise1); ,控制台打印出字符串'promise1'和处于已解决状态的promise1。
然后,执行console.log('promise2:', promise2); ,控制台打印出字符串‘promise2’和处于挂起状态的promise2。
至此,所有同步的代码,即当前的宏任务,都被执行了。然后 JavaScript 引擎检查微任务队列并依次执行它们。
res => {console.log(res)} 是微任务队列中唯一的任务,现在将被执行。然后控制台将打印 'reslove1' 。
结果:
4、
例子
const fn = () => (new Promise((resolve, reject) => {
console.log(1)
resolve('success')
}));
fn().then(res => {
console.log(res)
});
console.log(2)
分析:
与之前不同的是,在这个例子中,创建 Promise 对象的行为发生在 fn 函数中。fn函数虽然是一个普通的同步函数,但并没有什么特别之处,这个例子还是很简单的。
结果:
前面的例子比较简单,现在问题会逐渐变得复杂,你准备好了吗?
5、
例子:
console.log('start')
setTimeout(() => {
console.log('setTimeout')
})
Promise.resolve().then(() => {
console.log('resolve')
})
console.log('end')
分析:
首先,JS引擎中有两个任务队列:宏任务队列和微任务队列。
在程序开始时,所有的初始代码都被视为一个宏任务,被推入宏任务队列。
然后,执行第一行代码 console.log('start') 并在控制台中打印'start'。
那么 ,setTimeout(...) 就是一个等待时间为 0 的定时器,会立即执行。正如我们在本文开头提到的,setTimeout 是一个宏任务,所以 setTimeout(...), () => {console.log('setTimeout')} 的回调函数,不会立即执行,它会 被压入宏任务队列,等待稍后执行。
然后,它开始执行 Promise.resolve().then(...),并且 () => {console.log('resolve')} 被推入微任务队列。
现在,执行console.log(‘end’),在控制台打印‘end’,第一个宏任务就完成了。
当一个宏任务完成后,JS引擎会先检查微任务的队列,然后,依次执行所有的微任务。
当微任务队列为空时,JS引擎检查宏任务队列并开始执行下一个宏任务。
值得强调的是,虽然 setTimeout(...) 比 Promise.resolve().then(...) 执行得更早,但 setTimeout(...) 的回调函数仍然执行得较晚,因为 setTimeout 是一个宏任务。这是新手犯错误最多的地方。
好的,这就是上面示例代码的运行方式。我希望我的草图能帮到你。
结果:
6、
例子
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);
分析:
首先,我们暂时忽略那些回调函数,简化代码:
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(..., 0);
console.log(2);
});
promise.then(...);
console.log(4);
然后我们像以前一样绘制图片。起初,所有的代码都可以被认为是一个宏任务。
然后开始执行new Promise(...),然后进入executor内部,执行console.log(1)。
然后开始执行 setTimeout(..., 0) 。定时器立即结束,其回调函数被推入宏任务队列。
然后开始执行 console.log(2) 。
现在开始执行 promise.then(...)。因为promise对象还处于pending状态,所以它的回调函数还没有压入微任务队列。也就是说,微任务队列当前仍然是空的。
然后开始执行 console.log(4) 。
至此,第一个宏任务结束,微任务队列还是空的,所以JS引擎开始下一个宏任务。
然后,开始执行 console.log('timerStart') 。
现在 resolve() 函数被执行,promise 的状态将被解析,promise.then(…) 的回调函数被推入微任务队列。
然后,开始执行 console.log('timerEnd') 。
现在当前的宏任务已经结束,JS引擎再次检查微任务队列,依次执行。
结果:
7、
例子:
const timer1 = setTimeout(() => {
console.log('timer1');
const timer3 = setTimeout(() => {
console.log('timer3')
}, 0)
}, 0)
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
console.log('start')
分析:
本例中有 3 个 setTimeout 函数,所以程序累加了 3 个额外的宏任务。
首先,让我们绘制初始宏任务队列。
然后,开始执行 timer1 对应的 setTimeout(...) 。同时,创建了一个新的宏任务。
然后,开始执行 timer2 对应的 setTimeout 。同时,另一个新的宏任务被创建。
好的,现在我们有了三个宏任务,没有微任务。
然后
现在第一个宏任务和它的执行都完成了,而微任务队列仍然是空的,JS引擎将开始执行下一个宏任务。
console.log('timer1') 被执行。
然后,开始执行 timer3 对应的 setTimeout(...) 。创建了一个新的宏任务。
然后
然后
结果
8、
例子
const timer1 = setTimeout(() => {
console.log('timer1');
const promise1 = Promise.resolve().then(() => {
console.log('promise1')
})
}, 0)
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
console.log('start')
分析:
此示例与上一个示例类似,不同之处在于我们将其中一个 setTimeout 替换为 Promise.then。因为 setTimeout 是宏任务而 Promise.then 是微任务,并且微任务优先于宏任务,所以控制台输出的顺序是不一样的。
首先,让我们绘制初始任务队列。
然后
然后
然后
注意此时 Promise.then() 正在创建一个微任务。它的回调函数在下一个宏任务之前由 JS 引擎执行。
然后
注意,此时Promise.then()正在创建一个微任务。它的回调函数在下一个宏任务之前由 JS 引擎执行。
然后结束。
结果:
9、
例子
const promise1 = Promise.resolve().then(() => {
console.log('promise1');
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
});
const timer1 = setTimeout(() => {
console.log('timer1')
const promise2 = Promise.resolve().then(() => {
console.log('promise2')
})
}, 0)
console.log('start');
分析:
在这个例子中,宏任务和微任务交替创建,这是一个困难的话题。如果你只是在头脑中思考,那么,很容易犯错误。但是如果你开始和我一起画图,很容易找到正确的答案。
首先,让我们绘制初始宏任务队列。
然后执行第一段代码,并创建一个微任务。
然后执行第二段代码,并创建一个宏任务
然后
当前宏任务完成,微任务队列中的任务开始。
然后,开始执行setTimeout(...)与 timer2 相关的并创建一个新的宏任务
当前的微任务队列被清空,开始下一个宏任务。
然后,创建另一个微任务。
当前宏任务已完成,JS引擎再次检查微任务队列,发现队列不为空,开始对微任务队列中的任务进行优先级排序。
最后
结果
10、
例子
const promise1 = new Promise((resolve, reject) => {
const timer1 = setTimeout(() => {
resolve('success')
}, 1000)
})
const promise2 = promise1.then(() => {
throw new Error('error!!!')
})
console.log('promise1', promise1)
console.log('promise2', promise2)
const timer2 = setTimeout(() => {
console.log('promise1', promise1);
console.log('promise2', promise2);
}, 2000)
分析:
首先,它通过new Promise(…) 创建了promise1,它处于pending 状态。还创建了一个延迟为 1 秒的计时器。
然后,执行 const promise2 = promise1.then(...),因为 promise1 目前处于 Pending 状态,所以 promise1.then() 的回调函数还不会加入到微任务队列中。
然后,执行 console.log('promise1', promise1) 。此时,promise1 仍处于 Pending 状态。
然后,执行 console.log('promise2', promise2) 。此时,promise2 仍处于 Pending 状态。
然后,执行 const timer2 = setTimeout(…) 。还创建了一个延迟为 2 秒的计时器。
1000 毫秒后,timer1 完成。然后执行 thenresolve('success'),promise1 被解决。
调用 promise1.then(...) 的回调函数,并执行 throw new Error('error!!!')。抛出一个错误,promise2 被拒绝。
又过了 1000 毫秒,timer2 完成。() => {console.log('promise1', promise1); console.log('promise2', promise2);} 被执行。
结果:
总结
以上就是我今天与你分享的10道关于 Promise 和 setTimeout知识的面试题,希望这些面试题对你有帮助,如果你觉得有用的话,请记得点赞我,关注我,并将它分享给你身边的朋友,也许能够帮助到他。
最后,感谢你的阅读,祝编程愉快!
学习更多技能
请点击下方公众号