从 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(一个更通用的方式来调度微任务)
相关推荐
To_OC3 小时前
LC 128 最长连续序列:别上来就排序,O (n) 解法才是这题的灵魂
javascript·算法·leetcode
IT_陈寒8 小时前
Vue这个坑我跳了两次,原来问题出在这
前端·人工智能·后端
kyriewen8 小时前
我用 50 行代码重写了 React Router 核心,终于搞懂了前端路由原理
前端·javascript·react.js
WebInfra9 小时前
Rspack 2.1 发布:React Compiler 提速 10 倍!
前端
李明卫杭州9 小时前
CSS 媒体查询详解:一文掌握响应式设计的核心技术
前端
lichenyang4539 小时前
从 H5 按钮到 OpenHarmony 能力调用:我如何理解 ASCF 的运行链路
前端
下家10 小时前
我放弃了 Vue/React,选择自研框架
前端·前端框架
Asize10 小时前
HTML5 Canvas 基础:从按帧动画到 ECharts 数据可视化
前端·javascript·canvas
默_笙10 小时前
🎄 后端给我一堆扁平数据,我 10 行代码把它变成了树
前端·javascript
Mahut10 小时前
我用 Electron + FFmpeg 做了一个本地视频处理工作站 ClipForge
前端·ffmpeg·electron