从 Performance 了解事件循环

开门见题

本文从一道事件循环的题开始,尝试说明事件循环过程。

javascript 复制代码
    <script>
        const button1 = document.getElementById('button1')
        function logHandler() {
            console.log('start')
            setTimeout(() => {
                console.log('st1')
                const innerLog = () => {
                    console.log('inner log')
                }
                Promise.resolve().then(innerLog)
            }, 0);
            const res1Callback1 = (res)=> {
                console.log(res)
                return 'then1'
            }
            const res1Callback2 = (res)=> {
                console.log(res)
            }
            
            Promise.resolve('res1').then(res1Callback1).then(res1Callback2)
            const timerLog = () => {
                console.log('st2')
            }
            setTimeout(timerLog, 0);
        }
        button1.addEventListener('click', logHandler)
    </script>

思考一下,尝试写出答案

Performance 查看任务

Chrome Developer Tools 中的 Performance(性能)面板是一个强大的工具,专门用于分析和调试网页或Web应用程序的性能问题。其中可以查看到主线程任务的执行情况。我们这边仅使用该功能。来看一张 Performance的截图:

上面的代码给按钮绑定了一个 click 事件,执行了 logHandler 事件。查看 Performance 面板的 main 面板(红色箭头指向)。可以看到 logHandler 事件中执行了 4 个子任务。接下来挨个看看其中具体做了哪些事情。

火焰图执行栈

如果你之前没有使用过 Performance, 你可能不清楚,这些大大小小的方块表示什么。 x 轴表示随时间变化的记录。y 轴表示调用堆栈。顶部的事件会导致以下事件。

调用栈:

当函数中抛出错误,可以看到它的完整调用栈,代码最先是从脚本代码段执行,又 main() -> script1() -> script2()抛出错误。

所以可以将火焰图看做一个倒挂的调用堆栈,由于 main() 函数中可以调用多个子函数,所以上面的方块(表示调用方)总是会比下面的长。并且火焰图中展示的是从 JS 引擎线程渲染线程发起的任务。本文中仅考虑 JS的部分,渲染任务简单说明。

Performance.mark

知道了任务执行的流程,但是主线程中的方块比较多,为了方便查看,我们需要在火焰图中标记我们执行的函数,我们可以通过下面代码生成我们想要的标记。

JS 复制代码
    function logHandler() {
        // 已一个标志开始
        performance.mark('start')
        console.log('start')
        const timerLog = () => {
            console.log('st2')
            // 添加函数执行标志
            performance.mark('start--timerLog')
            //测量两个不同的标志。Performance 面板中可以看到一个 'start--timerLog' 调用
            performance.measure('startToTimerLog', 'start', 'start--timerLog');
        }
        setTimeout(timerLog, 0);
    }            

完整标志代码

JS 复制代码
    <script>
        const button1 = document.getElementById('button1')
        function logHandler() {
            performance.mark('start')
            console.log('start')
            setTimeout(() => {
                console.log('st1')
                const innerLog = () => {
                    console.log('inner log')
                    performance.mark('start---inner')
                    // 在 performance 中会添加一个 'start---inner' 函数名
                    performance.measure('measureInner', 'start', 'start---inner');
                }
                Promise.resolve().then(innerLog)
            }, 0);
            const res1Callback1 = (res) => {
                performance.mark('start---res1')
                // 在 performance 中会添加一个 'start---res1' 函数名
                performance.measure('measureRes1', 'start', 'start---res1');

                console.log(res)
                return 'then1'
            }
            const res1Callback2 = (res) => {
                console.log(res)
                performance.mark('start---res2')
                // 在 performance 中会添加一个 'start---res2' 函数名
                performance.measure('measureRes2', 'start', 'start---res2');
                return 'then2'
            }

            Promise.resolve('res').then(res1Callback1).then(res1Callback2)
            const timerLog = () => {
                console.log('st2')
                performance.mark('start--timerLog')
                // 在 performance 中会添加一个 'start--timerLog' 函数名
                performance.measure('startToTimerLog', 'start', 'start--timerLog');
            }
            setTimeout(timerLog, 0);
        }
        button1.addEventListener('click', logHandler)
    </script>

