js事件循环

1. 事件循环(Event Loop)

一些概念

1. 事件循环(Event Loop)

事件循环是浏览器(或类似环境如Node.js)中的一种机制,用于协调异步操作、处理任务队列以及调度各种类型的任务执行。其核心思想是维护一个或多个任务队列(如宏任务队列、微任务队列),按照特定的顺序将待执行的任务推送到主线程中执行。主要流程如下:

  • 宏任务(Macro Task):如setTimeoutsetIntervalI/O操作、UI交互事件(如点击、滚动)、脚本执行等。每个宏任务执行完毕后,检查微任务队列。

  • 微任务(Micro Task):如Promise.resolve/rejectMutationObserverprocess.nextTick(Node.js)等。在一个宏任务执行完毕后,所有微任务会立即执行,直到微任务队列清空,然后返回事件循环等待下一个宏任务。

2. 浏览器渲染(Rendering)

渲染是浏览器将DOM树、CSSOM树结合生成Render树,计算布局(Layout),最终绘制(Paint)到屏幕的过程。为了实现流畅的用户体验,浏览器通常以固定的帧率(如60fps,即每16.67毫秒渲染一次)进行渲染。渲染过程包括:

  • 布局(Layout/Reflow):计算元素尺寸、位置等几何信息。

  • 绘制(Paint):根据布局信息在画布上绘制文本、颜色、图像等。

  • 合成(Composite):对于层叠上下文中不同的图层,进行合成操作以提高渲染效率。

3. 帧动画(Frame Animation)

帧动画是指通过连续改变页面元素属性,利用浏览器的渲染机制来模拟连续动画效果。最常用的API是requestAnimationFrame,它会将回调函数安排在下一次渲染前调用,确保动画与浏览器的重绘同步,从而避免不必要的重排和重绘,提高动画性能。

4. 空闲回调(Idle Callbacks)

空闲回调通过requestIdleCallback API实现,允许开发者在浏览器主进程空闲时(即当前帧的所有工作完成后,且在下一帧开始前)执行低优先级任务。这对于避免阻塞渲染和响应用户交互非常有用,尤其适合处理非紧急但可能消耗大量CPU资源的工作。

5. 关系与协作

  • 事件循环与渲染:浏览器并不保证每一轮事件循环都伴随渲染。渲染是基于帧率的,而事件循环则是持续进行的。当有渲染需求(如DOM或样式发生变化、requestAnimationFrame回调触发等)时,浏览器会在合适的时机(通常是宏任务执行间隙)进行渲染。如果某次事件循环中没有导致视觉变化的操作,或者浏览器判断更新渲染不会带来视觉上的改变,可能会跳过此次渲染。

  • 事件循环与帧动画requestAnimationFrame回调被安排在下一次渲染前执行,它们作为宏任务的一部分,会在微任务之后、下一次渲染之前进入事件循环。这样,开发者可以在回调中更新动画状态,确保动画与浏览器渲染帧同步,避免动画卡顿。

  • 事件循环与空闲回调requestIdleCallback提供的回调会在事件循环中所有高优先级任务(如渲染、用户交互响应、宏任务、微任务)完成后,在浏览器空闲时段内被执行。如果当前帧没有剩余时间或者浏览器上下文不可见(如标签页被切换到后台),空闲回调可能会被延迟到下一个可用的空闲周期。

  • 渲染、帧动画与性能:浏览器会尽可能保持帧率稳定,如果页面性能无法维持60fps,会选择较低的帧率(如30fps)以避免频繁丢帧。同时,如果存在耗时过长的任务(如长时间运行的脚本),可能导致当前帧没有时间进行渲染,从而造成卡顿。合理使用requestAnimationFrame和避免阻塞主线程的任务可以帮助优化动画性能。

综上所述,事件循环渲染帧动画空闲回调在浏览器环境中紧密协作,共同决定了网页的视觉呈现、交互响应和性能表现。理解它们之间的关系和运作机制,是进行高效前端开发和性能优化的关键

先提几个问题

  1. 假如显示器的刷新频率是 60Hz 浏览器也是 60Hz么? 假如是 浏览器会每隔16.67ms (1000ms/60)渲染一次么?

  2. 浏览器中的一帧指的是什么?每一帧都干了那些工作?

  3. 假如有一个js任务执行的时间超过了16.67ms会怎么样?

  4. 事件循环中的队列,都有哪些?

  5. setTimeout(()=>{},1000) 这段代码是什么意思,是否是1s后执行?

  6. requestAnimationFrame requestIdleCallback 这两个api是否知道?

HTML Standard 中的描述 https://html.spec.whatwg.org/multipage/webappapis.html#task-queue

事件循环定义

  1. 事件循环是浏览器(或类似环境如Node.js)中的一种机制,用于协调异步操作、处理任务队列以及调度各种类型的任务执行。其核心思想是维护一个或多个任务队列(如宏任务队列、微任务队列),按照特定的顺序将待执行的任务推送到主线程中执行。

  2. 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.
    为了协调事件、用户交互、脚本、渲染、网络请求 等等,用户代理(即浏览器)必须使用在本节中描述的事件循环。每个代理都有一个关联的事件循环,这对于该代理来说是唯一的。

