JavaScript中Promise内setTimeout为何晚于外部setTimeout执行
核心原因
最终输出2,1的本质是:宏任务队列严格遵循先进先出的执行规则,任务的执行顺序只和它被注册进队列的时间先后有关,和回调写在微任务内部还是全局代码里没有任何关系。
复现代码
Promise.resolve().then(() => { setTimeout(() =>{ console.log("1") }, 0) }) setTimeout(() => { console.log("2") }, 0)
完整执行流程拆解
按照JS事件循环的标准规则,代码运行过程可以拆为3个阶段:
- 第一轮宏任务:初始脚本同步执行
整段脚本作为第一个宏任务启动,从上到下执行同步代码:- 运行到
Promise.resolve().then(...)时,因为Promise已经是resolve的完成状态,传入then的回调会被加入当前轮次的微任务队列,不会立刻运行。 - 继续往下运行到外层的
setTimeout(..., 0),这行代码本身是同步执行的,执行后会立刻把打印2的回调注册到宏任务队列里,这时候宏任务队列里第一个待执行的任务就是打印2的回调。
到这里所有同步代码运行完毕。
- 运行到
- 清空当前轮次全部微任务
事件循环的固定规则是:每一轮宏任务执行结束后,必须把当前微任务队列里的所有任务全部执行完,才会开始调度下一轮宏任务。- 从微任务队列取出之前存入的then回调开始执行,运行到回调内部的
setTimeout(..., 0)时,才会把打印1的回调注册到宏任务队列。这时候宏任务队列的顺序已经是[打印2的回调, 打印1的回调]——打印2的回调比打印1的回调早了整整一个微任务阶段注册,自然排在队列更靠前的位置。 - 微任务队列所有任务执行完毕,没有剩余待运行的微任务。
- 从微任务队列取出之前存入的then回调开始执行,运行到回调内部的
- 后续宏任务轮次调度
事件循环开始按先进先出的规则从宏任务队列取任务执行:- 先取出排在队首的打印2的回调运行,控制台输出
2;该回调运行结束后检查微任务队列,没有新增微任务,继续调度下一个宏任务。 - 取出队列里下一个打印1的回调运行,控制台输出
1。
- 先取出排在队首的打印2的回调运行,控制台输出
常见认知误区
你之前预期输出1,2,是错误理解了微任务的优先级规则:
- 微任务优先级高于宏任务,仅指「每轮宏任务执行完后,要优先清空所有微任务,才会处理下一个宏任务」,从来不是说写在微任务里的宏任务可以获得插队权限,跑到已经在队列里等待的其他宏任务前面。
- 所有setTimeout的回调都不会在写代码的时候就自动进入宏任务队列,只有真正运行到
setTimeout()这行语句时,对应的回调才会被推入宏任务队列排队,注册时间晚的任务自然要等前面的任务执行完才会运行。
内容的提问来源于stack exchange,提问作者GLHF
相关产品推荐
相关产品推荐

