<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Browser - 标签 - 菠菜眾長</title><link>https://lruihao.cn/tags/browser/</link><description>Browser - 标签 - 菠菜眾長</description><generator>Hugo -- gohugo.io</generator><language>zh-CN</language><managingEditor>1024@lruihao.cn (Lruihao)</managingEditor><webMaster>1024@lruihao.cn (Lruihao)</webMaster><lastBuildDate>Sun, 05 Feb 2023 21:02:52 +0800</lastBuildDate><atom:link href="https://lruihao.cn/tags/browser/" rel="self" type="application/rss+xml"/><item><title>浏览器渲染原理</title><link>https://lruihao.cn/posts/browser-rendering/</link><pubDate>Sun, 05 Feb 2023 21:02:52 +0800</pubDate><author>Lruihao</author><guid>https://lruihao.cn/posts/browser-rendering/</guid><description><![CDATA[<iframe src="files/browser-rendering.pdf" width="100%" height="550"></iframe>
<h2 id="浏览器是如何渲染页面的">浏览器是如何渲染页面的？</h2>
<p>当浏览器的网络线程收到 HTML 文档后，会产生一个渲染任务，并将其传递给渲染主线程的消息队列。</p>
<p>在事件循环机制的作用下，渲染主线程取出消息队列中的渲染任务，开启渲染流程。</p>
<hr>
<p>整个渲染流程分为多个阶段，分别是： HTML 解析、样式计算、布局、分层、绘制、分块、光栅化、画</p>
<p>每个阶段都有明确的输入输出，上一个阶段的输出会成为下一个阶段的输入。</p>
<p>这样，整个渲染流程就形成了一套组织严密的生产流水线。</p>
<hr>
<p>渲染的第一步是<strong>解析 HTML</strong>。</p>
<p>解析过程中遇到 CSS 解析 CSS，遇到 JS 执行 JS。为了提高解析效率，浏览器在开始解析前，会启动一个预解析的线程，率先下载 HTML 中的外部 CSS 文件和 外部的 JS 文件。</p>
<p>如果主线程解析到<code>link</code>位置，此时外部的 CSS 文件还没有下载解析好，主线程不会等待，继续解析后续的 HTML。这是因为下载和解析 CSS 的工作是在预解析线程中进行的。这就是 CSS 不会阻塞 HTML 解析的根本原因。</p>
<p>如果主线程解析到<code>script</code>位置，会停止解析 HTML，转而等待 JS 文件下载好，并将全局代码解析执行完成后，才能继续解析 HTML。这是因为 JS 代码的执行过程可能会修改当前的 DOM 树，所以 DOM 树的生成必须暂停。这就是 JS 会阻塞 HTML 解析的根本原因。</p>
<p>第一步完成后，会得到 DOM 树和 CSSOM 树，浏览器的默认样式、内部样式、外部样式、行内样式均会包含在 CSSOM 树中。</p>
<hr>
<p>渲染的下一步是<strong>样式计算</strong>。</p>
<p>主线程会遍历得到的 DOM 树，依次为树中的每个节点计算出它最终的样式，称之为 Computed Style。</p>
<p>在这一过程中，很多预设值会变成绝对值，比如<code>red</code>会变成<code>rgb(255,0,0)</code>；相对单位会变成绝对单位，比如<code>em</code>会变成<code>px</code></p>
<p>这一步完成后，会得到一棵带有样式的 DOM 树。</p>
<hr>
<p>接下来是<strong>布局</strong>，布局完成后会得到布局树。</p>
<p>布局阶段会依次遍历 DOM 树的每一个节点，计算每个节点的几何信息。例如节点的宽高、相对包含块的位置。</p>
<p>大部分时候，DOM 树和布局树并非一一对应。</p>
<p>比如<code>display:none</code>的节点没有几何信息，因此不会生成到布局树；又比如使用了伪元素选择器，虽然 DOM 树中不存在这些伪元素节点，但它们拥有几何信息，所以会生成到布局树中。还有匿名行盒、匿名块盒等等都会导致 DOM 树和布局树无法一一对应。</p>
<hr>
<p>下一步是<strong>分层</strong></p>
<p>主线程会使用一套复杂的策略对整个布局树中进行分层。</p>
<p>分层的好处在于，将来某一个层改变后，仅会对该层进行后续处理，从而提升效率。</p>
<p>滚动条、堆叠上下文、transform、opacity 等样式都会或多或少的影响分层结果，也可以通过<code>will-change</code>属性更大程度的影响分层结果。</p>
<hr>
<p>再下一步是<strong>绘制</strong></p>
<p>主线程会为每个层单独产生绘制指令集，用于描述这一层的内容该如何画出来。</p>
<hr>
<p>完成绘制后，主线程将每个图层的绘制信息提交给合成线程，剩余工作将由合成线程完成。</p>
<p>合成线程首先对每个图层进行分块，将其划分为更多的小区域。</p>
<p>它会从线程池中拿取多个线程来完成分块工作。</p>
<hr>
<p>分块完成后，进入<strong>光栅化</strong>阶段。</p>
<p>合成线程会将块信息交给 GPU 进程，以极高的速度完成光栅化。</p>
<p>GPU 进程会开启多个线程来完成光栅化，并且优先处理靠近视口区域的块。</p>
<p>光栅化的结果，就是一块一块的位图</p>
<hr>
<p>最后一个阶段就是<strong>画</strong>了</p>
<p>合成线程拿到每个层、每个块的位图后，生成一个个「指引（quad）」信息。</p>
<p>指引会标识出每个位图应该画到屏幕的哪个位置，以及会考虑到旋转、缩放等变形。</p>
<p>变形发生在合成线程，与渲染主线程无关，这就是<code>transform</code>效率高的本质原因。</p>
<p>合成线程会把 quad 提交给 GPU 进程，由 GPU 进程产生系统调用，提交给 GPU 硬件，完成最终的屏幕成像。</p>
<h2 id="什么是-reflow">什么是 reflow？</h2>
<p>reflow 的本质就是重新计算 layout 树。</p>
<p>当进行了会影响布局树的操作后，需要重新计算布局树，会引发 layout。</p>
<p>为了避免连续的多次操作导致布局树反复计算，浏览器会合并这些操作，当 JS 代码全部完成后再进行统一计算。所以，改动属性造成的 reflow 是异步完成的。</p>
<p>也同样因为如此，当 JS 获取布局属性时，就可能造成无法获取到最新的布局信息。</p>
<p>浏览器在反复权衡下，最终决定获取属性立即 reflow。</p>
<h2 id="什么是-repaint">什么是 repaint？</h2>
<p>repaint 的本质就是重新根据分层信息计算了绘制指令。</p>
<p>当改动了可见样式后，就需要重新计算，会引发 repaint。</p>
<p>由于元素的布局信息也属于可见样式，所以 reflow 一定会引起 repaint。</p>
<h2 id="为什么-transform-的效率高">为什么 transform 的效率高？</h2>
<p>因为 transform 既不会影响布局也不会影响绘制指令，它影响的只是渲染流程的最后一个「draw」阶段</p>
<p>由于 draw 阶段在合成线程中，所以 transform 的变化几乎不会影响渲染主线程。反之，渲染主线程无论如何忙碌，也不会影响 transform 的变化。</p>
]]></description></item><item><title>浏览器原理 - 事件循环</title><link>https://lruihao.cn/posts/event-loop/</link><pubDate>Wed, 11 Jan 2023 11:16:02 +0800</pubDate><author>Lruihao</author><guid>https://lruihao.cn/posts/event-loop/</guid><description><![CDATA[<div class="featured-image">
        <img src="/images/202208092131410.png" referrerpolicy="no-referrer">
      </div><p>最近在抖音上刷到很多次 <a href="http://yuanjin.tech/"target="_blank" rel="external nofollow noopener noreferrer">袁进老师<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> 的前端视频，然后就听了一下他的前端大师课，感觉了解一些浏览器原理后，原来工作中的一些疑问也自然解开了。</p>
