进阶篇:事件循环与任务队列

JavaScript 是一门单线程执行的语言,同一时刻只能处理一段代码。这套设计规避了多线程资源竞争、DOM 并发修改冲突等问题,降低了前端开发的心智负担,但也带来代码阻塞风险。

为了解决单线程阻塞的痛点,浏览器实现了异步任务、任务队列、事件循环(Event Loop)整套调度机制,让 JS 具备非阻塞执行能力。定时器、Promise、网络请求、页面渲染的调度,全部建立在这套底层机制之上。

浏览器进程与线程模型

Chrome 五大核心进程

Chrome 使用多进程隔离架构,不同模块运行在独立进程,单个进程崩溃不会造成整个浏览器闪退。

  • 浏览器主进程:浏览器顶层进程,负责地址栏、工具栏、弹窗、本地存储等不属于网页 Tab 的能力,内部包含 UI 线程、存储线程。
  • 渲染进程:每一个网页 Tab 通常对应一个渲染进程,是前端最核心的进程;网页解析、资源加载、JavaScript 执行、页面渲染都在这里完成。
  • 网络进程:统一接管全部网络请求,处理资源下载、请求解析、跨域校验逻辑。
  • 插件进程:浏览器插件独立运行于此进程,隔离插件异常,避免插件故障导致页面崩溃。
  • GPU 进程:负责图形绘制、CSS 3D、动画加速、图层合成,提升页面渲染流畅度。

渲染进程内部子线程

渲染进程内部有多条线程协同工作,JavaScript 执行和页面渲染存在互斥关系:

  • JS 引擎线程(V8):唯一执行 JS 的线程,所有同步代码串行运行。
  • GUI 渲染线程:解析 HTML、CSS,构建 DOM、CSSOM,执行布局、重排、重绘。和 JS 引擎线程互斥;当 JS 引擎线程正在运行代码,GUI 渲染线程会被冻结,长时间运行的 JS 代码直接造成页面卡死。
  • 事件触发线程:监听各类异步事件,当事件触发条件达成,会把回调函数放入任务队列排队。
  • 定时触发器线程:专门处理 setTimeoutsetInterval 的计时,不依赖 JS 主线程;计时到期后,将回调加入任务队列。
  • 异步 HTTP 线程:处理 XHR、Fetch 网络请求;请求状态变更后,将回调推入任务队列等待执行。

JavaScript 运行时基础概念

JS 运行时是 JS 代码的执行载体,由执行上下文、执行上下文栈(调用栈)、任务队列、微任务队列、事件循环共同构成。

执行上下文

所有 JavaScript 代码都运行在执行上下文中;代码开始执行创建上下文,代码执行结束上下文随即销毁。一共分为三类:

  • 全局上下文:页面初始化自动创建,存放全局作用域代码;页面关闭才销毁。
  • 函数上下文:函数被调用时创建,函数执行完毕就销毁。
  • eval 上下文:调用 eval() 动态执行代码时临时创建。

执行上下文栈(调用栈)

执行上下文栈遵循LIFO 后进先出规则,保存当前正在运行的全部执行上下文:

  1. 页面初始化,全局上下文压入栈顶;
  2. 调用函数,生成函数上下文压入栈顶,开始执行函数;
  3. 函数执行完成,上下文从栈弹出,等待垃圾回收;
  4. JS 引擎永远只执行栈顶位置的上下文。

备注:递归函数每一次调用都会新建函数上下文入栈,递归过深会造成调用栈溢出。

任务队列

代码分为同步任务、异步任务:

  • 同步任务:直接在调用栈执行,会阻塞主线程。
  • 异步任务:不会阻塞当前调用栈;等待满足触发条件之后,回调函数进入任务队列排队,遵循先进先出。

任务队列进一步分为:宏任务队列、微任务队列

备注:HTML 标准文档并没有“宏任务”这个术语,该名词属于社区约定,用来和微任务做区分。

事件循环完整执行流程

事件循环(Event Loop)是驱动 JS 异步调度的无限循环机制,规定了同步代码、微任务、动画回调、页面渲染、宏任务之间的执行顺序。

一轮事件循环:从一个宏任务开始,直到下一个宏任务即将开始之前的完整周期。

浏览器标准一轮事件循环执行顺序:

  1. 执行调用栈中全部同步代码,调用栈清空;
  2. 完整清空微任务队列:微任务执行过程中新添加的微任务,也必须在本轮全部执行完毕;
  3. 执行 requestAnimationFrame 动画回调队列;
  4. 浏览器执行 DOM 布局、重排、重绘,完成页面渲染;
  5. 从宏任务队列取出一个宏任务执行;
  6. 开启下一轮事件循环。

宏任务(MacroTask)

