一线大厂高级前端编写,前端初中阶面试题,帮助初学者应聘,需要联系微信:javadudu

揭开事件循环的神秘面纱

揭开事件循环的神秘面纱

作者 | 小萱

导读

这篇文章会全方位讲解事件循环机制,从这篇文章你可以学到,「事件循环」和「浏览器渲染」的关系,浏览器setTimeout、requestAnimationFrame(RAF)、requestIdleCallback(RIC)等API在事件循环的「执行时机」,导致浏览器卡顿的原因、交互指标是如何测量的以及如何提升网站的交互性能。

全文10503字,预计阅读时间27分钟。

01 前言

我们常常会提到页面性能,为什么要优化长任务,又为什么React要做时间切片呢。这篇文章把浏览器的渲染、事件循环与页面性能串联起来。

从这篇文章你可以学到,「事件循环」和「浏览器渲染」的关系,浏览器setTimeout、

requestAnimationFrame(RAF)、requestIdleCallback(RIC)等API在事件循环的「执行时机」,导致浏览器卡顿的原因、交互指标是如何测量的以及如何提升网站的交互性能。

学完这些,你可以对为什么动画要用RAF、又何时去用RIC、该不该选择setTimeout、如何规避长任务之类的问题应对自如。

02 事件循环概述

2.1 为什么要了解事件循环?

深入了解事件循环是性能优化的基础。在讨论事件循环之前,我们需要先了解浏览器的多进程和多线程架构。

2.2 浏览器的架构

回顾浏览器的架构,现代浏览器都是多进程和多线程的。

2.2.1 多进程

Chrome浏览器使用多进程架构,意味着每个标签页(在某些浏览器中也包括每个扩展程序)通常在其自己的进程中运行。这样做的好处是,一个标签页崩溃不会影响到其他标签页。

站点隔离特性,浏览器每个tab,都是独立的渲染进程,这点的好处是假设你打开三个标签页,一个标签卡死不影响其他两个。但如果三个标签共用一个进程,一个卡死会导致全部都卡,这样体验很差。

揭开事件循环的神秘面纱

△浏览器的多进程示意图

2.2.2 多线程

每个浏览器进程都可以包含多个线程。例如,主线程用于执行 JavaScript 代码和处理页面布局,而其他线程可能用于网络请求、渲染等任务。

主线程

Web 应用程序需要在此单个主线程上执行某些关键操作。当您导航到 Web 应用程序时,浏览器将创建并向您的应用程序授予该线程,以便您的代码在其上执行。

主线程指的是渲染进程下的主线程,负责解析HTML、计算CSS样式、执行JavaScript、计算布局、绘制图层等任务。

揭开事件循环的神秘面纱

△主进程即渲染进程包含的线程图

某些任务必须 在主线程上运行。例如,任何直接需要访问 DOM(即 DOM document)的操作都必须在主线程上运行(因为 DOM 不是线程安全的)。这将包括大多数 UI 相关代码。

主线程上一次只能运行 一个任务

此外,一个任务必须在主线程上运行完成,然后才能运行另一个任务。浏览器没有“部分”执行任务的机制,每个任务都完整地运行直至完成。

在下面的示例中,在浏览器展示界面的时候,按顺序运行下面的任务,并且每个任务都在主线程上完成:

揭开事件循环的神秘面纱

03 事件循环的具体流程

我们这里主要讨论的是 window event loop。也就是浏览器一个渲染进程内主线程所控制的 Event Loop。

揭开事件循环的神秘面纱

△发生一次事件循环的具体流程

发生一次事件循环,也就是浏览器一帧中可以用于执行JS的流程如下:

从task queue取出一个task(宏任务)执行并删除 -> 执行并清空队列中全部job(微任务) -> requestAnimationFrame — 浏览器更新渲染 — requestIdleCallback

3.1 更新渲染的步骤

前两个步骤,耳熟能详,这里不再讨论,重点讨论「更新渲染」之后的步骤。

1. Rendering opportunities: 标志是否一次事件循环后会发生渲染。在每次事件循环的结束,不一定会发生渲染。导致不渲染的可能:无法维持当前刷新率、浏览器上下文不可见、浏览器判断更新不会造成视觉改变并且raf的回调为空。

如果这些条件都不满足,当前文档不为空,设置 hasARenderingOpportunity 为 true。

2.如果窗口变化,执行resize。

3.如果滚动,执行scroll。

4.媒体查询。

5.canvas 。

6.执行RAF回掉,传递回掉参数DOMHighResTimeStamp,开始执行回调的时间。

7.重新执行Layout等计算,渲染绘制界面。

8.如果满足 任务队列和微任务队列都为空,并且渲染时机hasARenderingOpportunity为false,执行算法是否执行requestIdleCallback 的回调函数。

3.2 执行顺序与渲染

来一道简单的题目,将创建宏任务、微任务、RIC、RAF的代码同时定义,输出执行顺序。

console.log('开始执行');
console.log('start');
setTimeout(() => {
  console.log('setTimeout');
}, 0);

requestAnimationFrame(() => {
  console.log('requestAnimationFrame');
});
new Promise((resolve, reject) => {
  console.log('Promise');
  resolve('promise resolved');
})

requestIdleCallback(() => {
  console.log('requestIdleCallback');
});

(async function asyncFunction() {
  console.log(await 'asyncFunction');
})();

console.log('执行结束');
// 开始执行
// Promise
// 执行结束
// promise resolved
// asyncFunction
// setTimeout
// requestAnimationFrame
// requestIdleCallback

你可能会疑问为什么RAF会在setTimeout(fn, 0)之前执行,setTimeout(fn, 0)的执行时机是延迟0-4ms,RAF可以粗暴理解为settimeout(fn, Math.random() * 16.6),因此setTimeout会优先。但如果在setTimeout执行之前主线程被其他的任务跑满了,超过了一帧的耗时,setTimeout会在RAF的回调之后执行(用例见下面的代码段),因此setTimeout的延迟时间并不稳定,RAF的执行时机稳定,在一帧内注册的,都会在这一帧的结束,下一帧的开始之前执行。