事件循环

上面都是对于 Performance 面板的介绍,终于可以正式开始分析了。我们都知道 js 是单线程。在运行时,js引擎一次只能执行一行代码。

  • 同步代码: 当JavaScript引擎遇到同步代码时,它会立刻执行并阻塞主线程直到完成。
  • 异步代码 :
    • 宏任务(Macrotasks) : 宏任务是一类较为传统的异步任务,它们在事件循环的下一个循环迭代中执行。
    • 微任务(Microtasks) : 微任务是在当前宏任务执行栈清空之后、渲染之前立即执行的任务。相比宏任务,它们拥有更高的执行优先级。

分析代码,查看标志验证

由于事件循环是一轮轮反复把任务推到主线程中执行的过程,接下来就用 第一轮第二轮来描述一个任务的执行过程,一个任务中是包含宏任务和微任务两部分。

当 click 事件触发 logHandler() 函数时,作为第一轮宏任务。
第一轮宏任务

  1. log('start')
  2. 注册第一个setTimeout (注意是注册不是执行)
  3. 注册第二个setTimeout

第一轮微任务

  1. Promise.resolve('res1').then(res1Callback1) res1Callback1 回调执行
  2. res1Callback2 回调执行

第二轮宏任务 第二轮任务在第一个 setTimeout 回调函数中

  1. log('st1')

第二轮微任务

  1. Promise.resolve().then(innerLog) innerLog 回调执行

第三轮宏任务 第三轮任务在第二个 setTimeout 回调函数中

  1. log('st2')

第三轮微任务

验证

第一轮

  • 同步代码执行
  • 注册第一个 setTimeout
  • 注册第二个 setTimeout

任务2跳过 第二轮

  • 触发宏任务,由于 setTimeout 回调是一个匿名监听函数,所以这里展示的是匿名

  • 触发微任务,微任务也是在同一个任务中触发,属于同一个迭代,并且和函数调用同级。

第三轮

  • 触发宏任务

结果

输出结果如下

js 复制代码
start
res
then1
st1
inner log
st2

浏览器中常见的宏任务和微任务

宏任务(Macrotasks)

  • script(整体脚本,即一开始执行的主程序代码)
  • setTimeout
  • setInterval
  • requestAnimationFrame
  • I/O 操作(例如网络请求、文件读写)
  • UI 渲染(浏览器环境中,每一帧的渲染被认为是一个宏任务)

微任务(Microtasks)

  • Promise.then().catch().finally() 回调
  • MutationObserver(用于监控DOM树变化的API)
  • process.nextTick()(Node.js环境特有的,用于在当前事件循环的下一次循环中优先执行回调)
  • Promise.resolve().then() 或者使用原生Promise的微任务队列特性
  • queueMicrotask() API(一个更通用的方式来调度微任务)
相关推荐
再学一点就睡5 小时前
前端网络实战手册:15个高频工作场景全解析
前端·网络协议
C_心欲无痕6 小时前
有限状态机在前端中的应用
前端·状态模式
C_心欲无痕6 小时前
前端基于 IntersectionObserver 更流畅的懒加载实现
前端
candyTong6 小时前
深入解析:AI 智能体(Agent)是如何解决问题的?
前端·agent·ai编程
柳杉6 小时前
建议收藏 | 2026年AI工具封神榜:从Sora到混元3D,生产力彻底爆发
前端·人工智能·后端
weixin_462446236 小时前
使用 Puppeteer 设置 Cookies 并实现自动化分页操作:前端实战教程
运维·前端·自动化
CheungChunChiu6 小时前
Linux 内核动态打印机制详解
android·linux·服务器·前端·ubuntu
Irene19917 小时前
Vue 官方推荐:kebab-case(短横线命名法)
javascript·vue.js
GIS之路7 小时前
GDAL 创建矢量图层的两种方式
前端
2501_948195348 小时前
RN for OpenHarmony英雄联盟助手App实战:符文配置实现
javascript·react native·react.js