EventLoop 系列 - 跟随规范聊聊浏览器中的事件循环机制

发表于 3年以前  | 总阅读数:345 次

当我了解事件循环时,尝试去找一些规范来学习,但是查遍 EcmaScript 或 V8 发现它们没有这个东西的定义,例如,在 v8 里有的是执行栈、堆这些信息。确实,事件循环不在这里。

后来才逐渐的了解到,当在浏览器环境中,关于事件循环相关定义是在 HTML 标准中,之前 HTML 规范由 whatwg 和 w3c 制定,两个组织都有自己的不同,2019 年时两个组织签署了一项协议 就 HTML 和 DOM 的单一版本进行合作,最终,HTML、DOM 标准最终由 whatwg 维护。

本文的讲解主要也是以 whatwg 标准为主,在 HTML Living Standard Event loops 中,这个规范定义了浏览器内核该如何的去实现它。

浏览器规范中的事件循环

事件循环定义

为了协调事件、用户交互、脚本、渲染、网络等,用户代理必须使用本节描述的事件循环。每个代理有一个关联的事件循环,它对每个代理是唯一的。

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. Each agent has an associated event loop, which is unique to that agent.

从这个定义也可看出,事件循环主要是用来协调事件、网络、JavaScript 等之间的一个运行机制,我们以 JavaScript 为出发点来看下它们之间是如何交互的。

事件循环中有一个重要的概念任务队列,它决定了任务的执行顺序。

事件循环的处理模式

规范 8.1.6.3 处理模型 定义了事件循环的处理模式,当一个事件循环存在,它就会不断的执行以下步骤: 这些概念很晦涩难懂,简单总结下:

  • 执行 Task:任务队列有多个任务源(DOM、UI、网络等)队列,从中至少选出一个可运行的任务,放到 taskQueue 中。

  • 如果没有直接跳到微任务队列,就不会经过 2 ~ 5。

  • 否则从 taskQueue 中取出第一个可执行任务做为 oldestTask 执行,对应 2 ~ 5。

  • 注意,微任务不会在这里被选中,但是当一个任务队列里含有微任务,会将该微任务加入微任务队列。

  • 执行 Microtask:执行微任务队列,直到微任务队列为空,这里如果调度太多的微任务也会导致阻塞。

  • 更新渲染

看到一个图,描述一次事件循环的过程,差不多就是这个意思,主要呢,还是这三个阶段:Task、Microtask、Render 下文会展开的讨论。

图片来源:https://pic2.zhimg.com/80/v2-38e53b9df2d13e9470c31101bb82dbb1_1440w.jpg

Task(Macrotask)

之前也看过很多文章关于事件循环的介绍,**大多会把 “Task” 当作 “Marcotask” 也就是宏任务来介绍,但是在规范中没有所谓的 “Marcotask”,**因为规范里没有这个名词,所以我在这个标题上特意加了个括号,有很多的叫法,也有称为外部队列的,这其实是一个意思,如果你是学习事件循环的新朋友可能就会有疑问,为什么我搜索不到关于这个的解释。

下文我会继续使用规范中的名词 “任务队列” 来表达。

任务队列是一个任务的集合事件循环有一个或多个任务队列,事件循环做的第一步是从选择的队列中获取第一个可运行的任务,而不是出列第一个任务

传统的队列(Queue)是一个先进先出的数据结构,总是排在第一个的先执行,而这里的队列里面会包含一些类似于 setTimeout 这样延迟执行的任务,所以,在规范中有这样一句话:“Task queues are sets, not queues(翻译为任务队列是一个集合,不是队列)”。

任务队列的 任务源 主要包括以下这些:

  • DOM 操作:对 DOM 操作产生的任务,例如,将元素插入文档时以非阻塞方式发生的事情 document.body = aNewBodyElement;
  • 用户交互:用户交互产生的任务,例如鼠标点击、移动产生的 Callback 任务。
  • 网络:网络请求产生的任务,例如 fetch()。
  • 历史遍历:此任务源用于对 history.back() 和类似 API 的调用进行排队。
  • **setTimeout、setInterval:**定时器相关任务。

例如,当 User agent 有一个管理鼠标和键盘事件的任务队列和另一个其它任务源相关的任务队列,在事件循环中相比其它任务,它会多出四分之三的时间来优先执行鼠标和键盘事件的任务队列,这样使得其它任务源的任务队列在能够得到处理的情况下用户交互相关的任务可以得到更高优先级的处理,这也是提高了用户的体验。

Microtask

每个事件循环有一个微任务队列,它不是一个 task queue,两者是独立的队列。