<h2 id="浏览器的进程模型">浏览器的进程模型</h2>
<h3 id="何为进程">何为进程？</h3>
<p>程序运行需要有它自己专属的内存空间，可以把这块内存空间简单的理解为进程</p>
<p><figure><a class="lightgallery" href="/posts/event-loop/images/202208092057573.png" data-thumbnail="/posts/event-loop/images/202208092057573.png" data-sub-html="<h2>进程</h2><p>进程可以简单理解为单独的内存空间</p>"></a><figcaption class="image-caption">进程</figcaption>
  </figure></p>
<p>每个应用至少有一个进程，进程之间相互独立，即使要通信，也需要双方同意。</p>
<h3 id="何为线程">何为线程？</h3>
<p>有了进程后，就可以运行程序的代码了。</p>
<p>运行代码的「人」称之为「线程」。</p>
<p>一个进程至少有一个线程，所以在进程开启后会自动创建一个线程来运行代码，该线程称之为主线程。</p>
<p>如果程序需要同时执行多块代码，主线程就会启动更多的线程来执行代码，所以一个进程中可以包含多个线程。</p>
<p><figure><a class="lightgallery" href="/posts/event-loop/images/202208092108499.png" data-thumbnail="/posts/event-loop/images/202208092108499.png" data-sub-html="<h2>线程</h2><p>一个进程可以有 N 个线程</p>"></a><figcaption class="image-caption">线程</figcaption>
  </figure></p>
