从 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(一个更通用的方式来调度微任务)
相关推荐
开发者小天几秒前
react中的useDebounceEffect用法
前端·react.js·前端框架
想自律的露西西★1 分钟前
js.39. 组合总和
前端·javascript·数据结构·算法
ttod_qzstudio5 分钟前
事件冒泡踩坑记:一个TDesign Checkbox引发的思考
前端·javascript·vue.js·tdesign
IT_陈寒7 分钟前
Vue3性能优化实战:这7个技巧让我的应用加载速度提升40%
前端·人工智能·后端
Reuuse7 分钟前
登录突然失效:Axios 拦截器判空、localStorage 脏数据与环境变量踩坑
开发语言·前端
早川不爱吃香菜7 分钟前
MCP 教程:将 Figma 设计稿转化为前端代码
前端·figma
修炼前端秘籍的小帅9 分钟前
PinMe——极简、免费和无需服务器的开源前端部署工具
前端
嘿siri10 分钟前
自定义app端、小程序端和H5等多端自定义键盘输入框,跟随系统键盘弹出和隐藏
javascript·小程序·uni-app·uniapp
阿蒙Amon10 分钟前
JavaScript学习笔记:18.继承与原型链
javascript·笔记·学习
XXYBMOOO12 分钟前
基于 HTML5 Canvas 的终端日志流可视化实现(支持多 Pane / 运维模式)
运维·前端·html5