什么是 Microtask(微任务)

微任务是一个简短的函数,当创建该函数的函数执行后,并且 JavaScript 执行上下文栈为空,而控制权尚未交还给事件循环之前触发。

当我们在一个微任务里通过 queueMicrotask(callback) 继续向微任务队列中创建更多的任务,对于事件循环来说,它仍会持续调用微任务直至队列为空。

const log = console.log;
let i = 0;
log('sync run start');
runMicrotask();
log('sync run end');

function runMicrotask() {
  queueMicrotask(() => {
    log("microtask run, i = ", i++);
    if (i > 10) return; 
    runMicrotask();
  });
}

上面这段代码很简单,在主线程调用了 runMicrotask() 函数,该函数内部使用 queueMicrotask() 创建了微任务并且递归调用,微任务的触发是在执行栈为空时才执行,因为里面递归调用每次都会生成新的微任务,事件循环也是在微任务执行完毕才执行 Task Queue 里面的 setTimeout 回调。

sync run start
sync run end
microtask run, i = 0
microtask run, i = 1
microtask run, i = 2
microtask run, i = 3
microtask run, i = 4
microtask run, i = 5
microtask run, i = 6
microtask run, i = 7
microtask run, i = 8
microtask run, i = 9
microtask run, i = 10

通过这个示例,也可看到当调度大量的微任务也会导致和同步任务相同的性能缺陷,后面的任务得不到执行,浏览器的渲染工作也会被阻止。微任务这里的队列才是真正的队列。

创建一个 Microtask(Promise VS queueMicrotask)

在以往我们创建一个微任务很简单,可以创建一个立即 resolve 的 Promise,每次都需要创建一个 Promise 实例,同时也带来了额外的内存开销,另外 Promise 中抛出的错误是一个非标准的 Error,如果未正常捕获通常会得到这样一个错误 UnhandledPromiseRejectionWarning:

使用 Promise 创建一个微任务。

const p = new Promise((resolve, reject) => {
  // reject('err')
  resolve(1);
});
p.then(() => {
  log('Promise microtask.')
});

现在 Window 对象上提供了 queueMicrotask() 方法以一种标准的方式,可以安全的引入微任务,而无需使用额外的技巧,它提供了一种标准的异常。

使用 queueMicrotask() 创建一个微任务。

queueMicrotask(() => {
  log('queueMicrotask.');
});

在我们写业务功能时,一个功能或方法内涉及多个异步调度的任务也是很常见的,基于 Promise 我们很熟悉,还可以使用 Async/Await 以一种同步线性的思维来书写代码。而 queueMicrotask 需要传递一个回调函数,当层级多了很容易出现嵌套。

重点是大多数情况下我们也不需要去创建微任务,过多的滥用也会造成性能问题,也许在做一些类似创建框架或库时可能需要借助微任务来达到某些功能。这里我想到了一个经常问的面试题 “实现一个 Promise” 这个在实现时也许可以采用 queueMicrotask(),在《JavaScript 异步编程》的源码系列,会再看到这个问题。

Microtask 总结

Microtask 总结一句话来讲就是:“它是在当前执行栈尾部下一次事件循环前执行”,需要注意的是,事件循环在处理微任务时,如果微任务队列不为空,就会继续执行微任务,例如,使用递归不停的增加新的微任务,这就很糟糕了。

微任务所包含的任务源没有明确的定义,通常包括这几个:Promise.then()、Object.observe(已废弃)、MutaionObserver、queueMicrotask。

更新渲染

渲染是事件循环中另一个很重要的阶段,这里有一个关于 浏览器工作原理 的讲解很好,整个渲染过程,理解下来主要是下面几个步骤,其中 Layout、 **Paint **这些词在下面的示例还会再次看到。

  • 解析 HTML 文档转化为 DOM Tree,同时也会解析外部 CSS 文件及内嵌的 CSS 样式为 CSSOM Tree。
  • DOM Tree、CSSOM Tree 两者的结合创建出另外一个树结构 Render Tree。
  • Render Tree 完毕之后进入布局(Layout)阶段,为每个节点分配一个在屏幕上的坐标位置。
  • 接下来根据节点坐标位置对整个页面绘制(Paint)。
  • 当我们对 DOM 元素修改之后,例如元素颜色改变、添加 DOM 节点,这时也还会触发布局和重绘(Repaint)。

图片来源:https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/webkitflow.png

结合 Task 与 Microtask 看渲染过程

