技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

JavaScript 中 script 整体代码属于宏任务怎么理解呢?

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/29-javascript中script整体代码属于宏任务怎么理解呢?
本文目录9 个章节
  1. 一、先给结论
  2. 二、历史社区模型中的 macro-task 和 micro-task
  3. 三、script 整体代码是怎样开始执行的
  4. 四、原始示例为什么输出 1、2、3、5、4
  5. 五、浏览器事件循环的几个重要边界
  6. 六、浏览器与 Node.js 不要混用一张表
  7. 七、在控制台输入 console.log() 是宏任务吗
  8. 八、原文中的说法如何保留和修正
  9. 九、参考资料与原文

JavaScript 中 script 整体代码属于宏任务怎么理解呢?

Category(分类): JavaScript Status: 已整理(2026)

原文是一个关于“script 整体代码是不是宏任务”的社区问答。本文保留问题、历史术语和原始示例,同时把“宏任务/微任务”的社区说法改成浏览器规范中的 task、microtask 和脚本处理模型。

一、先给结论

console.log() 这样的同步语句本身不是一个宏任务。它只是当前脚本执行步骤中的一条同步语句。

在浏览器中,宿主环境会通过 HTML Standard 定义的脚本处理算法启动一段经典脚本、模块脚本、事件监听器回调或计时器回调。社区常把“初始 script 整体代码”作为一个 task 来帮助理解,但这不表示:

  • 每条同步语句都是一个 task;
  • 所有脚本都进入同一个叫“宏任务队列”的队列;
  • 脚本一定被规范包装成一个隐藏事件回调;
  • Promise 对象本身是一个微任务;
  • 某个 task 只能执行由它自己创建的 microtask。

更准确的浏览器执行轮廓是:

宿主选择并运行一个 task
  ↓
执行该 task 中的同步 JavaScript,调用栈清空
  ↓
执行 microtask checkpoint,直到微任务队列为空
  ↓
浏览器可能更新渲染
  ↓
宿主选择下一个可运行的 task

HTML Standard 还定义了多个 task source。用户代理可以在不同来源之间选择下一个任务,因此不能把浏览器理解成只有一个严格先进先出的“宏任务总队列”。

二、历史社区模型中的 macro-task 和 micro-task

原文引用的说法是:

任务队列分为 macro-task(宏任务)与 micro-task(微任务),在最新标准中,它们被分别称为 task 与 jobs。

这段话有历史背景,但不能直接当作现行规范定义:

  • HTML Standard 主要讨论 task、task source、task queue 和 microtask queue
  • ECMAScript 使用 Job 抽象描述 Promise reaction 等语言层工作,但浏览器如何调度脚本、计时器和渲染还需要 HTML Standard;
  • setTimeout 的回调属于计时器相关 task;
  • Promise.prototype.then() 注册的 reaction 会在 microtask queue 中运行;
  • queueMicrotask() 直接安排一个微任务;
  • process.nextTick() 是 Node.js API,不是浏览器标准的微任务 API;
  • setImmediate() 主要是 Node.js 等运行时的 API,不应列为浏览器通用宏任务;
  • Object.observe 已被废弃并移除,不应作为现代示例;
  • UI rendering 不是可以在所有浏览器中直接调用或排队的一个统一“宏任务”接口。

所以“宏任务/微任务”可以作为入门简称,但写严谨文章时应注明运行时,并优先使用 task/microtask 的规范术语。

三、script 整体代码是怎样开始执行的

页面加载时,浏览器解析 HTML,并依据脚本元素的类型和属性执行脚本。脚本并不是先被转换成“指令区”,再由 JavaScript 自己把它排入一个宏任务队列;解析器、脚本元素算法、模块加载器和事件循环共同决定执行时机。

3.1 常见脚本类型

脚本形式典型调度特点
经典内联 <script>解析器遇到它时通常会暂停解析并执行;执行期间的同步语句属于当前脚本执行步骤
经典外部 <script src>默认会阻塞解析,下载并执行后继续解析
<script defer src>下载可与解析并行,按文档顺序在解析完成后执行;通常在 DOMContentLoaded 之前完成
<script async src>下载可与解析并行,下载完成后尽快执行;多个 async 脚本不保证文档顺序
<script type="module">模块脚本默认具有类似 defer 的延后执行特征,并按模块依赖图加载
<script type="module" async>模块图准备好后尽快执行,不再保持普通 defer 的顺序语义
动态插入的脚本具体时机由动态脚本、async 设置和加载结果决定,不应与解析器插入脚本混为一谈

“script 整体代码是宏任务”是对常见页面启动阶段的一种简化描述。更精确的说法是:脚本执行由宿主的 HTML 脚本处理算法启动,通常在一个任务或脚本处理步骤中运行;脚本里面的同步代码不会各自拆成一个个 task。

3.2 调用栈与 task 不是一回事

可以把几个层次区分开:

  1. task:宿主调度的一次工作入口,例如计时器回调或事件回调;
  2. 调用栈:该 task 或脚本执行同步代码时,函数调用逐层进入和退出的运行时结构;
  3. microtask:在当前 task 的同步执行完成后,于 microtask checkpoint 中运行的工作,例如 Promise reaction。

调用一个普通函数通常只会增加调用栈,不会自动产生新的 task:

function greet() {
  console.log('函数调用仍在当前同步执行中')
}

greet()

四、原始示例为什么输出 1、2、3、5、4

