从 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(一个更通用的方式来调度微任务)
相关推荐
FuckPatience5 分钟前
Halcon 寻找方形Mark
前端·javascript·数据库
小陈工6 分钟前
Python Web开发入门(八):用户认证系统实现,给你的应用加上安全锁
开发语言·前端·数据库·python·安全·django·sqlite
NocoBase23 分钟前
本周更新|事件流添加 JS 变量
javascript·人工智能·低代码·开源·无代码
weixin1997010801637 分钟前
《XMZ 商品详情页前端性能优化实战》
前端·性能优化
ThridTianFuStreet小貂蝉1 小时前
面试题2、讲一讲JS运行机制、微任务、宏任务、事件循环
javascript
happymaker06261 小时前
vue中对list的函数处理方式总结,以及常见功能的实现方法
javascript·vue.js·list
还是大剑师兰特1 小时前
vueup/vue-quill 详细介绍(Vue3 富文本编辑器)
javascript·vue.js·ecmascript
蜡台1 小时前
Uniapp H5Builderx 预览Html 显示404问题解决
前端·uni-app
threelab2 小时前
引擎案例分析 02|GeoLayer 大厂地理可视化方案深度拆解
javascript·3d·webgl