JavaScript中的事件循环

浏览器的进程模型

现代浏览器为了提高性能,安全性及稳定性,采用了多进程架构,确保不会产生因为某一进程的崩溃而造成的不可用。
1.浏览器主进程
也叫做UI进程,只有一个,负责管理浏览器的UI如标签页,地址栏等。还负责调度各类进程,比如打开很多页面窗口,就是开启了很多个渲染进程。主进程由操作系统直接启动。还有很多其他的职责:

  • 管理标签页
  • 网络请求(DNS,HTTP等)
  • 资源下载管理
  • 提供安全沙箱支持
  • 跨站导航支持
  • 和磁盘,文件系统的交互
  • 与其他进程进行进程间通信(IPC)

2.渲染进程
一般来说,每个页面就对应着一个渲染进程,但也会有同一站点的多个页面共享一个渲染进程的时候,这样可以减少进程的创建和销毁,提高资源的利用率。渲染进程运行在沙箱中,沙箱机制隔离内容进程,防止恶意代码访问本地资源,提高安全性。
3.GPU进程
GPU进程负责加速以下操作:页面滚动、动画、合成,canvas/WebGL绘图,视频播放(解码/播放)。第二个职责是接收来自渲染进程的"图层指令"进行实际的图形绘制。GPU图层是浏览器为了加速页面渲染和动画效果,把某些DOM元素(未来会发生动画变化的元素)单独提取出来在GPU上渲染的一个图层。没有独立显卡也不会影响GPU图层的使用,现代浏览器默认会用系统集成的GPU,即使没有专用GPU浏览器也会用CPU模拟一部分GPU加速功能。GPU进程只有一个。
4.网络进程
网络进程用于隔离网络请求,现代浏览器将其从主进程中分离(可以避免渲染进程直接访问网络,提升安全性),主要处理HTTP/HTTPS请求,资源的缓存与下载,Cookie、代理配置、TLS握手。渲染进程通过IPC请求网络进程获取资源。网络进程只有一个。

阐述一下JS 的事件循环

事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
在Chrome的源码中,它开启一个不会结束的for循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。
过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。
根据W3C官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。

如何理解JS的异步?

JS是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。而渲染主线程承担着诸多的工作,渲染页面、执行JS都在其中运行。在JS代码执行过程中会遇到一些无法立即处理的任务,比如计时完成后需要执行的任务:setTimeoutsetInterval,网络通信完成后需要执行的任务:XHRFetch,用户操作后需要执行的任务:addEventListener,如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象。
所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行。
在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。

事件循环和异步的关系

事件循环是JS实现异步编程的核心机制,w3c标准中叫做event loop,chrome源码实现中被叫做message loop。前面提到过渲染进程中的渲染主线程既要负责解析HTML,解析CSS,计算样式,又要执行全局JS代码等等等等。用一句话来解释它们的关系就是:
单线程是异步产生的原因,事件循环是异步的实现方式
JS是单线程的语言,为了防止阻塞页面,JS通过事件循环来实现异步。在事件循环开始时,渲染主进程会进入一个无限循环,每一次的循环都会检查消息队列中是否有任务存在,如果有,那就取出第一个任务执行,执行完第一个任务后进入下一个循环。如果没有任务了就会进入休眠状态。其他所有的进程(包括其他进程的线程)可以随时向消息队列中添加任务,新任务被添加到队列的末端,如果此时主线程是休眠状态则会唤醒主线程以继续循环处理任务。

多任务的优先级队列

任务是没有优先级的,在消息队列中先进先出,但消息队列是有优先级的。根据W3C的最新解释:事件循环由多个任务队列组成,每种任务类型通常在特定的队列中调度,例如计时器、用户交互、渲染任务等。这些任务队列由浏览器调度,决定任务在事件循环中的执行时机。在一次事件循环中,浏览器可以根据实际情况从不同的队列中取出任务执行。浏览器必须准备好一个微队列,微队列中的任务优先其他任务执行。由于浏览器复杂度的急剧提升,W3C不再使用宏队列的说法。目前在chrome的实现中至少包含了下面的队列:

  • 延时队列:用于存放计时器到达后的回调任务,优先级【中】(setTimeout
  • 交互队列:用于存放用户操作后产生的事件处理任务,优先级【高】
  • 微队列:用户存放需要最快执行的任务,优先级【最高】(Promise.then,queueMicrotask

计时器精准性问题

JS中的计时器能做到精确计时吗?为什么?
答案是不行的,因为

  • 计算机硬件没有原子钟,无法做到精确计时
  • 操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的函数,也就携带了这些偏差
  • 按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过5层,则会带有4毫秒的最少时间,这样在计时时间少于4毫秒时又带来了偏差
  • 受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差

为什么setTimeout嵌套超过五层会有偏差

setTimeout多层嵌套(递归或链式调用)后会出现延时偏差,甚至就算设置setTimeout(fn, 0),也可能不是马上执行。这是由于浏览器出于性能和电池优化考虑对定时器嵌套层级做了限制。
根据W3C标准:当setTimeout被嵌套调用超过 5 层时,浏览器必须将最小超时时间限制为 4ms。
这叫作:Minimum delay clamp (最小延迟钳制)
为什么这么做?
这是为了防止恶意脚本大量设置短时间定时器导致浏览器 CPU 占用过高,页面无法响应用户操作或电池快速消耗

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