前言

对于单线程来说,事件循环可以说是重中之重了,它为任务分配不同的优先级,井然有序的调度。让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 与浏览器渲染的时序问题

跟着whatwg看一遍事件循环的更多相关文章

  1. js事件循环

    之前有看过一些事件循环的博客,不过一阵子没看就发现自己忘光了,所以决定来自己写一个博客总结下! 首先,我们来解释下事件循环是个什么东西: 就我们所知,浏览器的js是单线程的,也就是说,在同一时刻,最多 ...

  2. 有关JavaScript事件循环的若干疑问探究

    起因 即使我完全没有系统学习过JavaScript的事件循环机制,在经过一定时间的经验积累后,也听过一些诸如宏任务和微任务.JavaScript是单线程的.Ajax和Promise是一种异步操作.se ...

  3. Qt 学习之路:线程和事件循环

    前面一章我们简单介绍了如何使用QThread实现线程.现在我们开始详细介绍如何“正确”编写多线程程序.我们这里的大部分内容来自于Qt的一篇Wiki文档,有兴趣的童鞋可以去看原文. 在介绍在以前,我们要 ...

  4. 深入理解 JavaScript 事件循环(一)— event loop

    引言 相信所有学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 .在初期许多人会把异步理解成类似多线程的编程模式 ...

  5. Libevent 事件循环(2)---事件被加入激活队列

    由Libevent 事件循环(1) 在上文中我们提到了libevent 事件循环event_dispatch 的大致过程,以epoll为例,我们看一下事件被如何加入激活队列. //在epoll_dis ...

  6. js事件循环机制辨析

     对于新接触js语言的人来说,最令人困惑的大概就是事件循环机制了.最开始这也困惑了我好久,花了我几个月时间通过书本,打代码,查阅资料不停地渐进地理解他.接下来我想要和大家分享一下,虽然可能有些许错误的 ...

  7. QT中的线程与事件循环理解(1)

    1.需要使用多线程管理的例子 一个进程可以有一个或更多线程同时运行.线程可以看做是“轻量级进程”,进程完全由操作系统管理,线程即可以由操作系统管理,也可以由应用程序管理.Qt 使用QThread 来管 ...

  8. JavaScript 事件循环 — event loop

    引言 相信所有学过 JavaScript 都知道它是一门单线程的语言,这也就意味着 JS 无法进行多线程编程,但是 JS 当中却有着无处不在的异步概念 .在初期许多人会把异步理解成类似多线程的编程模式 ...

  9. Qt 学习之路 2(72):线程和事件循环

    Qt 学习之路 2(72):线程和事件循环 <理解不清晰,不透彻>  --  有需求的话还需要进行专题学习  豆子  2013年11月24日  Qt 学习之路 2  34条评论 前面一章我 ...

随机推荐

  1. JAVASE(十七) 多线程:程序、进程、线程与线程的生命周期、死锁、单例、同步锁

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 1.程序.进程.线程的理解 1.1 概念 程序(program)是为完成特定任务.用某种语言编写的一组指 ...

  2. elasticsearch中保存时间格式

    利用logstash从文档中导入数据到es中,若未事先设定数据格式,有可能存储时间并未保存为date格式而是text格式. 时间若保存为text,则在会以字符串数组格式存储在es中,是乱序,不好查询. ...

  3. Java实现 蓝桥杯VIP 算法提高 插入排序

    算法提高 插入排序 时间限制:1.0s 内存限制:256.0MB  插入排序 问题描述 排序,顾名思义,是将若干个元素按其大小关系排出一个顺序.形式化描述如下:有n个元素a[1],a[2],-,a[ ...

  4. Java实现 LeetCode 26 删除排序数组中的重复项

    26. 删除排序数组中的重复项 给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度. 不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) ...

  5. Java实现 LeetCode 25 K个一组翻转链表

    25. K 个一组翻转链表 给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表. k 是一个正整数,它的值小于或等于链表的长度. 如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持 ...

  6. Java实现 洛谷 P1208 [USACO1.3]混合牛奶 Mixing Milk

    输入输出样例 输入 #1 100 5 5 20 9 40 3 10 8 80 6 30 输出 #1 630 import java.util.Arrays; import java.util.Scan ...

  7. 从linux源码看socket的阻塞和非阻塞

    从linux源码看socket的阻塞和非阻塞 笔者一直觉得如果能知道从应用到框架再到操作系统的每一处代码,是一件Exciting的事情. 大部分高性能网络框架采用的是非阻塞模式.笔者这次就从linux ...

  8. 基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(一)

    系列文章 基于 abp vNext 和 .NET Core 开发博客项目 - 使用 abp cli 搭建项目 基于 abp vNext 和 .NET Core 开发博客项目 - 给项目瘦身,让它跑起来 ...

  9. UniRx精讲(一):UniRx简介&定时功能实现

    1.UniRx 简介 UniRx 是一个 Unity3D 的编程框架.它专注于解决时间上异步的逻辑,使得异步逻辑的实现更加简洁和优雅. 简洁优雅如何体现? 比如,实现一个"只处理第一次鼠标点 ...

  10. https绕过证书认证请求 Get或Post请求(证书过期,忽略证书)

    报错信息 解决: postman方式 java请求 报错信息 javax.net.ssl.SSLHandshakeException: sun.security.validator.Validator ...