规范中明确提到了“事件循环”这一术语,并描述了其基本作用:

An event loop is a construct that handles events and callbacks in a web application. Each event loop has one or more task queues.

这里指出事件循环是一种处理web应用程序中事件和回调的构造,每个事件循环都关联有一个或多个任务队列。

2. 任务队列(Task Queues)

规范详细定义了任务队列的概念:

A task queue is an ordered set of tasks. Unless otherwise specified, a user agent must use the first-in-first-out ordering.

任务队列是一个有序的任务集合,除非另有规定,用户代理(即浏览器)应采用先进先出(FIFO)的顺序处理任务。

3. 任务(Tasks)

规范中定义了任务(Task)为事件循环中可执行的基本单位:

A task is a unit of execution that is part of a task queue. Most tasks are callback functions from API calls, events, or other user actions.

任务是构成任务队列的执行单元。大多数任务来源于API调用、事件或其他用户操作的回调函数。

4. 事件循环过程

规范描述了事件循环的基本处理流程:

1.选择一个待处理任务队列:从所有关联的任务队列中选择一个(如果没有可选的任务队列,则进入等待状态)。

2.取出并执行队列中的第一个任务:取出所选队列中的第一个任务并执行。

  1. 重复步骤1和2:直到所选队列为空,然后返回第一步选择下一个任务队列。

5. 微任务(Microtasks)

尽管上述内容主要描述了常规任务队列,规范中还提到了微任务(Microtasks)和微任务队列(Microtask Queue),它们是事件循环中更为精细的调度单位,具有更高的优先级。微任务通常在每个任务(包括事件处理程序、脚本执行等)执行完毕后立即执行,直至微任务队列清空。微任务常用于实现Promise、MutationObserver等API的异步行为。

This specification does not mandate any particular model for selecting rendering opportunities. But for example, if the browser is attempting to achieve a 60Hz refresh rate, then rendering opportunities occur at a maximum of every 60th of a second (about 16.7ms). If the browser finds that a navigable is not able to sustain this rate, it might drop to a more sustainable 30 rendering opportunities per second for that navigable, rather than occasionally dropping frames. Similarly, if a navigable is not visible, the user agent might decide to drop that page to a much slower 4 rendering opportunities per second, or even less.
这个规范没有强制任何特定的模型来选择渲染机会。但是,例如,如果浏览器试图实现60Hz刷新率,那么 渲染机会最多每60秒发生一次(约16.7ms)。如果浏览器发现一个网页(tab)不能维持这个速率,它可能会为那个tab网页下降到更可持续的每秒30次渲染机会,而不是偶尔丢弃帧。同样,如果一个导航不可见,用户代理可能会决定将该页面降到更慢的每秒4次渲染机会,甚至更少。

  • 事件循环与渲染:浏览器并不保证每一轮事件循环都伴随渲染。渲染是基于帧率的,而事件循环则是持续进行的。当有渲染需求(如DOM或样式发生变化、requestAnimationFrame回调触发等)时,浏览器会在合适的时机(通常是宏任务执行间隙)进行渲染。如果某次事件循环中没有导致视觉变化的操作,或者浏览器判断更新渲染不会带来视觉上的改变,可能会跳过此次渲染。

  • 事件循环与帧动画requestAnimationFrame回调被安排在下一次渲染前执行,它们作为宏任务的一部分,会在微任务之后、下一次渲染之前进入事件循环。这样,开发者可以在回调中更新动画状态,确保动画与浏览器渲染帧同步,避免动画卡顿。

  • 事件循环与空闲回调requestIdleCallback提供的回调会在事件循环中所有高优先级任务(如渲染、用户交互响应、宏任务、微任务)完成后,在浏览器空闲时段内被执行。如果当前帧没有剩余时间或者浏览器上下文不可见(如标签页被切换到后台),空闲回调可能会被延迟到下一个可用的空闲周期。

  • 渲染、帧动画与性能:浏览器会尽可能保持帧率稳定,如果页面性能无法维持60fps,会选择较低的帧率(如30fps)以避免频繁丢帧。同时,如果存在耗时过长的任务(如长时间运行的脚本),可能导致当前帧没有时间进行渲染,从而造成卡顿。合理使用requestAnimationFrame和避免阻塞主线程的任务可以帮助优化动画性能。

事件处理与页面更新

  1. 用户交互(如鼠标点击、键盘输入、滚动等)产生事件,这些事件被操作系统传递给浏览器,经由事件队列进入事件循环。

2.事件循环检测到事件队列中有待处理事件时,取出事件并交由渲染引擎的事件处理模块处理。

3.事件处理可能会触发JavaScript脚本执行(如onclick事件的回调函数),此时事件循环暂停渲染引擎的工作,将控制权交给JavaScript引擎(如V8)执行脚本。