做一个测试,使用 queueMicrotask 创建一个微任务,在自定义的 runMicrotask() 函数内部递归调用了 10 次,每一次里我都希望来回变换 container 这个 div 的背景色,另外还放置了一个 setTimeout 属于 Task queue 这个是让大家顺便看下 Task queue 在事件循环中的执行顺序。

<div id="container" style="width: 200px; height: 200px; background-color: red; font-size: 100px; color: #fff;">
  0
</div>
<script>
  let i = 0;
  const container = document.getElementById('container');
  setTimeout(() => {});
  runMicrotask();
  function runMicrotask() {
    queueMicrotask(() => {
      if (i > 10) return; 
      container.innerText = i;
      container.style.backgroundColor = i % 2 === 0 ? 'blue' : 'red';
      runMicrotask();
    });
  }
</script>

通过 Chrome 的 Performance 记录,运行过程,首先看下 Frame 只有一个,直接渲染出了最后的结果,如果按照上例,我们可能会觉得应该是在每个微任务执行时都会有一次渲染 blue -> red -> blue -> ...

再看一个更详细的执行过程,可以看到在执行脚步执行后,首先运行的是微任务,对应的是我们代码 runMicrotask() 函数,下图紫色的是 Layout,Paint 是渲染绘制能够看到就是在运行完所有的微任务之后执行的,在之后是下一次事件循环最后执行了 Task Queue Timer。

根据事件循环处理模式规范中的描述,渲染是在一次事件循环的微任务结束之后运行,上例差不多验证了这个结果,这个时候有个疑问:“为什么不是在每一次微任务结束之后执行,当你把 queueMicrotask 替换成 setTimeout 也是一样的,不会在每次事件中都去执行”。

Render 在事件循环中什么时候执行?

规范中还有这样一段描述,得到一个信息是:在每一次的事件循环结束后不一定会执行渲染 每一轮的事件循环如果没有阻塞操作,这个时间是很快的,考虑到硬件刷新频率限制和性能原因的 user agent 节流,浏览器的更新渲染不会在每次事件循环中被触发。如果浏览器试图达到每秒 60Hz 的刷新率,也简称 60fps(60 frame per second),这时绘制一个 Frame 的间隔为 16.67ms(1000/60)。如果在 16ms 内有多次 DOM 操作,也是不会渲染多次的。

如果浏览器无法维持 60fps 就会降低到 30fps、4fps 甚至更低。

如果想在每次事件循环中或微任务之后执行一次绘制,可以通过 requestAnimationFrame 重新渲染。

结合 requestAnimationFrame 再看渲染过程

requestAnimationFrame 是浏览器 window 对象下提供的一个 API,它的应用场景是告诉浏览器,我需要运行一个动画。该方法会要求浏览器在下次重绘之前调用指定的回调函数更新动画。

修改上述示例,加上 requestAnimationFrame() 方法。

function runMicrotask() {
    queueMicrotask(() => {
      requestAnimationFrame(() => {
        if (i > 10) return; 
        container.innerText = i;
        container.style.backgroundColor = i % 2 === 0 ? 'blue' : 'red';
        i++;
        runMicrotask();
      });
    });
  }

运行之后如下所示,每一次的元素改变都得到了重新绘制。 放大其中一个看看任务的执行情况,requestAnimationFrame 也可以看作一个任务,可以看到它在运行之后执行微任务。

Render 总结

事件循环中 Render 阶段可能在一次事件循环中运行,也可能在多次事件循环后运行。它会受到浏览器的刷新频率影响,如果是 60fps 那就是每间隔 16.67ms 执行一次,另一方面当浏览器认为更新渲染对用户没有影响的情况下,也会认为这不是一次必要的渲染。

总的来说它的机制和浏览器是相关的,了解即可,不用特别的纠结。

总结

浏览器中事件循环主要由 Task、Microtask、Render 三个阶段组成,Task、Microtask 是我们会用到的比较多的,无论是网络请求、还是 DOM 操作、Promise 这些大致都划分为这两类任务,每一轮的事件循环都会检查这两个任务队列里是否有要执行的任务,等 JavaScript 上下文栈空后,先情况微任务队列里的所有任务,之后在执行宏任务,而 Render 则不是必须的,它受浏览器的一些因素影响,并不一定在每次事件循环中执行。

Reference

  • https://yu-jack.github.io/2020/02/03/javascript-runtime-event-loop-browser/
  • https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/
  • https://html.spec.whatwg.org/multipage/webappapis.html#event-loops
  • https://zhuanlan.zhihu.com/p/34229323

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/oNpZL7DlFobDtNKD7N8D5w

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237287次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8126次阅读
 目录