event loop它的执行顺序:
- 一开始整个脚本作为一个宏任务执行
- 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
- 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完
- 执行浏览器UI线程的渲染工作
- 检查是否有Web Worker任务,有则执行
- 执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
微任务包括:MutationObserver、Promise.then()或reject()、Promise为基础开发的其它技术,比如fetch API、V8的垃圾回收过程、Node独有的process.nextTick。
宏任务包括:script、script 、setTimeout、setInterval 、setImmediate 、I/O 、UI rendering
# 1 Promise的几道基础题
# 1.1 题目一
30 秒速记
new Promise(executor)会立即调用执行器- 顺序:
promise1→1与期约对象 - 没调用
resolve、reject,状态保持pending - 同步抛错也会拒绝,不能只查显式
reject - 控制台展开对象可能展示稍后的状态
这段代码先打印 promise1,再打印 1 和一个待定的期约对象。 Promise 构造器里的函数会同步执行,不会因为写在期约里就排到后面。执行器既没调用成功或失败回调,也没抛出异常,所以这个期约一直待定。真正延后执行的是通过 then 等方法注册的回调;创建期约本身仍是普通的同步调用。
const promise1 = new Promise((resolve, reject) => {
console.log('promise1')
})
console.log('1', promise1);
- 从上至下,先遇到new Promise,执行该构造函数中的代码promise1
- 然后执行同步代码1,此时promise1没有被resolve或者reject,因此状态还是pending
'promise1'
'1' Promise{<pending>}
面试官追问
追问 1执行器里没有定时器,为什么期约还是待定?
待定只表示还没有得到成功或失败结果,与有没有异步操作无关。执行器执行结束不会自动完成期约。
追问 2执行器直接 return 123,状态会成功吗?
不会,构造器忽略执行器的普通返回值。要通过 resolve(123) 交付结果。
追问 3把第一行日志后面换成 throw new Error() 呢?
执行器中的同步异常会让期约拒绝。外层构造调用通常返回拒绝的期约,需要通过拒绝处理函数接住。
追问 4控制台展开后看到成功,能证明打印时已经成功吗?
不一定,开发工具可能延迟读取对象内容。分析顺序时根据代码状态变化判断,必要时记录独立的标量日志。
# 1.2 题目二
30 秒速记
- 输出:
1 → 2 → 4 → 3 resolve('success')使期约成功,不终止执行器then回调走微任务,当前同步代码先完成return resolve(...)才能顺便退出当前函数- 区分“状态改变”和“回调执行”的时机
输出顺序是 1、2、4、3,因为完成期约不会打断正在执行的同步代码。 执行器先打印 1,调用 resolve 后继续打印 2;注册的 then 回调不会当场运行,所以外面的 4 先出现。等当前同步任务结束,微任务才打印 3。这里最容易混淆的是期约已经成功,但成功回调仍然要等待执行机会。
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve('success')
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
- 从上至下,先遇到new Promise,执行其中的同步代码1
- 再遇到resolve(‘success’), 将promise的状态改为了resolved并且将值保存下来
- 继续执行同步代码2
- 跳出promise,往下执行,碰到promise.then这个微任务,将其加入微任务队列
- 执行同步代码4
- 本轮宏任务全部执行完毕,检查微任务队列,发现promise.then这个微任务且状态为resolved,执行它。
1 2 4 3
面试官追问
追问 1调用了 resolve,后面的 console.log(2) 为什么还执行?
resolve 是函数调用,不是控制流程语句。需要停止后续执行时写 return resolve('success')。
追问 2在已经成功的期约上调用 then,能同步得到回调结果吗?
不能,回调仍会异步执行。想使用结果,就继续放在回调里,或在异步函数中用 await。
追问 3把外面的 4 放到一个零延迟定时器里呢?
在普通浏览器执行场景下,成功回调的微任务会先执行,再进入定时器任务。前面的同步 1、2 仍然最先输出。
追问 4resolve 后再调用 reject,会打印哪个回调?
第一次有效确定结果后,后续的完成调用不会改变它。这里仍走成功分支,不会因为后来调用 reject 改成失败。
# 1.3 题目三
30 秒速记
- 输出:
1 → 2 → 4,没有3 - 注册成功回调不会主动让期约成功
- 待定时保存反应,完成后才安排对应回调
- 等再久也不会自动从
pending变成成功 - 超时与取消需要单独设计
这里不会打印 3,因为期约一直没有被完成。 执行器中的两条日志同步输出,外层接着打印 4,但成功回调只是在等待结果。微任务机制不会替业务补出一个结果,也不会因为执行器运行完了就自动调用 then。排查类似的请求一直转圈时,要检查所有分支有没有交付结果,而不是只检查回调是否注册成功。
const promise = new Promise((resolve, reject) => {
console.log(1);
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
- 和题目二相似,只不过在
promise中并没有resolve或者reject - 因此
promise.then并不会执行,它只有在被改变了状态之后才会执行。
1 2 4
面试官追问
追问 1等一分钟,成功回调会自己执行吗?
不会,期约没有默认超时。必须有某条路径调用完成回调,或者由外部超时逻辑给调用方一个失败结果。
追问 2给它加一个 catch,能结束等待吗?
不能,拒绝处理也只是等待拒绝结果。期约既不成功也不失败时,这两个处理分支都不会运行。
追问 3想让调用方最多等三秒,可以怎么写?
可以把任务与一个定时拒绝的期约交给 Promise.race。这只限制等待结果,不会自动终止底层请求或清理资源。
追问 4执行器某个分支漏了 resolve,怎么避免?
把成功、失败和提前退出路径逐一列清楚,确保需要返回结果的路径都有完成动作。能直接返回已有异步接口时,就不要再手动包一层构造器。
# 1.4 题目四
30 秒速记
- 同步输出:
promise1,再打印两个期约 promise1已成功,值为resolve1promise2此时待定,等待then回调执行- 随后打印
resolve1,promise2成功为undefined then返回新期约,不是返回原来的对象
第二个期约要等成功回调执行结束,才能确定自己的结果。 第一个期约在执行器里已经成功,所以同步打印它时逻辑状态是成功;调用 then 得到的第二个期约此时还在等待。同步日志完成后,回调打印 resolve1,又因为没有显式返回值,第二个期约最终以 undefined 成功。控制台对象展示可能延后,判断状态要按这条执行链来推。
const promise1 = new Promise((resolve, reject) => {
console.log('promise1')
resolve('resolve1')
})
const promise2 = promise1.then(res => {
console.log(res)
})
console.log('1', promise1);
console.log('2', promise2);
- 从上至下,先遇到
new Promise,执行该构造函数中的代码promise1 - 碰到
resolve函数, 将promise1的状态改变为resolved, 并将结果保存下来 - 碰到
promise1.then这个微任务,将它放入微任务队列 promise2是一个新的状态为pending的Promise`- 执行同步代码1, 同时打印出
promise1的状态是resolved - 执行同步代码2,同时打印出
promise2的状态是pending - 宏任务执行完毕,查找微任务队列,发现
promise1.then这个微任务且状态为resolved,执行它。