4.脚本执行过程中可能修改DOM结构或CSS样式,这些变化被浏览器记录下来,等待后续的渲染更新。

  1. 脚本执行完成后,事件循环恢复对渲染引擎的控制,渲染引擎检查是否有待处理的DOM变更,并根据需要触发回流(layout)和重绘(paint)过程,更新页面内容。

6.更新完成后,渲染引擎通知事件循环工作已完成,事件循环继续处理队列中的下一个事件。

每一帧的时间通常是指浏览器两次渲染之间的时间间隔。 在浏览器渲染过程中,为了形成连续流畅的视觉效果,屏幕内容通常以一定的频率(帧率)进行更新。这一频率通常以每秒的帧数(Frames Per Second, FPS)来衡量,常见的目标帧率为60 FPS,即每一帧的时间间隔大约为16.67毫秒(1秒 ÷ 60帧 ≈ 16.67 ms)。

事件循环的工作原理和执行过程

1.事件循环的目的:事件循环是为了处理用户交互、网络请求、定时器等事件,以及执行与这些事件相关的回调函数。

2.宏任务(Macrotasks)和微任务(Microtasks):事件循环中的任务分为宏任务和微任务两种类型。宏任务包括脚本执行、用户交互事件、定时器等,而微任务则包括 Promise 的回调函数、MutationObserver 的回调函数等。

3.宏任务队列和微任务队列:事件循环维护了一个宏任务队列和一个微任务队列。当事件触发时,相关的任务会被添加到相应的队列中。

4.事件循环的执行过程:事件循环的执行过程是一个循环迭代的过程。在每一次迭代中,事件循环会执行以下步骤:

* 从宏任务队列中取出一个宏任务并执行。

* 执行过程中,如果产生了微任务,将其添加到微任务队列中。

* 执行微任务队列中的所有微任务。

*  

    更新渲染(如果需要)。

*   

    重复以上步骤,直到宏任务队列和微任务队列都为空。
  1. 微任务的优先级:微任务具有比宏任务更高的优先级。也就是说,在每次迭代中,事件循环会首先执行微任务队列中的所有微任务,然后再执行宏任务队列中的下一个宏任务。

  2. 事件循环的终止条件:事件循环会一直执行,直到宏任务队列和微任务队列都为空,或者被终止。

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

每一帧的时间通常是指浏览器两次渲染之间的时间间隔。 在浏览器渲染过程中,为了形成连续流畅的视觉效果,屏幕内容通常以一定的频率(帧率)进行更新。这一频率通常以每秒的帧数(Frames Per Second, FPS)来衡量,常见的目标帧率为60 FPS,即每一帧的时间间隔大约为16.67毫秒(1秒 ÷ 60帧 ≈ 16.67 ms)。

帧的概念与浏览器渲染流程:

  1. 渲染一帧:浏览器渲染一帧的过程包括但不限于以下步骤:

    • 布局计算(Layout/Reflow):计算Render树中元素的几何位置和尺寸。

    • 绘制(Paint):将元素的视觉信息(如颜色、图像、文本)绘制到一块离屏画布上。

    • 合成(Composite):如果有多个图层(Layer),将各层合并成单一的显示列表,并可能利用GPU加速进行合成操作。

    • 提交(Compositor Commit):将最终的显示列表提交给操作系统,由硬件显示设备显示到屏幕上。

  2. 帧间隔:浏览器完成一帧渲染后,会等待下一次垂直同步信号(VSync)到来,标志着新的一帧开始。在这个间隔时间内,浏览器可以处理JavaScript任务、更新DOM/CSSOM、处理用户输入事件、调度网络请求等。然后,浏览器开始准备渲染下一帧,重复上述渲染流程。

帧率与平滑度:

保持稳定的高帧率(如60 FPS)有助于提供平滑、响应迅速的用户体验。如果浏览器不能在每一帧的时间间隔内完成所有渲染工作,导致帧率下降(如低于30 FPS),用户可能会感知到卡顿或掉帧现象。为了达到高帧率,浏览器会尽可能优化渲染流水线,如使用分层渲染、硬件加速、预渲染等技术,同时开发者也需要编写高效的JavaScript代码,避免长时间阻塞主线程。

总结来说,每一帧的时间确实是浏览器两次渲染之间的时间间隔。这个时间间隔受到显示器刷新率、浏览器渲染性能、页面复杂度、JavaScript执行效率等多种因素的影响。浏览器努力在每一帧的时间内完成所有必要的渲染工作,以提供流畅的视觉体验。

只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。(https://ruanyifeng.com/blog/2014/10/event-loop.html

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Event_loop

https://developer.mozilla.org/zh-CN/docs/Web/API/window/requestAnimationFrame

https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback

https://html.spec.whatwg.org/multipage/webappapis.html#event-loops

https://zhuanlan.zhihu.com/p/142742003

https://juejin.cn/post/7323017937752719369

https://juejin.cn/post/6844904006108594190

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/constructing-the-object-model

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335