<h3 id="浏览器有哪些进程和线程">浏览器有哪些进程和线程？</h3>
<p><strong>浏览器是一个多进程多线程的应用程序</strong>，浏览器内部工作极其复杂。</p>
<p>为了避免相互影响，为了减少连环崩溃的几率，当启动浏览器后，它会自动启动多个进程。</p>
<p><figure><a class="lightgallery" href="/posts/event-loop/images/202208092131410.png" data-thumbnail="/posts/event-loop/images/202208092131410.png" data-sub-html="<h2>浏览器进程</h2><p>浏览器进程</p>"></a><figcaption class="image-caption">浏览器进程</figcaption>
  </figure></p>
<blockquote>
<p>可以在浏览器的任务管理器中查看当前的所有进程</p>
</blockquote>
<p>其中，最主要的进程有：</p>
<ol>
<li>
<p>浏览器进程</p>
<p>主要负责界面显示、用户交互、子进程管理等。浏览器进程内部会启动多个线程处理不同的任务。</p>
</li>
<li>
<p>网络进程</p>
<p>负责加载网络资源。网络进程内部会启动多个线程来处理不同的网络任务。</p>
</li>
<li>
<p><strong>渲染进程</strong>（本篇重点讲解的进程）</p>
<p>渲染进程启动后，会开启一个<strong>渲染主线程</strong>，主线程负责执行 HTML、CSS、JS 代码。</p>
<p>默认情况下，浏览器会为每个标签页开启一个新的渲染进程，以保证不同的标签页之间不相互影响。</p>
<blockquote>
<p>将来该默认模式可能会有所改变，有兴趣的同学可参见 <a href="https://chromium.googlesource.com/chromium/src/&#43;/main/docs/process_model_and_site_isolation.md#Modes-and-Availability"target="_blank" rel="external nofollow noopener noreferrer">chrome 官方说明文档<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></p>
</blockquote>
</li>
</ol>
<h2 id="渲染主线程是如何工作的">渲染主线程是如何工作的？</h2>
<p>渲染主线程是浏览器中最繁忙的线程，需要它处理的任务包括但不限于：</p>
<ul>
<li>解析 HTML</li>
<li>解析 CSS</li>
<li>计算样式</li>
<li>布局</li>
<li>处理图层</li>
<li>每秒把页面画 60 次</li>
<li>执行全局 JS 代码</li>
<li>执行事件处理函数</li>
<li>执行计时器的回调函数</li>
<li>&hellip;&hellip;</li>
</ul>
<blockquote>
<p>思考题：为什么渲染进程不适用多个线程来处理这些事情？</p>
</blockquote>
<p>要处理这么多的任务，主线程遇到了一个前所未有的难题：如何调度任务？</p>
<p>比如：</p>
<ul>
<li>我正在执行一个 JS 函数，执行到一半的时候用户点击了按钮，我该立即去执行点击事件的处理函数吗？</li>
<li>我正在执行一个 JS 函数，执行到一半的时候某个计时器到达了时间，我该立即去执行它的回调吗？</li>
<li>浏览器进程通知我“用户点击了按钮”，与此同时，某个计时器也到达了时间，我应该处理哪一个呢？</li>
<li>&hellip;&hellip;</li>
</ul>
<p>渲染主线程想出了一个绝妙的主意来处理这个问题：排队</p>
<p><figure><a class="lightgallery" href="/posts/event-loop/images/202208092230847.png" data-thumbnail="/posts/event-loop/images/202208092230847.png" data-sub-html="<h2>消息队列</h2><p>所有要执行的任务都要排队，FIFO</p>"></a><figcaption class="image-caption">消息队列</figcaption>
  </figure></p>