常见宏任务

  • Script 顶层全局代码(页面最先执行的宏任务)
  • setTimeoutsetInterval
  • UI 交互事件回调:clickscrollresize
  • 网络回调:Ajax、Fetch 回调
  • postMessage、I/O 读写回调
  • Node.js 特有:setImmediate

宏任务关键特性

  • 一轮事件循环只会执行一个宏任务;
  • 宏任务运行期间不会触发页面渲染,长时间运行会阻塞 UI;
  • 单个宏任务执行完毕后,先处理微任务队列、再处理渲染,之后才开启下一轮循环;
  • 在宏任务内部可以新增微任务,新增微任务在本轮就会执行。

微任务(MicroTask)

微任务执行时机:当前宏任务执行完毕、DOM 渲染之前、下一个宏任务启动之前,优先级高于宏任务。

常见微任务

  • Promise:.then.catch.finally 的回调(Promise 执行器函数同步执行)
  • async / awaitawait 之后的代码包装为微任务
  • MutationObserver DOM 变更监听回调
  • queueMicrotask():浏览器提供手动添加微任务的标准 API
  • Vue 框架:$nextTick
  • Node.js 特有:process.nextTick(Node 内部优先级最高)

微任务关键特性

  • 本轮清空原则:微任务队列必须全部消费完;执行微任务过程中新加入的微任务,依旧在本轮执行;
  • 微任务会阻塞页面渲染以及下一轮宏任务;如果无限循环添加微任务,会造成事件循环卡死;
  • 微任务内部创建的宏任务,进入宏任务队列,要等到下一轮事件循环才会执行。

setTimeout / setInterval 详解

setTimeout():设置定时器,到期之后执行指定函数或者代码片段。 setInterval():以固定时间间隔重复调用回调函数。

setTimeout(function[, delay, arg1, arg2, ...])
setInterval(function[, delay, arg1, arg2, ...])

核心特性

  • setTimeoutsetInterval 共用一套定时器 ID 池clearTimeout()clearInterval() 可以交叉清除定时器,但业务代码建议配对使用。
  • 支持向回调传递额外参数(IE9‑及更早版本不兼容)。
  • 定时器到期之后,回调会作为宏任务,加入宏任务队列排队,不是立刻执行

this 指向陷阱

定时器回调运行在独立全局执行环境,不会继承外层作用域的 this

  • 非严格模式:回调内部 this 指向 window
  • 严格模式:回调内部 this 值为 undefined

定时器特殊延迟规则

  • 嵌套定时器最小 4ms:当定时器嵌套层级达到阈值,浏览器强制设置最小延迟为 4ms,即使传入 delay:0 也不能绕过该限制。
  • 后台标签页节流:处于后台非激活状态的 Tab,浏览器会限制定时器最小延迟,一般为 1000ms,降低 CPU 和电量消耗。
  • 最大延时溢出:浏览器内部用 32 位有符号整数存储延时,超过 2147483647 毫秒(约 24.8 天)发生溢出,定时器几乎立刻触发。

定时器计时不准的原因

setTimeoutsetInterval 无法做到高精度计时,天然存在误差:

  • HTML 规范规定嵌套定时器达到层级后强制保底 4ms;
  • 操作系统底层时间 API 本身存在微小误差;
  • JS 主线程被占用:定时器计时完成,回调进入队列之后,必须等待主线程空闲才可以执行,进一步叠加延迟。

requestAnimationFrame

requestAnimationFrame(rAF) 不属于宏任务,也不属于微任务,浏览器单独维护动画回调队列。

执行优先级顺序:微任务队列全部清空 → 执行 requestAnimationFrame 队列 → 浏览器执行 DOM 渲染

requestAnimationFrame(() => {
    console.log(2)
})
setTimeout(() => {
    console.log(1)
}, 0)
new Promise(resolve => resolve()).then(() => {
    console.log(4)
})
console.log(3)

// 输出结果:3 4 2 1

执行拆解:

  1. 同步代码执行,打印 3
  2. Promise .then 回调加入微任务队列;
  3. setTimeout 回调加入宏任务队列,rAF 回调加入动画队列;
  4. 调用栈清空,执行全部微任务,打印 4
  5. 执行动画队列回调,打印 2
  6. 浏览器执行页面渲染,本轮循环结束;
  7. 下一轮事件循环取出宏任务执行,打印 1

代码案例解析

案例1:基础事件循环顺序

规则:Promise 的执行器函数同步执行;仅 .then / .catch / .finally 属于微任务回调,且必须等状态变更后才入队。

setTimeout(function(){
    console.log(1)
})

new Promise(function(resolve){
    console.log(2)
    resolve()
}).then(function(){
    console.log(3)
})
console.log(4)

// 输出顺序:2 4 3 1

步骤拆解:

  1. 启动全局宏任务,自上而下扫描代码;
  2. setTimeout 回调加入宏任务队列;
  3. Promise 执行器同步执行,打印 2,调用 resolve().then 回调进入微任务队列;
  4. 同步执行 console.log(4),打印 4
  5. 调用栈清空,执行全部微任务,打印 3
  6. 页面渲染完成,本轮结束;
  7. 下一轮事件循环执行宏任务,打印 1

案例2:Promise then 链式执行

ES Promise Jobs 规则要点

  • .then 返回普通值,下一级回调直接加入微任务队列;
  • .then 返回 Promise 对象,不会立刻把后续回调入队,需要先做 unwrap 解析动作;等当前 Job 执行完毕后,才推入 unwrapJob;unwrapJob 执行完毕,才把业务回调加入队列;
  • PromiseJobs 队列先进先出,任务只能追加队尾,不能插队。
Promise.resolve().then(() => {
    return Promise.resolve(1)
})
.then(res => {
    console.log(res)
})
Promise.resolve().then(() => {
    console.log(2)
})
.then(() => {
    console.log(3)
})
.then(() => {
    console.log(4)
})
.then(() => {
    console.log(5)
})

// 输出顺序:2 3 4 1 5

步骤拆解:

  1. 两个 Promise.resolve() 同步完成,两个第一个 .then 回调入队;队列初始状态:[fnA, fnB]
  2. 执行 fnA:返回 Promise.resolve(1);打印 1 的回调不能直接入队,标记 unwrap 待解析。队列变为 [fnB]
  3. 执行 fnB:打印 2;返回普通值,把下一级回调 fnC 入队;fnB 执行结束,推入 unwrapJob。队列变为 [fnC, unwrapJob]
  4. 执行 fnC:打印 3;返回普通值,推入 fnD。队列:[unwrapJob, fnD]
  5. 执行 unwrapJob:解析 Promise,把打印 1 的回调追加到队尾。队列:[fnD, fnPrint1]
  6. 执行 fnD:打印 4;返回普通值,推入 fnE。队列:[fnPrint1, fnE]
  7. 执行 fnPrint1:打印 1。队列:[fnE]
  8. 执行 fnE:打印 5。队列清空。

案例3:Promise 嵌套场景

关键要点:

  • new Promise(executor) 内部代码同步执行;仅 .then 回调进入微任务队列;
  • .then 不写 return,默认返回 undefined,下一级 .then 直接追加队列尾部;
  • 队列严格先进先出;执行过程新增任务只能排到队尾,不能插队;
  • .then 内部新建 Promise,会生成独立子链;外层链式不受内部子链阻塞,多条链的微任务混合排队。
new Promise((resolve) => {
    console.log(1)
    resolve()
})
.then(() => { // t1
    new Promise(resolve => {
        console.log(2)
        resolve()
    })
    .then(() => { // t2
        console.log(3)
    })
    .then(() => { // t4
        console.log(6)
    })
})
.then(() => { // t3
    console.log(4)
    new Promise((resolve) => {
        console.log(5)
        resolve()
    })
    .then(() => { // t5
        console.log(7)
        new Promise((resolve) => {
            console.log(8)
            resolve()
        })
        .then(() => { // t7
            console.log(10)
        })
    })
    .then(() => { // t8
        console.log(11)
    })
})
.then(() => { // t6
    console.log(9)
})

// 输出顺序:1 2 3 4 5 6 7 8 9 10 11

核心理解:一个 .then 一个微任务;当前 .then 完成,才会去执行下一个 .then

步骤拆解:

  1. 同步执行打印 1t1 入队;队列:[ t1 ]
  2. 执行 t1:打印 2;内部第一个 .then(t2)、同级下一个 .then(t3) 入队;队列:[ t2, t3 ]
  3. 执行 t2:打印 3;下一级 t4 入队;队列:[ t3, t4 ]
  4. 执行 t3:打印 4;同步执行内部 Promise,打印 5;内部 t5、同级 t6 入队;队列:[ t4, t5, t6 ]
  5. 执行 t4:打印 6;队列:[ t5, t6 ]
  6. 执行 t5:打印 7;同步执行内部 Promise,打印 8;内部 t7t8 入队;队列:[ t6, t7, t8 ]
  7. 执行 t6:打印 9;队列:[ t7, t8 ]
  8. 执行 t7:打印 10;队列:[ t8 ]
  9. 执行 t8:打印 11;队列清空。

参考资料

MDN 深入:微任务与 JavaScript 运行时环境

MDN 在 JavaScript 中通过 queueMicrotask() 使用微任务

MDN window.setTimeout

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-08-23 17:16:28

results matching ""

    No results matching ""