从 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(一个更通用的方式来调度微任务)
相关推荐
Sammyyyyy3 分钟前
2025年,Javascript后端应该用 Bun、Node.js 还是 Deno?
开发语言·javascript·node.js
timeweaver13 分钟前
深度解析 Nginx 前端 location 配置与优先级:你真的用对了吗?
前端·nginx·前端工程化
鲸落落丶14 分钟前
网络通信---Axios
前端
wwy_frontend15 分钟前
React性能优化实战:从卡顿到丝滑的8个技巧
前端·react.js
小高00730 分钟前
面试官:npm run build 到底干了什么?从 package.json 到 dist 的 7 步拆解
前端·javascript·vue.js
天选打工圣体31 分钟前
个人学习笔记总结(四)抽离elpis并发布npm包
前端
wayhome在哪41 分钟前
用 fabric.js 搞定电子签名拖拽合成图片
javascript·产品·canvas
JayceM2 小时前
Vue中v-show与v-if的区别
前端·javascript·vue.js
HWL56792 小时前
“preinstall“: “npx only-allow pnpm“
运维·服务器·前端·javascript·vue.js
咪咪渝粮2 小时前
JavaScript 中constructor 属性的指向异常问题
开发语言·javascript