• 跟着whatwg看一遍事件循环


    前言


    对于单线程来说,事件循环可以说是重中之重了,它为任务分配不同的优先级,井然有序的调度。让js解析,用户交互,页面渲染等互不冲突,各司其职。


    我们书写的代码无时无刻都在和事件循环打交道,要想写出更流畅,我们就必须深入了解事件循环,下面我们将从规范中翻译和解读整个流程。


    以下内容来自whatwg文档,均为个人理解,若有不对,烦请指出,我会第一时间修改,避免误导他人!


    正文


    为了协调用户操作,js执行,页面渲染,网络请求等事件,每个宿主中,存在事件循环这样的角色,并且该角色在当前宿主中是唯一的。



    简单解释一下宿主:宿主是一个ECMAScript执行上下文,一般包含执行上下文栈,运行时执行环境,宿主记录和一个执行线程,除了这个执行线程外,其他的专属于当前宿主。例如,某些浏览器在不同的tabs使用同一个执行线程。



    不仅如此,事件循环又存于在各个不同场景,有浏览器环境下的,worker环境下的和Worklet环境下的。



    Worklet是一个轻量级的web worker,可以让开发者访问更底层的渲染工作线,也就是说你可以通过Worklet去干预浏览器的渲染环境。



    提到了worklet,那就顺便看一个例子(需开启服务,不要以file协议运行),通过这个例子,可以看到事件循环不同阶段触发了什么钩子函数:


    <!DOCTYPE html>
    <html lang='en'>
    <head>
    <meta charset='UTF-8' />
    <meta name='viewport' content='width=device-width, initial-scale=1.0' />
    <title>Document</title>
    <style>
    .fancy {
    background-image: paint(headerHighlight);
    display: layout(sample-layout);
    background-color: green;
    }
    </style>
    </head>
    <body>
    <h1 class='fancy'>My Cool Header</h1>
    <script>
    console.log('开始');
    CSS.paintWorklet.addModule('./paint.js');
    CSS.layoutWorklet.addModule('./layout.js');

    requestAnimationFrame(() => {
    console.log('requestAnimationFrame');
    });
    Promise.resolve().then(() => {
    console.log('微任务');
    });
    setTimeout(function () {
    document.querySelector('.fancy').style.height = '150px';
    ('translateZ(0)');

    Promise.resolve().then(() => {
    console.log('新一轮的微任务');
    });
    requestAnimationFrame(() => {
    console.log('新一轮的requestAnimationFrame');
    });
    }, 2000);
    console.log(2);
    </script>
    </body>
    </html>


    // paint.js
    registerPaint(
    'headerHighlight',
    class {
    static get contextOptions() {
    console.log('contextOptions');
    return {alpha: true};
    }

    paint(ctx) {
    console.log('paint函数');
    }
    }
    );

    // ==========================分割线

    // layout.js
    registerLayout(
    'sample-layout',
    class {
    async intrinsicSizes(children, edges, styleMap) {}

    async layout(children, edges, constraints, styleMap, breakToken) {
    console.log('layout阶段');
    }
    }
    );


    事件循环有一个或多个Task队列,每个Task队列都是Task的一个集合。其中Task不是指我们的某个函数,而是一个上下文环境,结构如下:



    • step:一系列任务将要执行的步骤

    • source:任务来源,常用来对相关任务进行分组和系列化

    • document:与当前任务相关的document对象,如果是非window环境则为null

    • 环境配置对象:在任务期间追踪记录任务状态



    这里的Task队列不是Task,是一个集合,因为取出一个Task队列中的Task是选择一个可执行的Task,而不是出队操作。




    微任务队列是一个入对出对的队列。



    这里说明一下,Task队列为什么有多个,因为不同的Task队列有不同的优先级,进而进行次序排列和调用,有没有感觉react的fiber和这个有点类似?


    举个例子,Task队列可以是专门负责鼠标和键盘事件的,并且赋予鼠标键盘队列较高的优先级,以便及时响应用户操作。另一个Task队列负责其他任务源。不过也不要饿死任何一个task,这个后续处理模型中会介绍。


    Task封装了负责以下任务的算法:



    • Events: 由专门的Task在特定的EventTarget(一个具有监听订阅模式列表的对象)上分发事件对象

    • Parsing: html解析器标记一个或多个字节,并处理所有生成的结果token

    • Callbacks: 由专门的Task触发回调函数

    • Using a resource: 当该算法获取资源的时候,如果该阶段是以非阻塞方式发生,那么一旦部分或者全部资源可用,则由Task进行后续处理

    • Reacting to DOM manipulation: 通过dom操作触发的任务,例如插入一个节点到document


    事件循环有一个当前运行中的Task,可以为null,如果是null的话,代表着可以接受一个新的Task(新一轮的步骤)。


    事件循环有微任务队列,默认为空,其中的任务由微任务排队算法创建。


    事件循环有一个执行微任务检查点,默认为false,用来防止微任务死循环。


    微任务排队算法:



    1. 如果未提供event loop,设置一个隐式event loop。

    2. 如果未提供document,设置一个隐式document.

    3. 创建一个Task作为新的微任务

    4. 设置setp、source、document到新的Task上

    5. 设置Task的环境配置对象为空集

    6. 添加到event loop的微任务队列中


    微任务检查算法:



    1. 如果微任务检查标志为true,直接return

    2. 设置微任务检查标志为true

    3. 如果微任务队里不为空(也就是说微任务添加的微任务也会在这个循环中出现,直到微任务队列为空):

      1. 从微任务队列中找出最老的任务(防饿死)

      2. 设置当前执行任务为这个最老的任务

      3. 执行

      4. 重置当前执行任务为null


    4. 通知环境配置对象的promise进行reject操作

    5. 清理indexdb事务(不太明白这一步,如果有读者了解,烦请点拨一下)

    6. 设置微任务检查标志为false


    处理模型


    event loop会按照下面这些步骤进行调度:



    1. 找到一个可执行的Task队列,如果没有则跳转到下面的微任务步骤

    2. 让最老的Task作为Task队列中第一个可执行的Task,并将其移除

    3. 将最老的Task作为event loop的可执行Task

    4. 记录任务开始时间点

    5. 执行Task中的setp对应的步骤(上文中Task结构中的step)

    6. 设置event loop的可执行任务为null

    7. 执行微任务检查算法

    8. 设置hasARenderingOpportunity(是否可以渲染的flag)为false

    9. 记住当前时间点

    10. 通过下面步骤记录任务持续时间

      1. 设置顶层浏览器环境为空

      2. 对于每个最老Task的脚本执行环境配置对象,设置当前的顶级浏览器上下文到其上

      3. 报告消耗过长的任务,并附带开始时间,结束时间,顶级浏览器上下文和当前Task


    11. 如果在window环境下,会根据硬件条件决定是否渲染,比如刷新率,页面性能,页面是否在后台,不过渲染会定期出现,避免页面卡顿。值得注意的是,正常的刷新率为60hz,大概是每秒60帧,大约16.7ms每帧,如果当前浏览器环境不支持这个刷新率的话,会自动降为30hz,而不是丢帧。而李兰其在后台的时候,聪明的浏览器会将这个渲染时机降为每秒4帧甚至更低,事件循环也会减少(这就是为什么我们可以用setInterval来判断时候能打开其他app的判断依据的原因)。如果能渲染的话会设置hasARenderingOpportunity为true。



    除此之外,还会在触发resize、scroll、建立媒体查询、运行css动画等,也就是说浏览器几乎大部分用户操作都发生在事件循环中,更具体点是事件循环中的ui render部分。之后会进行requestAnimationFrame和IntersectionObserver的触发,再之后是ui渲染




    1. 如果下面条件都成立,那么执行空闲阶段算法,对于开发者来说就是调用window.requestIdleCallback方法

      1. 在window环境下

      2. event loop中没有活跃的Task

      3. 微任务队列为空

      4. hasARenderingOpportunity为false



    借鉴网上的一张图来粗略表示下整个流程



    小结


    上面就是整个事件循环的流程,浏览器就是按照这个规则一遍遍的执行,而我们要做的就是了解并适应这个规则,让浏览器渲染出性能更高的页面。


    比如:



    1. 非首屏相关性能打点可以放到idle callback中执行,减少对页面性能的损耗

    2. 微任务中递归添加微任务会导致页面卡死,而不是随着事件循环一轮轮的执行

    3. 更新元素布局的最好时机是在requestAnimateFrame中

    4. 尽量避免频繁获取元素布局信息,因为这会触发强制layout(哪些属性会导致强制layout?),影响页面性能

    5. 事件循环有多个任务队列,他们互不冲突,但是用户交互相关的优先级更高

    6. resize、scroll等会伴随事件循环中ui渲染触发,而不是根据我们的滚动触发,换句话说,这些操作自带节流

    7. 等等,欢迎补充


    最后感谢大家阅读,欢迎一起探讨!


    提前祝大家端午节nb


    参考


    composite


    深入探究 eventloop 与浏览器渲染的时序问题

  • 相关阅读:
    tcpdump 抓包
    mysql常用日期函数
    接口自动化测试,一键快速校验接口返回值全部字段
    实训项目
    vue proxyTable 代理方式 解决 跨域pathRewrite
    cookie, session, token 发展史
    Failed to set locale, defaulting to C.UTF-8
    java String与Integer的相互转化
    java LocalDateTime 常用 转换 方法
    Java命令里面的EQ、NE、GT、LT、GE、LE分别代表含义
  • 原文地址:https://www.cnblogs.com/yangzhuxian/p/13371612.html
Copyright © 2020-2023  润新知