<ol>
<li>在最开始的时候，渲染主线程会进入一个无限循环</li>
<li>每一次循环会检查消息队列中是否有任务存在。如果有，就取出第一个任务执行，执行完一个后进入下一次循环；如果没有，则进入休眠状态。</li>
<li>其他所有线程（包括其他进程的线程）可以随时向消息队列添加任务。新任务会加到消息队列的末尾。在添加新任务时，如果主线程是休眠状态，则会将其唤醒以继续循环拿取任务</li>
</ol>
<p>这样一来，就可以让每个任务有条不紊的、持续的进行下去了。<strong>整个过程，被称之为事件循环（消息循环）</strong>。</p>
<h2 id="若干解释">若干解释</h2>
<h3 id="何为异步">何为异步？</h3>
<p>代码在执行过程中，会遇到一些无法立即处理的任务，比如：</p>
<ul>
<li>计时完成后需要执行的任务 —— <code>setTimeout</code>、<code>setInterval</code></li>
<li>网络通信完成后需要执行的任务 &ndash; <code>XHR</code>、<code>Fetch</code></li>
<li>用户操作后需要执行的任务 &ndash; <code>addEventListener</code></li>
</ul>
<p>如果让渲染主线程等待这些任务的时机达到，就会导致主线程长期处于「阻塞」的状态，从而导致浏览器「卡死」</p>
<p><figure><a class="lightgallery" href="/posts/event-loop/images/202208101043348.png" data-thumbnail="/posts/event-loop/images/202208101043348.png" data-sub-html="<h2>同步策略</h2><p>同步策略会导致卡死</p>"></a><figcaption class="image-caption">同步策略</figcaption>
  </figure></p>
<p><strong>渲染主线程承担着极其重要的工作，无论如何都不能阻塞！</strong></p>
<p>因此，浏览器选择<strong>异步</strong>来解决这个问题</p>
<p><figure><a class="lightgallery" href="/posts/event-loop/images/202208101048899.png" data-thumbnail="/posts/event-loop/images/202208101048899.png" data-sub-html="<h2>异步策略</h2><p>异步策略，渲染主线程永不阻塞</p>"></a><figcaption class="image-caption">异步策略</figcaption>
  </figure></p>
<p>使用异步的方式，<strong>渲染主线程永不阻塞</strong></p>
<div class="details admonition question">
    <div class="details-summary admonition-title">
      <i class="icon fa-solid fa-question-circle fa-fw" aria-hidden="true"></i>面试题：如何理解 JS 的异步？<i class="details-icon fa-solid fa-angle-right fa-fw" aria-hidden="true"></i>
    </div>
    <div class="details-content">
      <div class="admonition-content"><p>参考答案：</p>
<p>JS 是一门单线程的语言，这是因为它运行在浏览器的渲染主线程中，而渲染主线程只有一个。</p>
<p>而渲染主线程承担着诸多的工作，渲染页面、执行 JS 都在其中运行。</p>
<p>如果使用同步的方式，就极有可能导致主线程产生阻塞，从而导致消息队列中的很多其他任务无法得到执行。这样一来，一方面会导致繁忙的主线程白白的消耗时间，另一方面导致页面无法及时更新，给用户造成卡死现象。</p>
<p>所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时，比如计时器、网络、事件监听，主线程将任务交给其他线程去处理，自身立即结束任务的执行，转而执行后续代码。当其他线程完成时，将事先传递的回调函数包装成任务，加入到消息队列的末尾排队，等待主线程调度执行。</p>
<p>在这种异步模式下，浏览器永不阻塞，从而最大限度的保证了单线程的流畅运行。</p>
</div>
    </div>
  </div>
<h3 id="js-为何会阻碍渲染">JS 为何会阻碍渲染？</h3>
<p>先看代码</p>
<div class="highlight" id="id-1"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Mr.Yuan is awesome!<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">button</span><span class="p">&gt;</span>change<span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="kd">var</span> <span class="nx">h1</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;h1&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="kd">var</span> <span class="nx">btn</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;button&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// 死循环指定的时间
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="kd">function</span> <span class="nx">delay</span><span class="p">(</span><span class="nx">duration</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">var</span> <span class="nx">start</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">-</span> <span class="nx">start</span> <span class="o">&lt;</span> <span class="nx">duration</span><span class="p">)</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">btn</span><span class="p">.</span><span class="nx">onclick</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">h1</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s1">&#39;袁老师很帅！&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">delay</span><span class="p">(</span><span class="mi">3000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span></span></span></code></pre></td></tr></table>
</div>
</div><p>点击按钮后，会发生什么呢？</p>
<p>&lt;复制代码自行演示&gt;</p>
<h3 id="任务有优先级吗">任务有优先级吗？</h3>
<p>任务没有优先级，在消息队列中先进先出</p>
<p>但<strong>消息队列是有优先级的</strong></p>
<p>根据 W3C 的最新解释：</p>
<ul>
<li>每个任务都有一个任务类型，同一个类型的任务必须在一个队列，不同类型的任务可以分属于不同的队列。
在一次事件循环中，浏览器可以根据实际情况从不同的队列中取出任务执行。</li>
<li>浏览器必须准备好一个微队列，微队列中的任务优先所有其他任务执行
<a href="https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint"target="_blank" rel="external nofollow noopener noreferrer">https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></li>
</ul>
<blockquote>
<p>随着浏览器的复杂度急剧提升，W3C 不再使用宏队列的说法</p>
</blockquote>
<p>在目前 chrome 的实现中，至少包含了下面的队列：</p>
<ul>
<li>微队列：用户存放需要最快执行的任务，优先级「最高」</li>
<li>延时队列：用于存放计时器到达后的回调任务，优先级「中」</li>
<li>交互队列：用于存放用户操作后产生的事件处理任务，优先级「高」</li>
</ul>
<p><figure><a class="lightgallery" href="/posts/event-loop/images/multiple-queues.png" data-thumbnail="/posts/event-loop/images/multiple-queues.png" data-sub-html="<h2>多队列演示图</h2><p>微队列、延时队列、交互队列</p>"></a><figcaption class="image-caption">多队列演示图</figcaption>
  </figure></p>
<p>添加任务到微队列的主要方式主要是使用 Promise、MutationObserver，例如：</p>
<div class="highlight" id="id-2"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="c1">// 立即把一个函数添加到微队列
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nb">Promise</span><span class="p">.</span><span class="nx">resolve</span><span class="p">().</span><span class="nx">then</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="p">{});</span></span></span></code></pre></td></tr></table>
</div>
</div><p>浏览器还有很多其他的队列，由于和我们开发关系不大，不作考虑。</p>
<div class="details admonition question">
    <div class="details-summary admonition-title">
      <i class="icon fa-solid fa-question-circle fa-fw" aria-hidden="true"></i>面试题：阐述一下 JS 的事件循环<i class="details-icon fa-solid fa-angle-right fa-fw" aria-hidden="true"></i>
    </div>
    <div class="details-content">
      <div class="admonition-content"><p>参考答案：</p>
<p>事件循环又叫做消息循环，是浏览器渲染主线程的工作方式。</p>
<p>在 Chrome 的源码中，它开启一个不会结束的 for 循环，每次循环从消息队列中取出第一个任务执行，而其他线程只需要在合适的时候将任务加入到队列末尾即可。</p>
<p>过去把消息队列简单分为宏队列和微队列，这种说法目前已无法满足复杂的浏览器环境，取而代之的是一种更加灵活多变的处理方式。</p>
<p>根据 W3C 官方的解释，每个任务有不同的类型，同类型的任务必须在同一个队列，不同的任务可以属于不同的队列。不同任务队列有不同的优先级，在一次事件循环中，由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列，微队列的任务一定具有最高的优先级，必须优先调度执行。</p>
</div>
    </div>
  </div>
<div class="details admonition question">
    <div class="details-summary admonition-title">
      <i class="icon fa-solid fa-question-circle fa-fw" aria-hidden="true"></i>面试题：JS 中的计时器能做到精确计时吗？为什么？<i class="details-icon fa-solid fa-angle-right fa-fw" aria-hidden="true"></i>
    </div>
    <div class="details-content">
      <div class="admonition-content"><p>参考答案：</p>
<p>不行，因为：</p>
<ol>
<li>计算机硬件没有原子钟，无法做到精确计时</li>
<li>操作系统的计时函数本身就有少量偏差，由于 JS 的计时器最终调用的是操作系统的函数，也就携带了这些偏差</li>
<li>按照 W3C 的标准，浏览器实现计时器时，如果嵌套层级超过 5 层，则会带有 4 毫秒的最少时间，这样在计时时间少于 4 毫秒时又带来了偏差</li>
<li>受事件循环的影响，计时器的回调函数只能在主线程空闲时运行，因此又带来了偏差</li>
</ol>
</div>
    </div>
  </div>]]></description></item></channel></rss>