关于ECMAScript规范中Job与Job Queue的技术疑问
我来逐个解答你关于ECMA-262第8版(ES2017)和浏览器事件循环的问题:
在ECMA-262第8版的定义里,PromiseJobs属于微任务队列,而ScriptJobs是承载脚本执行任务的队列(属于宏任务范畴)。虽然ECMA规范本身的RunJobs抽象操作没有强制规定严格的优先级顺序,但在浏览器这种实际运行环境中,结合HTML规范的事件循环机制,PromiseJobs的优先级是明确高于ScriptJobs的:
- 每当执行完一个同步任务栈(或者一个Job任务)后,浏览器会先把
PromiseJobs队列里的所有任务清空,才会去处理ScriptJobs这类宏任务队列的任务。
RunJobs是ECMA规范定义的一个“后台”抽象操作,它的核心作用就是持续检查并执行所有Job队列里的任务,直到所有队列都为空。它的触发时机和场景主要包括:
- 全局脚本(比如页面里的
<script>标签)执行完毕后,会启动RunJobs来处理已经排队的任务; - 当Promise的状态变更(比如
resolve/reject被调用)时,对应的回调会被加入PromiseJobs队列,此时会触发RunJobs来处理这些微任务; - 每执行完一个Job任务(比如动态插入的脚本执行完成)后,都会再次触发
RunJobs,检查是否还有剩余的任务需要处理。
先看你的代码运行日志:script start → 3 → script end → 2 → 1,我们一步步拆解原因:
为什么动态创建的<script>会在当前上下文执行?
这是HTML规范明确规定的行为:当你通过document.appendChild()这类方法把动态创建的<script>元素插入DOM时,浏览器会立即同步执行这个脚本,而不是把它放到宏任务队列里等待。所以在你调用document.body.appendChild(x)的瞬间,脚本里的console.log(3)就直接执行了,自然插在script start和script end之间。
完整日志顺序解析
- 首先执行
console.log('script start'),输出script start; - 创建并插入
<script>元素,浏览器立即执行脚本内容,输出3; - 回到当前回调函数,执行
console.log('script is end'),输出script end; - 当前
DOMContentLoaded回调的同步代码执行完毕,执行栈清空,此时开始处理微任务队列:Promise.resolve(2).then(console.log)的回调已经在PromiseJobs里了,执行它输出2; - 微任务队列清空后,才会处理宏任务队列:
setTimeout的回调是宏任务,执行它输出1。
补充:HTML规范对Job队列选择的定义
ECMA-262里的RunJobs提到“以实现定义的方式选择非空Job队列”,但HTML规范给浏览器的事件循环做了明确的任务处理顺序规则:
- 先执行当前执行栈中的所有同步代码;
- 执行栈清空后,优先处理所有微任务队列(包括
PromiseJobs)的任务,直到微任务队列为空; - 然后从宏任务队列(包括
ScriptJobs、setTimeout、DOM事件回调等)中取出一个任务执行; - 重复上述循环。
简单说就是:微任务永远比宏任务先执行,PromiseJobs属于微任务,ScriptJobs属于宏任务,所以优先级差异很明确。
关于你给出的Job队列执行逻辑的验证
你写的这段逻辑是完全正确的:
ScriptJobs.push(something); // pop and run by Event loop // blow will happens while `something` runs PromiseJobs.push(anotherOne); ScriptJobs.push(theother); //end of `something` //and in here, PromiseJobs will pop?
当something这个ScriptJobs任务执行时,中途把anotherOne加入PromiseJobs、theother加入ScriptJobs:
- 等
something执行完毕,当前执行栈清空,浏览器会先处理所有微任务,也就是PromiseJobs里的anotherOne会被取出执行; - 只有当微任务队列彻底清空后,才会回到宏任务队列,取出
theother这个ScriptJobs任务执行。
内容的提问来源于stack exchange,提问作者ENvironmentSet