原文示例:

(function test() {
  setTimeout(function () {
    console.log(4)
  }, 0)

  new Promise(function executor(resolve) {
    console.log(1)
    for (var i = 0; i < 10000; i++) {
      if (i === 9999) resolve()
    }
    console.log(2)
  }).then(function () {
    console.log(5)
  })

  console.log(3)
})()

在常见浏览器环境中,输出为:

1
2
3
5
4

执行过程可以这样描述:

  1. 当前脚本执行同步代码;setTimeout 注册一个未来的计时器 task,但不会立即执行回调;
  2. new Promise 的 executor 是同步调用的,所以先输出 1
  3. 循环结束时调用 resolve(),但 .then() 回调不会同步执行,只会安排一个 Promise reaction microtask;
  4. executor 继续同步输出 2,随后脚本继续输出 3
  5. 当前脚本 task 的同步部分完成,microtask checkpoint 执行 .then() 回调,输出 5
  6. 之后浏览器才会选择计时器 task,输出 4

原文把这个过程比作“宏任务是主人,微任务是奴隶”。这个比喻容易记忆,但不准确:微任务不是只属于创建它的 task,也不能插入当前同步代码中间。microtask checkpoint 会持续清空微任务队列;一个微任务还可以继续安排新的微任务。

更小的示例:

console.log('script start')

setTimeout(() => console.log('timer task'), 0)
queueMicrotask(() => console.log('queueMicrotask'))
Promise.resolve().then(() => console.log('promise reaction'))

console.log('script end')

常见输出顺序是:

script start
script end
queueMicrotask
promise reaction
timer task

setTimeout(..., 0) 表示“计时器达到最早可调度时间后排队”,不表示零毫秒立即执行。实际延迟还会受到当前任务、嵌套计时器规则、后台页面降频和浏览器调度影响。

五、浏览器事件循环的几个重要边界

5.1 微任务会一直清空

在一次 microtask checkpoint 中,队列不一定只执行当时已有的任务。微任务可以继续安排微任务,浏览器会继续处理,直到队列为空;因此无限产生微任务可能阻塞渲染和后续 task:

let count = 0

function scheduleAgain() {
  count += 1
  if (count < 3) queueMicrotask(scheduleAgain)
}

queueMicrotask(scheduleAgain)

不要用递归微任务替代需要让出主线程的长计算;需要分片时可以考虑 setTimeoutMessageChannelscheduler.yield()(按目标浏览器验证)或 Worker。

5.2 渲染不是一个普通的“宏任务”

浏览器会在适当时机进行样式计算、布局、绘制和合成。requestAnimationFrame 回调通常用于下一次绘制前的视觉更新,但不能简单列入一个跨运行时都相同的宏任务清单。后台页面、设备刷新率和浏览器策略都可能改变具体时机。

5.3 Promise executor 与 reaction 不同

下面两部分的时机不同:

const promise = new Promise(resolve => {
  console.log('executor:同步')
  resolve()
})

promise.then(() => {
  console.log('reaction:微任务')
})

console.log('当前脚本继续同步执行')

executor 在创建 Promise 时同步运行;then 注册的 reaction 在当前同步代码完成后的 microtask checkpoint 中运行。

六、浏览器与 Node.js 不要混用一张表

浏览器和 Node.js 都有事件循环,但宿主实现不同:

  • 浏览器的 task、microtask、渲染和 HTML 脚本调度由 Web 平台规范共同定义;
  • Node.js 使用 libuv 阶段处理计时器、I/O、check 等工作,并提供 process.nextTick()
  • Node.js 还会处理 Promise 微任务,但 process.nextTick() 的调度优先级和浏览器没有直接对应物;
  • setImmediate() 在 Node.js 中有明确语义,在浏览器中不能作为通用 API;
  • 在 Node.js 中比较 setTimeout(0)setImmediate() 的输出顺序,还取决于代码处于主模块还是 I/O 回调等上下文。

因此,面试题若没有说明运行环境,不能直接把浏览器和 Node.js 的输出规则混在一起。process.nextTicksetImmediate 和浏览器渲染应单独标注运行时。

七、在控制台输入 console.log() 是宏任务吗

不能简单回答“是”。DevTools 控制台的求值入口是浏览器开发者工具实现的宿主行为,不应直接等同于页面 <script> 标签的初始脚本 task。console.log() 调用本身仍是当前求值中的同步操作:

console.log('这是一条同步调用')

如果控制台代码中安排了异步工作,才分别观察对应机制:

setTimeout(() => console.log('计时器回调'), 0)
Promise.resolve().then(() => console.log('Promise reaction'))

控制台的具体封装、是否复用同一个执行上下文和顶层 await 行为可能随浏览器 DevTools 改变,所以应把控制台当作宿主特定环境,而不是用它定义规范模型。

八、原文中的说法如何保留和修正

原文说“保存起来待执行的回调才能算任务”,可以作为入门直觉,但需要补充:

  • 任务入口不只来自回调函数,也可能由解析器和宿主算法启动;
  • 不是所有函数调用都是任务;
  • Promise reaction 是微任务,不是普通计时器 task;
  • 一个 task 完成后会进行微任务检查点,但微任务不是“主人 task 的附属物”;
  • 事件循环不是简单的“从宏任务队列取一个,再取它创建的微任务”,而是由宿主根据 task source、微任务检查点和渲染机会共同调度。

九、参考资料与原文

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS