当前位置: 首页 > news >正文

js时间循环机制、浏览器生命周期

事件循环机制:

主线程中存在一个「调用栈」(Call Stack)

function foo() {console.log('foo');setTimeout(() => console.log('foo timeout'));Promise.resolve().then(() => console.log('foo microtask'));
}function bar() {console.log('bar');setTimeout(() => console.log('bar timeout'));Promise.resolve().then(() => console.log('bar microtask'));
}foo();
bar();

🧭 二、执行顺序总览

我们分步骤看整个过程是如何“循环插入”的。

Step 1:调用 foo()

  1. foo 入栈 → 执行同步代码:输出 foo

  2. setTimeout(...) 注册回调(交给 Web API),计时结束后放入 宏任务队列

  3. Promise.then(...) 注册微任务(放入 微任务队列

  4. foo() 执行完毕 → 出栈

此时:

  • 宏任务队列:[foo timeout]

  • 微任务队列:[foo microtask]

Step 2:调用 bar()

  1. bar 入栈 → 执行同步代码:输出 bar

  2. setTimeout(...) 注册宏任务

  3. Promise.then(...) 注册微任务

  4. bar() 出栈

此时:

  • 宏任务队列:[foo timeout, bar timeout]

  • 微任务队列:[foo microtask, bar microtask]

 

Step 3:同步代码执行完毕 → 启动事件循环

事件循环规则:

每一轮循环 = 执行一个宏任务 → 执行所有微任务 → 渲染 → 下一轮宏任务

但注意:执行同步代码本身也算一个宏任务!
也就是说:

整个脚本(<script>)的执行就是第一个宏任务。

因此,在脚本宏任务结束时,会先执行 当前的所有微任务


⚙️ Step 4:执行微任务队列

当前同步代码(脚本)宏任务结束:

  • 执行所有微任务(FIFO 顺序):

输出:

foo microtask
bar microtask

执行完微任务后 → 进入下一个宏任务。

🕐 Step 5:进入宏任务队列(定时器回调)

事件循环取出第一个宏任务:foo timeout

执行 foo timeout 回调:

foo timeout

该回调执行完毕 → 再次执行本轮产生的微任务(如果有的话)。
当前没有新的微任务 → 进入下一个宏任务。

再执行 bar timeout

bar timeout

📤 最终输出顺序

foo
bar
foo microtask
bar microtask
foo timeout
bar timeout

🧠 五、重要补充:每个宏任务内部都可能触发新的微任务

setTimeout(() => {console.log('T1');Promise.resolve().then(() => console.log('T1 micro'));
});setTimeout(() => {console.log('T2');
});

输出顺序:

T1
T1 micro
T2

解释:

  • 执行宏任务 T1

  • 在其中注册了一个微任务 → 紧接执行

  • 然后才进入下一个宏任务 T2

image

 

浏览器生命周期

🧩 一、浏览器每帧的生命周期(事件循环中的“渲染节奏”)

┌───────────────────────────────┐
│   执行宏任务(包括脚本执行)   │
│ ───────────────────────────── │
│   执行微任务(Promise.then等)│
│ ───────────────────────────── │
│   执行渲染前回调(requestAnimationFrame) │
│ ───────────────────────────── │
│   执行样式计算、布局、绘制     │
│ ───────────────────────────── │
│   下一帧开始(目标16.6ms)     │
└───────────────────────────────┘

这意味着浏览器理想状态是:

  • 每一帧(frame)大约 16.6ms;

  • 在这一帧内,JS 执行(宏+微任务)必须在渲染前完成;

  • 如果 JS 执行太久(>16ms),会阻塞渲染


🕐 二、如果宏任务执行太久,会发生什么?

举例:

function heavyTask() {const start = Date.now();while (Date.now() - start < 50) {} // 模拟耗时任务(50ms)
}setInterval(heavyTask, 0);

情况如下:

  1. setInterval 的回调是一个宏任务;

  2. 每个宏任务执行时阻塞主线程;

  3. 浏览器在执行 JS 时 不能渲染帧

  4. 渲染机会被推迟到当前宏任务完全结束之后。

🔍 实际效果:

  • 浏览器原计划在 16.6ms 时渲染下一帧;

  • 但此时主线程仍在执行 heavyTask

  • 所以该帧跳过(skipped frame)

  • 浏览器只好等到 JS 执行完毕后,再进行一次绘制;

  • 这就是你看到的「掉帧」或「卡顿」。

⚙️ 三、那宏任务会“堆积”到下一帧吗?

严格来说:

宏任务不会堆积到下一帧的“同步任务之后”执行,而是阻塞了帧的产生本身

解释:

  • 浏览器不会插入新的渲染帧,直到主线程空闲;

  • 所以 “上一帧没执行完” 的 JS 任务不会被切走,也不会“跨帧”;

  • 它只是拖慢了下一帧的开始时间

也就是说:

宏任务执行完毕 → 浏览器才有机会:

  • 执行微任务

  • 执行 requestAnimationFrame

  • 执行渲染(Reflow/Repaint)

 

🎬 四、帧与事件循环的真实关系(关键理解)

浏览器的 事件循环(event loop)渲染帧(frame loop) 并非严格同步。
渲染是事件循环中的一个阶段:

┌──────────────────────────────────┐
│ 宏任务(执行JS代码)              │
│ → 微任务(Promise.then等)       │
│ → requestAnimationFrame回调      │
│ → 浏览器渲染(layout + paint)   │
│ → 下一个宏任务(或下一帧)        │
└──────────────────────────────────┘

当 JS 代码执行过久时(例如阻塞 100ms),浏览器的渲染阶段就被延后。
下一帧不是「等你执行完再补一次」,而是「直接跳帧」。

 

🧠 五、举个现实例子对比

function sleep(ms) {const t = Date.now();while (Date.now() - t < ms) {}
}function tick() {console.log('frame');sleep(30); // 阻塞 30msrequestAnimationFrame(tick);
}
requestAnimationFrame(tick);

期望:frame 每 ~16ms 打印一次
实际:frame 每 ~30ms 打印一次

🧩 说明:

  • 浏览器原本想 16.6ms 执行下一帧;

  • sleep(30) 占满了主线程;

  • 结果浏览器只能推迟下一帧调度 → 实际帧率降低到 30~33ms 一次。

 

🧭 六、结论总结

结论点 说明
✅ 宏任务不会跨帧执行 它必须执行完才会释放主线程
🚫 不存在“堆积到下一帧再执行”的情况 事件循环和渲染帧不是异步并发,而是串行阶段
⚠️ 宏任务执行过久会阻塞渲染 浏览器无法在执行 JS 时渲染 UI
🎨 requestAnimationFrame 仅在渲染前触发 它不会抢占宏任务
🧩 理想状态是宏任务 + 微任务 ≤ 16ms 否则就会掉帧(jank)

 

🧰 七、补充建议(性能优化角度)

如果你发现“上一帧的任务太重”:

  • ✅ 拆分为多个小任务:

     
    function chunkTask() {// 每帧执行一小段doSmallWork();requestIdleCallback(chunkTask); }
  • ✅ 使用 requestIdleCallbackrequestAnimationFrame 进行节流。

  • ✅ 避免在宏任务中执行密集循环或频繁 DOM 操作。

  • ✅ 大计算任务用 Web Worker 分线程。

http://www.zskr.cn/news/48630.html

相关文章:

  • 2025年成都火锅品牌排行榜出炉,这些网红店你打卡了吗?社区火锅/老火锅/附近火锅/重庆火锅/火锅/成都火锅/牛肉火锅/地摊火锅品牌哪个好
  • docker - 3 存储和网络
  • Esxi许可证,Esxi许可证密钥是什么?
  • 555定时器-3 双稳态多谐振荡器配置
  • 实用指南:【装配式建筑学习感想】
  • 2025年最新原木家具定制产品综合评分榜单,护墙板/卫浴柜/衣柜/木门/实木楼梯/橱柜/酒柜供应商口碑推荐
  • 2025年山西美术馆展示柜厂家十大排行榜:专业选择指南与权威推荐
  • 2025年京津冀地区园林绿化服务商综合测评:民宿景观绿化公司/园区景观绿化/厂区景区绿化/屋顶花园绿化/专业能力、服务范围与特色优势全解析
  • 户外落地式广告机嘉兴今日报价厂家直销
  • 2025年工业大吊扇厂家创新亮点:永磁技术引领行业变革
  • 痞子衡嵌入式:在i.MXRTxxx下使能DMA动态链式传输误区及各外设驱动对DMA链式支持情况
  • 【Linux】Linux进程间通信:命名管道(FIFO)的模拟实现重要知识点梳理 - 实践
  • Akamai 简单 记录
  • Redis分布式锁:从“能用”到“好用”,中间差了多少细节?
  • Zabbix监控mysl数据库配置
  • 嵌入式学习笔记-Chapter4
  • Java 线程同步与线程间通信
  • HarmonyOS ArkTS卡片开发:多种方式实现卡片信息刷新
  • 可视化图解算法68:数组中出现次数超过一半的数字
  • 2025年国内自助入住系统公司TOP5权威推荐:智慧住宿新选择
  • 内蒙古太空菌酸奶厂家,厚乳老酸奶厂家排名,希腊酸奶公司排行榜,奶皮子糖葫芦生产厂家,干咽酸奶厂家,冷萃酸奶源头工厂,口碑推荐!
  • PG系列:并行创建索引
  • bluetooth matlab GFSK 调制解调,误码率统计
  • without updating the macOS to figure out the Markdown import to Mac Note app
  • 统计学第二章
  • MATLAB 对于小目标检测,绘制roc曲线
  • 构建数据安全体系,数据分类分级是核心
  • 破解传统数据安全监测瓶颈,数据安全平台是关键
  • go beego http
  • 新乡LCD拼接屏实用指南:聚焦跨平台能力与售后体系