<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>一曝十寒博客</title>
    <link>https://yuhhhy.cn</link>
    <atom:link href="https://yuhhhy.cn/feed" rel="self" type="application/rss+xml"/>
    <description>纵使不安彷徨，即便茫然无措，也依然迈步前行</description>
    <language>zh-CN</language>
    <lastBuildDate>Fri, 30 Oct 2026 04:03:43 GMT</lastBuildDate>
    <generator>blog-vue-koa</generator>
    <item>
      <title>初入深圳</title>
      <link>https://yuhhhy.cn/blog/ms4uvcly-6i</link>
      <guid isPermaLink="true">https://yuhhhy.cn/blog/ms4uvcly-6i</guid>
      <pubDate>Tue, 28 Jul 2026 16:13:51 GMT</pubDate>
      <dc:creator><![CDATA[一曝十寒]]></dc:creator>
      <description><![CDATA[<p>这两天在深圳看房，几乎没有房东愿意短租的。</p>
<p>因为工作的原因我住在宝安这边，中介老哥带我去看了一套骨灰盒房，没有窗户密不透风，感觉我死在里面一个星期都不会有人发现。</p>
<p>深圳的房租比其他地方贵，贵太多了，而且就我看到的而言居住条件都不太好。</p>
<p>好在最后挑到了一套性价比还算高的，房间在一楼，一床一卫带一厨，十来平的小房间，这样的机会再晚或许就没有了，没考虑多久我就以 2200 的价格租下来了。</p>
<p>深圳这边大多数人是外来移民，在路上很难见到深圳本地人。</p>
<p>初到深圳，地铁播报是有粤语的，城中村真的挺狭窄的，食物的选择上也很多元，外来的人口也带来了各地的饮食习惯，所以在这里能品尝到到各式各样的美食。</p>
<p>1980 年深圳成为经济特区，特殊的经济政策以及人们的积极性造就了“三天一层楼”的深圳速度，现在看来深圳的摩天大楼的确很高，而且很多</p>
<p>—— 按照 CTBUH（世界高层建筑与都市人居委员会）统计，单是深圳 200 米以上的高楼数量，就超过了惊人的 200 栋，比北京和上海加起来还多，稳居全球第一。</p>
<p>真不愧是大城市。</p>
</br>
<p><img src="https://yuhhhy.cn/images/shenzhen_202607271535.avif" alt="深圳的摩天大楼"></p>
</br>
<p>这期间还吃了猪脚饭，虽然算不上什么特色吧，但是管饱。</p>
<p><img src="https://yuhhhy.cn/images/shenzhen_202607271743.avif" alt=""></p>
</br>
<p>萨利亚我觉得还是有点贵。</p>
<p><img src="https://yuhhhy.cn/images/shenzhen_202607281306.avif" alt=""></p>
<p>过两天要去实习报到了，祝我好运。</p>
]]></description>
      <content:encoded><![CDATA[<p>这两天在深圳看房，几乎没有房东愿意短租的。</p>
<p>因为工作的原因我住在宝安这边，中介老哥带我去看了一套骨灰盒房，没有窗户密不透风，感觉我死在里面一个星期都不会有人发现。</p>
<p>深圳的房租比其他地方贵，贵太多了，而且就我看到的而言居住条件都不太好。</p>
<p>好在最后挑到了一套性价比还算高的，房间在一楼，一床一卫带一厨，十来平的小房间，这样的机会再晚或许就没有了，没考虑多久我就以 2200 的价格租下来了。</p>
<p>深圳这边大多数人是外来移民，在路上很难见到深圳本地人。</p>
<p>初到深圳，地铁播报是有粤语的，城中村真的挺狭窄的，食物的选择上也很多元，外来的人口也带来了各地的饮食习惯，所以在这里能品尝到到各式各样的美食。</p>
<p>1980 年深圳成为经济特区，特殊的经济政策以及人们的积极性造就了“三天一层楼”的深圳速度，现在看来深圳的摩天大楼的确很高，而且很多</p>
<p>—— 按照 CTBUH（世界高层建筑与都市人居委员会）统计，单是深圳 200 米以上的高楼数量，就超过了惊人的 200 栋，比北京和上海加起来还多，稳居全球第一。</p>
<p>真不愧是大城市。</p>
</br>
<p><img src="https://yuhhhy.cn/images/shenzhen_202607271535.avif" alt="深圳的摩天大楼"></p>
</br>
<p>这期间还吃了猪脚饭，虽然算不上什么特色吧，但是管饱。</p>
<p><img src="https://yuhhhy.cn/images/shenzhen_202607271743.avif" alt=""></p>
</br>
<p>萨利亚我觉得还是有点贵。</p>
<p><img src="https://yuhhhy.cn/images/shenzhen_202607281306.avif" alt=""></p>
<p>过两天要去实习报到了，祝我好运。</p>
]]></content:encoded>
      <media:content url="https://yuhhhy.cn/images/cd9377d2a2e4058b0c35b7be0a22c130.avif" medium="image" type="image/avif"/>
    </item>
    <item>
      <title>Something About React Query</title>
      <link>https://yuhhhy.cn/blog/mn8uu5ai-cj</link>
      <guid isPermaLink="true">https://yuhhhy.cn/blog/mn8uu5ai-cj</guid>
      <pubDate>Fri, 27 Mar 2026 12:05:28 GMT</pubDate>
      <dc:creator><![CDATA[一曝十寒]]></dc:creator>
      <description><![CDATA[<h1>异步状态管理器</h1>
<p>数据获取很简单，<strong>异步状态管理</strong>却并非如此。</p>
<p>React Query 并非数据获取库，而是一个<strong>异步状态管理器</strong>。它使 Web 应用程序中的服务器状态的获取、缓存、同步和更新变得轻而易举。</p>
<p>React Query 将服务端状态（server state）与客户端状态（client state）明确分离，提供声明式的数据获取范式。</p>
<p>常见的误区是认为 React Query 是&quot;全局状态管理工具&quot;，然而 React Query 管理的是<strong>服务端状态</strong>，不是 UI 状态。</p>
<p><strong>什么是服务端状态和客户端状态？</strong></p>
<p>服务端状态（如 API 数据）本质上是缓存数据，与“客户端状态”（如 UI 弹窗开关）有着根本区别，需要不同的管理策略。</p>
<p><strong>客户端状态的管理与服务端状态完全不同。</strong></p>
<p>首先，服务端状态拥有的问题：</p>
<ol>
<li><strong>异步性</strong>与不可控性：服务端数据保存在远程，获取数据需要经过网络请求，存在延迟、失败或超时等不可控因素。</li>
<li>数据<strong>易过期</strong>（Stale）：服务端数据是“只读快照”，一旦数据返回给前端，服务器端的原始数据可能随时被其他用户修改，导致前端数据失效。</li>
<li>多组件共享与<strong>重复请求</strong>：多个组件同时渲染时，若都依赖同一份服务端数据，容易触发冗余的 API 调用，造成带宽和性能浪费。</li>
<li>复杂的<strong>缓存失效</strong>逻辑：手动管理缓存非常困难，需要精准判断何时该保留旧数据、何时该强制从后端刷新数据。</li>
<li><strong>状态同步</strong>挑战：在执行“增删改”操作（Mutation）后，确保本地缓存与服务端数据实时同步（如更新列表、重新拉取）逻辑繁琐。</li>
<li><strong>竞态条件</strong>：由于网络请求返回顺序的不确定性，<strong>较晚发出的请求可能比较早发出的请求先返回</strong>，导致数据覆盖错误。</li>
</ol>
<p>而 React Query 可以：</p>
<ol>
<li>全自动<strong>缓存管理</strong>：根据唯一的 Query Key 自动存储请求结果，并在下次请求时直接返回缓存数据，显著提升响应速度。</li>
<li>声明式<strong>状态处理</strong>：内置 <code>isLoading</code>、<code>isError</code>、<code>isFetching</code> 等状态位，开发者无需手动维护多个 <code>useState</code> 来跟踪请求进度。</li>
<li>智能<strong>请求去重</strong>：在同一时间内，针对相同 Query Key 的多次请求会被合并为一次，有效减少后端服务器压力。</li>
<li>后台静默刷新：支持在窗口聚焦、网络重连或配置的时间间隔内自动在后台触发请求，确保数据始终处于准实时状态。</li>
<li><strong>过时判定</strong>机制（Stale Time）：允许开发者定义数据“新鲜”的时间长度，在此时间内不会触发重新获取，精细化控制网络开销。</li>
<li>乐观更新：在提交修改请求时先行更新 UI，等服务器响应后再根据结果进行确认或回滚，提供极速的用户交互体验。</li>
<li>无缝的分页与无限滚动：内置专用的 API（如 <code>useInfiniteQuery</code>）来简化复杂的翻页加载和瀑布流数据维护逻辑。</li>
<li>自动垃圾回收：当数据不再被任何组件使用时，React Query 会在设定的时间后自动清理缓存，防止内存泄漏。</li>
</ol>
<p><strong>React Query 可以做到。但，怎么做到的？</strong></p>
<h1>ReactQuery的本质</h1>
<p>在 Reacr Query 中，<strong>代表请求唯一标识的并不是请求 path，而是 queryKey</strong>，它作为 useQuery 必传的第一个参数，接收字符串，数组，对象等一切可以被序列化的值。</p>
<p>试想这样一个场景，假设页面上有三个子组件 A，B，C（下方的蓝色方块），都需要用到同一个接口的 todo 数据。</p>
<p>如果只是把模板代码搬到 useQuery 内部的话，3 次 useQuery 就会发送 3 个相同的请求。</p>
<p>但是实际上，<strong>请求只会发出一次</strong>。</p>
<p><img src="https://yuhhhy.cn/images/20260327-react-query-1.avif" alt="react query 多组件请求流程"></p>
<p>原因就是 3 个 useQuery，我们都定义了同一个值为 <code>'todos'</code> 的 queryKey，它代表<strong>请求唯一标识</strong>。</p>
<p>请求都收敛到了 Query 实例内部发出，直接与服务端交互。3 个 queryKey 对应 1 个 Query 实例，所以只会有一次请求。</p>
<pre><code class="language-JavaScript">import { QueryClient, QueryClientProvider } from 'react-query';

const queryClient = new QueryClient();

function App () {
  return (
   &lt;QueryClientProvider client={queryClient}&gt;
     &lt;Example /&gt;
   &lt;/QueryClientProvider&gt;
  )
}
</code></pre>
<p>那么 Query 实例保存在哪个地方呢？它保存在了 QueryClient，并注入到了整个应用中。</p>
<p>在图上加上 queryClient，整体流程如下，<strong>它的本质就是一个外部的状态管理库。</strong></p>
<p><img src="https://yuhhhy.cn/images/20260327-react-query-2.avif" alt="react query 多组件请求流程2"></p>
<p>这又引出了一个新的问题：</p>
<p>既然 Query 保存在组件外部，请求的 data，loading 等状态也都脱离了 React 管理，那么 React 是如何感知到它们的变化，来影响渲染的呢？</p>
<p><strong>如何观测数据，观测到数据变更后如何更新。</strong> 这其实是所有状态管理库的核心命题。</p>
<p>React Query 的实现使用了<strong>观察者模式</strong>，这也是它的一个核心。</p>
<p>上面的例子中，App 内有多个地方都依赖了 queryClient 中的同一个 todos 数据。有了观察者，就可以在这部分数据更新时，通知到每一个订阅者。</p>
<p>当每次调用 useQuery 时，内部都会实例一个观察者对象 observer。observer 会订阅 query 的状态，当这些状态变化时，触发 React 强制更新。</p>
<p>在 React 中，手动强制触发更新无非就是 forceUpdate。React Query 在 v3 版本就是这么做的。但是在 React 18 发布后，由于 Fiber 设计特性的原因，forceUpdate 可能会造成<strong>撕裂问题，</strong> 已经不能再继续使用了。</p>
<p>为了防止 tearing 的出现，React 18 给出了官方答案，推出了新的 API：<strong>useSyncExternalStore，</strong> 直译为 “同步外部 store”。</p>
<p>于是，React Query 在最新的 v4 将 forceUpdate 换成了 useSyncExternalStore。</p>
<p><strong>总结一下，Reacr Query 基于 useQuery 的 queryKey 来识别和对应 Query 实例，Query 实例保存在 QueryClient 中，通过观察者模式实现了数据的订阅和更新。</strong></p>
<h1>React Query 缓存</h1>
<p>客户端允许我们使用缓存——那么缓存是什么呢？</p>
<p>简单来说，这<code>QueryCache</code>是一个内存对象，其中键是<em>查询键</em>的稳定序列化版本（称为<em>查询键哈希</em>），值是该类的一个实例<code>Query</code>。</p>
<p>我认为理解这一点很重要：React Query 默认情况下只将数据存储在** <strong><strong>JavaScript</strong></strong> <strong><strong>内存</strong></strong> **中，不会存储在其他任何地方。如果您重新加载浏览器页面，缓存就会丢失。如果你想将缓存写入外部存储，例如本地存储，请查看<a href="https://tanstack.com/query/v5/docs/framework/react/plugins/persistQueryClient">持久化器</a>。</p>
<p><img src="https://yuhhhy.cn/images/20260327-react-query-3.avif" alt="react query 缓存"></p>
<p>缓存包含 queries，大部分逻辑都在 <code>Query</code> 里执行。它不仅包含关于请求的所有信息（数据、状态字段或元信息，例如上次获取时间），还执行请求函数，并包含重试、取消和去重逻辑。</p>
<p>它内部有一个<em>状态机</em>，确保不会陷入不可能的状态。例如，如果在获取数据时触发查询函数，则可以对获取的数据进行去重。如果查询被取消，它会返回到之前的状态。</p>
<p>最重要的是，查询知道谁对查询数据感兴趣，并且可以将所有更改通知 <code>Observers</code>（<code>Observer</code>是连接<code>Query</code>和需要使用它的组件之间的桥梁）。</p>
<p><strong>总结：React Query 使用名为 <code>QueryCache</code> 的内存对象，将数据存储在浏览器缓存中，<code>QueryCache</code> 包含了<code>Queries</code>，大部分逻辑都是在 <code>Query</code> 里执行的。</strong></p>
<h1>React Query 自动缓存管理</h1>
<p>下面这张图是为react-query 的一个异步请求的逻辑流程图。</p>
<p><img src="https://yuhhhy.cn/images/20260327-react-query-4.avif" alt="react query 异步请求流程"></p>
<p>react-query 首先通过检查查询缓存 <strong>[2]</strong> 来确定数据是否已经被获取。如果数据不在缓存中，或者缓存的数据已经过期，react-query将发送一个 HTTP 请求到 API Server <strong>[3]</strong> 来检索数据，然后将数据存储在缓存中 <strong>[4]</strong>。最后，react-query 从缓存中**[5] **拿到数据，将数据返回给用户 。</p>
<p>通过缓存查询结果，react-query 减少了网络流量，提高了应用性能。</p>
<p>但是这还算不上自动，那么自动在哪呢？</p>
<p>React Query 非常智能，设置一些选项，它会<strong>选择一些关键节点来触发重新获取数据</strong>：</p>
<ul>
<li><code>refetchOnMount</code> 每当有新的调用<code>useQuery</code>的组件 mounts 时，React Query 都会进行重新验证数据是否新鲜。</li>
<li><code>refetchOnWindowFocus</code> 每当您聚焦浏览器标签页时，都会进行一次重新加载。在生产环境中，这很可能表明用户之前将我们的应用保持在某个标签页中，现在正在查看邮件或浏览推特，然后返回应用。在这种情况下，向他们显示最新更新。</li>
<li><code>refetchOnReconnect</code> 如果网络连接中断后又恢复，这也是一个很好的时机，可以重新验证屏幕上显示的内容是否新鲜。</li>
</ul>
<p>最后，如果作为应用程序的开发者，可以通过手动方式调用失效机制<code>queryClient.invalidateQueries</code>。</p>
<h2>staleTime</h2>
<p>当反复卸载再重新挂载组件时，React Query 无法进行去重。这意味着每次你**挂载（Mount）**一个新的组件实例时，都会进行后台重新获。取如果你频繁这样做，尤其是在短时间内连续挂载组件，且这些挂载不在同一渲染周期内，你可能会在网络选项卡中看到大量的请求。这主要是因为<code>staleTime</code><strong>默认值为零。</strong></p>
<p>（<strong>但这并不意味着，每渲染（Re-render）一次就请求一次！！！每渲染一次就请求一次是疯狂的，因为在 React 中，重新渲染是非常频繁的！即使<code>staleTime</code>默认值为零，React Query 也不会在每次重新渲染时都调用<code>queryFn</code></strong>）</p>
<p>解决方案是将其设置<code>staleTime</code>为一个适合具体使用场景的值。设置 <code>staleTime</code> 后，只要数据是最新的，就始终只会从缓存中获取。</p>
<pre><code class="language-JavaScript">useQuery({ queryKey: ['todos'], queryFn: fetchTodos, staleTime: 5000 })
</code></pre>
<p>这样，在 5 秒内无论你如何重新挂载组件，React Query 都会认为缓存数据是可靠的（新鲜的），<strong>直接读取缓存而不发起任何网络请求</strong>。</p>
<h2>gcTime</h2>
<p>非活动（inactive）的查询从缓存中移除前的等待时间。默认值为 5 分钟。当没有注册的 Observer 时，也就是说，当<strong>所有使用该查询的组件都已卸载时</strong>，查询就会进入非活动状态，在 gcTime 结束后，数据将从内存中抹除。</p>
<p><strong>总结：React Query 会自动选择一些关键节点来触发重新获取数据，比如组件挂载、重新聚焦标签页、网络重连。通过 staleTime 来设置数据从 <code>fresh</code> 变为 <code>stale</code> 的时间，fresh 时会直接读取缓存而不发起任何网络请求。用 gcTime 设置垃圾回收时间，在 gcTime 超时后会将数据从内存中抹除。</strong></p>
<h1>Stale-While-Revalidate (SWR) 原理</h1>
<p>这是 React Query 的核心策略：<strong>“在校验数据的同时，先给用户看旧的”</strong>。它的流程如下：</p>
<p>一个组件挂载，调用 <code>useQuery</code>。React Query 检查 <code>QueryCache</code>。如果里面有旧数据，<strong>立即同步返回</strong>给组件去渲染。此时再更加 staleTime（基于 <code>lastUpdated</code> 时间戳），检查是否需要更新数据。如果不需要更新，流程结束，如果需要，<strong>触发 Revalidate（重新验证）:</strong></p>
<p><strong>发起网络请求 -&gt; 更新全局缓存 -&gt; 通知观察者（Observer）</strong></p>
<p><strong>参考资料</strong></p>
<p><a href="https://zhuanlan.zhihu.com/p/638385612">https://zhuanlan.zhihu.com/p/638385612</a></p>
<p><a href="https://juejin.cn/post/7169515109172609032">https://juejin.cn/post/7169515109172609032</a></p>
<p><a href="https://tkdodo.eu/blog/inside-react-query">Inside React Query</a></p>
<p><a href="https://tkdodo.eu/blog/react-query-as-a-state-manager">React Query as a State Manager</a></p>
<p><a href="https://tkdodo.eu/blog/practical-react-query">separate-server-and-client-state - CodeSandbox</a></p>
]]></description>
      <content:encoded><![CDATA[<h1>异步状态管理器</h1>
<p>数据获取很简单，<strong>异步状态管理</strong>却并非如此。</p>
<p>React Query 并非数据获取库，而是一个<strong>异步状态管理器</strong>。它使 Web 应用程序中的服务器状态的获取、缓存、同步和更新变得轻而易举。</p>
<p>React Query 将服务端状态（server state）与客户端状态（client state）明确分离，提供声明式的数据获取范式。</p>
<p>常见的误区是认为 React Query 是&quot;全局状态管理工具&quot;，然而 React Query 管理的是<strong>服务端状态</strong>，不是 UI 状态。</p>
<p><strong>什么是服务端状态和客户端状态？</strong></p>
<p>服务端状态（如 API 数据）本质上是缓存数据，与“客户端状态”（如 UI 弹窗开关）有着根本区别，需要不同的管理策略。</p>
<p><strong>客户端状态的管理与服务端状态完全不同。</strong></p>
<p>首先，服务端状态拥有的问题：</p>
<ol>
<li><strong>异步性</strong>与不可控性：服务端数据保存在远程，获取数据需要经过网络请求，存在延迟、失败或超时等不可控因素。</li>
<li>数据<strong>易过期</strong>（Stale）：服务端数据是“只读快照”，一旦数据返回给前端，服务器端的原始数据可能随时被其他用户修改，导致前端数据失效。</li>
<li>多组件共享与<strong>重复请求</strong>：多个组件同时渲染时，若都依赖同一份服务端数据，容易触发冗余的 API 调用，造成带宽和性能浪费。</li>
<li>复杂的<strong>缓存失效</strong>逻辑：手动管理缓存非常困难，需要精准判断何时该保留旧数据、何时该强制从后端刷新数据。</li>
<li><strong>状态同步</strong>挑战：在执行“增删改”操作（Mutation）后，确保本地缓存与服务端数据实时同步（如更新列表、重新拉取）逻辑繁琐。</li>
<li><strong>竞态条件</strong>：由于网络请求返回顺序的不确定性，<strong>较晚发出的请求可能比较早发出的请求先返回</strong>，导致数据覆盖错误。</li>
</ol>
<p>而 React Query 可以：</p>
<ol>
<li>全自动<strong>缓存管理</strong>：根据唯一的 Query Key 自动存储请求结果，并在下次请求时直接返回缓存数据，显著提升响应速度。</li>
<li>声明式<strong>状态处理</strong>：内置 <code>isLoading</code>、<code>isError</code>、<code>isFetching</code> 等状态位，开发者无需手动维护多个 <code>useState</code> 来跟踪请求进度。</li>
<li>智能<strong>请求去重</strong>：在同一时间内，针对相同 Query Key 的多次请求会被合并为一次，有效减少后端服务器压力。</li>
<li>后台静默刷新：支持在窗口聚焦、网络重连或配置的时间间隔内自动在后台触发请求，确保数据始终处于准实时状态。</li>
<li><strong>过时判定</strong>机制（Stale Time）：允许开发者定义数据“新鲜”的时间长度，在此时间内不会触发重新获取，精细化控制网络开销。</li>
<li>乐观更新：在提交修改请求时先行更新 UI，等服务器响应后再根据结果进行确认或回滚，提供极速的用户交互体验。</li>
<li>无缝的分页与无限滚动：内置专用的 API（如 <code>useInfiniteQuery</code>）来简化复杂的翻页加载和瀑布流数据维护逻辑。</li>
<li>自动垃圾回收：当数据不再被任何组件使用时，React Query 会在设定的时间后自动清理缓存，防止内存泄漏。</li>
</ol>
<p><strong>React Query 可以做到。但，怎么做到的？</strong></p>
<h1>ReactQuery的本质</h1>
<p>在 Reacr Query 中，<strong>代表请求唯一标识的并不是请求 path，而是 queryKey</strong>，它作为 useQuery 必传的第一个参数，接收字符串，数组，对象等一切可以被序列化的值。</p>
<p>试想这样一个场景，假设页面上有三个子组件 A，B，C（下方的蓝色方块），都需要用到同一个接口的 todo 数据。</p>
<p>如果只是把模板代码搬到 useQuery 内部的话，3 次 useQuery 就会发送 3 个相同的请求。</p>
<p>但是实际上，<strong>请求只会发出一次</strong>。</p>
<p><img src="https://yuhhhy.cn/images/20260327-react-query-1.avif" alt="react query 多组件请求流程"></p>
<p>原因就是 3 个 useQuery，我们都定义了同一个值为 <code>'todos'</code> 的 queryKey，它代表<strong>请求唯一标识</strong>。</p>
<p>请求都收敛到了 Query 实例内部发出，直接与服务端交互。3 个 queryKey 对应 1 个 Query 实例，所以只会有一次请求。</p>
<pre><code class="language-JavaScript">import { QueryClient, QueryClientProvider } from 'react-query';

const queryClient = new QueryClient();

function App () {
  return (
   &lt;QueryClientProvider client={queryClient}&gt;
     &lt;Example /&gt;
   &lt;/QueryClientProvider&gt;
  )
}
</code></pre>
<p>那么 Query 实例保存在哪个地方呢？它保存在了 QueryClient，并注入到了整个应用中。</p>
<p>在图上加上 queryClient，整体流程如下，<strong>它的本质就是一个外部的状态管理库。</strong></p>
<p><img src="https://yuhhhy.cn/images/20260327-react-query-2.avif" alt="react query 多组件请求流程2"></p>
<p>这又引出了一个新的问题：</p>
<p>既然 Query 保存在组件外部，请求的 data，loading 等状态也都脱离了 React 管理，那么 React 是如何感知到它们的变化，来影响渲染的呢？</p>
<p><strong>如何观测数据，观测到数据变更后如何更新。</strong> 这其实是所有状态管理库的核心命题。</p>
<p>React Query 的实现使用了<strong>观察者模式</strong>，这也是它的一个核心。</p>
<p>上面的例子中，App 内有多个地方都依赖了 queryClient 中的同一个 todos 数据。有了观察者，就可以在这部分数据更新时，通知到每一个订阅者。</p>
<p>当每次调用 useQuery 时，内部都会实例一个观察者对象 observer。observer 会订阅 query 的状态，当这些状态变化时，触发 React 强制更新。</p>
<p>在 React 中，手动强制触发更新无非就是 forceUpdate。React Query 在 v3 版本就是这么做的。但是在 React 18 发布后，由于 Fiber 设计特性的原因，forceUpdate 可能会造成<strong>撕裂问题，</strong> 已经不能再继续使用了。</p>
<p>为了防止 tearing 的出现，React 18 给出了官方答案，推出了新的 API：<strong>useSyncExternalStore，</strong> 直译为 “同步外部 store”。</p>
<p>于是，React Query 在最新的 v4 将 forceUpdate 换成了 useSyncExternalStore。</p>
<p><strong>总结一下，Reacr Query 基于 useQuery 的 queryKey 来识别和对应 Query 实例，Query 实例保存在 QueryClient 中，通过观察者模式实现了数据的订阅和更新。</strong></p>
<h1>React Query 缓存</h1>
<p>客户端允许我们使用缓存——那么缓存是什么呢？</p>
<p>简单来说，这<code>QueryCache</code>是一个内存对象，其中键是<em>查询键</em>的稳定序列化版本（称为<em>查询键哈希</em>），值是该类的一个实例<code>Query</code>。</p>
<p>我认为理解这一点很重要：React Query 默认情况下只将数据存储在** <strong><strong>JavaScript</strong></strong> <strong><strong>内存</strong></strong> **中，不会存储在其他任何地方。如果您重新加载浏览器页面，缓存就会丢失。如果你想将缓存写入外部存储，例如本地存储，请查看<a href="https://tanstack.com/query/v5/docs/framework/react/plugins/persistQueryClient">持久化器</a>。</p>
<p><img src="https://yuhhhy.cn/images/20260327-react-query-3.avif" alt="react query 缓存"></p>
<p>缓存包含 queries，大部分逻辑都在 <code>Query</code> 里执行。它不仅包含关于请求的所有信息（数据、状态字段或元信息，例如上次获取时间），还执行请求函数，并包含重试、取消和去重逻辑。</p>
<p>它内部有一个<em>状态机</em>，确保不会陷入不可能的状态。例如，如果在获取数据时触发查询函数，则可以对获取的数据进行去重。如果查询被取消，它会返回到之前的状态。</p>
<p>最重要的是，查询知道谁对查询数据感兴趣，并且可以将所有更改通知 <code>Observers</code>（<code>Observer</code>是连接<code>Query</code>和需要使用它的组件之间的桥梁）。</p>
<p><strong>总结：React Query 使用名为 <code>QueryCache</code> 的内存对象，将数据存储在浏览器缓存中，<code>QueryCache</code> 包含了<code>Queries</code>，大部分逻辑都是在 <code>Query</code> 里执行的。</strong></p>
<h1>React Query 自动缓存管理</h1>
<p>下面这张图是为react-query 的一个异步请求的逻辑流程图。</p>
<p><img src="https://yuhhhy.cn/images/20260327-react-query-4.avif" alt="react query 异步请求流程"></p>
<p>react-query 首先通过检查查询缓存 <strong>[2]</strong> 来确定数据是否已经被获取。如果数据不在缓存中，或者缓存的数据已经过期，react-query将发送一个 HTTP 请求到 API Server <strong>[3]</strong> 来检索数据，然后将数据存储在缓存中 <strong>[4]</strong>。最后，react-query 从缓存中**[5] **拿到数据，将数据返回给用户 。</p>
<p>通过缓存查询结果，react-query 减少了网络流量，提高了应用性能。</p>
<p>但是这还算不上自动，那么自动在哪呢？</p>
<p>React Query 非常智能，设置一些选项，它会<strong>选择一些关键节点来触发重新获取数据</strong>：</p>
<ul>
<li><code>refetchOnMount</code> 每当有新的调用<code>useQuery</code>的组件 mounts 时，React Query 都会进行重新验证数据是否新鲜。</li>
<li><code>refetchOnWindowFocus</code> 每当您聚焦浏览器标签页时，都会进行一次重新加载。在生产环境中，这很可能表明用户之前将我们的应用保持在某个标签页中，现在正在查看邮件或浏览推特，然后返回应用。在这种情况下，向他们显示最新更新。</li>
<li><code>refetchOnReconnect</code> 如果网络连接中断后又恢复，这也是一个很好的时机，可以重新验证屏幕上显示的内容是否新鲜。</li>
</ul>
<p>最后，如果作为应用程序的开发者，可以通过手动方式调用失效机制<code>queryClient.invalidateQueries</code>。</p>
<h2>staleTime</h2>
<p>当反复卸载再重新挂载组件时，React Query 无法进行去重。这意味着每次你**挂载（Mount）**一个新的组件实例时，都会进行后台重新获。取如果你频繁这样做，尤其是在短时间内连续挂载组件，且这些挂载不在同一渲染周期内，你可能会在网络选项卡中看到大量的请求。这主要是因为<code>staleTime</code><strong>默认值为零。</strong></p>
<p>（<strong>但这并不意味着，每渲染（Re-render）一次就请求一次！！！每渲染一次就请求一次是疯狂的，因为在 React 中，重新渲染是非常频繁的！即使<code>staleTime</code>默认值为零，React Query 也不会在每次重新渲染时都调用<code>queryFn</code></strong>）</p>
<p>解决方案是将其设置<code>staleTime</code>为一个适合具体使用场景的值。设置 <code>staleTime</code> 后，只要数据是最新的，就始终只会从缓存中获取。</p>
<pre><code class="language-JavaScript">useQuery({ queryKey: ['todos'], queryFn: fetchTodos, staleTime: 5000 })
</code></pre>
<p>这样，在 5 秒内无论你如何重新挂载组件，React Query 都会认为缓存数据是可靠的（新鲜的），<strong>直接读取缓存而不发起任何网络请求</strong>。</p>
<h2>gcTime</h2>
<p>非活动（inactive）的查询从缓存中移除前的等待时间。默认值为 5 分钟。当没有注册的 Observer 时，也就是说，当<strong>所有使用该查询的组件都已卸载时</strong>，查询就会进入非活动状态，在 gcTime 结束后，数据将从内存中抹除。</p>
<p><strong>总结：React Query 会自动选择一些关键节点来触发重新获取数据，比如组件挂载、重新聚焦标签页、网络重连。通过 staleTime 来设置数据从 <code>fresh</code> 变为 <code>stale</code> 的时间，fresh 时会直接读取缓存而不发起任何网络请求。用 gcTime 设置垃圾回收时间，在 gcTime 超时后会将数据从内存中抹除。</strong></p>
<h1>Stale-While-Revalidate (SWR) 原理</h1>
<p>这是 React Query 的核心策略：<strong>“在校验数据的同时，先给用户看旧的”</strong>。它的流程如下：</p>
<p>一个组件挂载，调用 <code>useQuery</code>。React Query 检查 <code>QueryCache</code>。如果里面有旧数据，<strong>立即同步返回</strong>给组件去渲染。此时再更加 staleTime（基于 <code>lastUpdated</code> 时间戳），检查是否需要更新数据。如果不需要更新，流程结束，如果需要，<strong>触发 Revalidate（重新验证）:</strong></p>
<p><strong>发起网络请求 -&gt; 更新全局缓存 -&gt; 通知观察者（Observer）</strong></p>
<p><strong>参考资料</strong></p>
<p><a href="https://zhuanlan.zhihu.com/p/638385612">https://zhuanlan.zhihu.com/p/638385612</a></p>
<p><a href="https://juejin.cn/post/7169515109172609032">https://juejin.cn/post/7169515109172609032</a></p>
<p><a href="https://tkdodo.eu/blog/inside-react-query">Inside React Query</a></p>
<p><a href="https://tkdodo.eu/blog/react-query-as-a-state-manager">React Query as a State Manager</a></p>
<p><a href="https://tkdodo.eu/blog/practical-react-query">separate-server-and-client-state - CodeSandbox</a></p>
]]></content:encoded>
      <media:content url="https://yuhhhy.cn/images/20260327-react-query.avif" medium="image" type="image/avif"/>
    </item>
    <item>
      <title>构建高性能 Web 应用实录</title>
      <link>https://yuhhhy.cn/blog/mmhuhmnu-um</link>
      <guid isPermaLink="true">https://yuhhhy.cn/blog/mmhuhmnu-um</guid>
      <pubDate>Sun, 08 Mar 2026 14:25:57 GMT</pubDate>
      <dc:creator><![CDATA[一曝十寒]]></dc:creator>
      <description><![CDATA[<p>前段时间心血来潮写了个练手项目，本来只是个随便玩的玩具，但写完后强迫症犯了，非要测测 Lighthouse 跑分，看看能不能搞个“秒开”。结果在优化过程中踩了不少坑，这里随便记录一下。</p>
<p>这里记录一下我在这场“白屏歼灭战”中踩过的坑和积累的经验。</p>
<h1>换掉 Webpack 之后，Vite 真的开箱即快吗？</h1>
<p>在打包工具的选择上，最开始我还是按照老习惯，直接用 Webpack 搭建了基础配置。（毕竟生态成熟，省一点事是一点事）。
Webpack 也确实很稳，各种 Loader 和 Plugin 往上一堆，基本搞定了自己想要的代码转译和打包功能。但是！作为个人项目，它的体验真的越来越让人心累。虽然代码量不算庞大，不至于每次 HMR 卡死，但每次修改一行代码，按保存后都要盯着终端等上好几秒，太打断思路了。</p>
<p>忍无可忍之下，我花了半天把构建工具整体迁移到了 Vite。</p>
<p>得益于浏览器原生的 ES Module 支持和底层的 ESBuild，Vite 在本地开发时的冷启动和热更新速度简直堪称魔法，几乎做到了真正的“毫秒级响应”。</p>
<p>但这有个很容易踩的坑：<strong>本地开发快，不代表打包出来的线上代码也快。</strong></p>
<p>Vite 在打包生产环境代码时，底层依赖的依然是 Rollup。对于个人项目来说，即使你自己写的代码量不大，但如果你图省事，把所有的页面组件、甚至偶尔用到的第三方依赖（比如图表库、动画库）全部默认打包进同一个 index.js 里，产物依然会变成一个略显臃肿的包。</p>
<p>在千兆宽带下，这点体积或许连零点几秒的白屏都感觉不到。但一跑 Lighthouse 的模拟弱网测试，这就成了扣分的重灾区。</p>
<p>只要你一进首页，浏览器就不得不去下载和解析那些属于其他子页面的、“当前毫无用处”的 JS 代码。Lighthouse 的报告上毫不留情地给你标红：“Reduce unused JavaScript”（减少未使用的 JS） 以及 “Minimize main-thread work”（最小化主线程工作）。白屏时间并不会因为你用了 Vite 而凭空消失。</p>
<p>最终，我还是老老实实地按路由进行了代码分割（Code Splitting），才算把首屏不需要的 JS 负担卸下来。</p>
<h1>毁掉首屏的罪魁祸首：那张 5MB 的 Hero 大图</h1>
<p>解决了代码体积，加载速度依然不达标。排查 Network 面板后，我找到了真正的“性能杀手”。</p>
<p>为了让网站一打开就具备极强的视觉冲击力，我在首屏放置了一张全屏的 Hero 高清视觉图。当时图省事，直接拿了一张高达 5MB 的原图，用原生的 <code>&lt;img&gt;</code> 标签硬塞进了页面。</p>
<p>在弱网环境下，这张大图直接把浏览器的下载通道占满了，导致后续的关键文件都被堵在后面排队。</p>
<p>既然找到了病因，接下来就是对症下药。针对图片，我主要做了两件事：</p>
<p><strong>第一步：引入 WebP/AVIF 格式与优雅降级。</strong></p>
<p>提到现代图片格式，很多人第一反应是 WebP，大家都知道用 WebP 压缩图片，但我这次试了一下 AVIF。</p>
<p>作为基于 AV1 视频编码技术的新一代图像格式，AVIF 的压缩率简直令人发指。相比于已经很优秀的 WebP，它还能再缩小 20%~30% 的体积。</p>
<p>然而，享受极限性能是要付出代价的。AVIF 虽然美好，却有两个极其扎眼的缺点：</p>
<p>首先是高昂的编码成本。由于压缩算法极其复杂，我在本地用脚本将大图转码为 AVIF 时，电脑的 CPU 风扇直接起飞，转换一张图片的耗时比生成 WebP 慢了好几倍。如果项目里有海量图片，构建时间的延长会是个大麻烦。</p>
<p>其次是残酷的兼容性断层。虽然 Chrome 和较新的 Safari 都已支持 AVIF，但在一些老旧设备（比如 iOS 16 以下的 iPhone）或非主流浏览器上，它依然是一片无法解析的空白。</p>
<p>为了在“极致跑分”和“稳健展示”之间找到完美的平衡，我放弃了单一的 <code>&lt;img&gt;</code> 标签，果断采用了 HTML5 的 <code>&lt;picture&gt;</code> 标签来实现多级优雅降级：首选极速且极致压缩的 AVIF，备选体积稍大的 WebP，这样一来，支持 AVIF 的现代浏览器能享受到秒开的快感，而老旧设备则会乖乖回退加载常规图片。</p>
<p><strong>第二步：祭出按需加载（Lazy Loading）。</strong>
虽然首屏的 Hero 图必须立刻加载，但页面下方（不在首屏可视区域内）的大量配图完全没必要在第一时间抢占带宽。我利用浏览器的 <code>Intersection Observer</code> API 封装了一个自定义 Hook，监听这些 DOM 元素是否进入视口，只有当用户滚动到附近时，才将真实的图片 URL 赋给 <code>src</code> 属性。这一步直接将首屏的并发请求数砍掉了一大半。</p>
<p>（不过为了追求极致的跑分，我最后还是忍痛把首屏那张 5MB 的背景图给删了，QAQ）</p>
<h1>顺手做一下产物压缩</h1>
<p>搞定了最棘手的大图和冗余 JS，Lighthouse 的跑分其实已经非常好看了。但这依然满足不了我的强迫症，在 Network 面板里，我盯上了那堆打包出来的 HTML、CSS 和 JS 小文件。</p>
<p>纯文本文件在网络传输中其实有着巨大的压缩潜力。为此，我在 Vite 配置中引入了体积优化：</p>
<p><strong>1. 开启 Gzip 与 Brotli 文本压缩</strong></p>
<p>我通过配置 <code>vite-plugin-compression</code> 插件，在打包时直接对文本产物进行高强度压缩。除了常规的 Gzip，我还特意开启了压缩率更高的 Brotli（<code>.br</code>）格式：</p>
<pre><code class="language-javascript">// vite.config.js
plugins: [
  viteCompression({ algorithm: 'gzip' }),
  viteCompression({ algorithm: 'brotliCompress' })
]
</code></pre>
<p>这样做的好处是，Vite 会在打包时直接生成打包好的 <code>.gz</code> 和 <code>.br</code> 文件。当配置好 Nginx 服务器后，面对用户的访问，服务器不再需要实时消耗 CPU 去动态压缩文件，而是直接把提前准备好的 <code>.gz</code> 或 <code>.br</code> 文件顺着网线扔给浏览器，浏览器再利用原生能力瞬间解压渲染。这一步，让文本资源的传输体积再次暴降了 70% 左右。</p>
<p><strong>2. 静态碎图的自动化瘦身</strong></p>
<p>除了那张被干掉的 Hero 大图，项目里其实还散落着不少 UI 切图、Logo 和占位小图。为了避免以后每次加新图都要手动去 TinyPNG 压一遍，我直接接上了 <code>vite-plugin-imagemin</code> 插件。每次执行 <code>build</code> 构建时，它会在本地默默把所有的图片素材进行无损或微损的批量极限压缩，彻底杜绝了因疏忽导致大图上线的可能。</p>
<h1>解决拆包带来的新问题：拥抱 HTTP/2</h1>
<p>把前面这些都做完后，我再看 Network 面板，发现 Waterfall（请求瀑布流）还是有一点阶梯感。</p>
<p>仔细一想，这其实是我前面做的代码分割带来的副作用：拆包固然减小了单个 JS 的体积，但也导致首屏需要同时请求多个小 Chunk 文件。而在传统的 HTTP/1.1 协议下，浏览器对同一个域名的并发请求数通常限制在 6 个左右，超过的数量只能在队列里傻等（队头阻塞）。</p>
<p>资源再小，排队也要花时间。为了解决这个问题，我给服务器配置了 HTTPS（这是使用现代 Web 协议的先决条件），并开启了 HTTP/2。</p>
<p>HTTP/2 最大的优势就是支持 <strong>多路复用</strong>。在升级之后，浏览器和服务器之间只需要建立一个 TCP 连接，在这个连接里可以同时并行下载多个文件，不用再一个个等了。开启之后，加载速度果然提上去了。</p>
<h1>最后</h1>
<p>全部搞完之后，重新跑了一遍 Lighthouse，各项指标终于全绿，冲破了 95 大关。</p>
<p>性能调优从来没有一劳永逸的银弹，纯粹是个抠细节的活儿，一点一点把能省的地方都省下来罢了。</p>
]]></description>
      <content:encoded><![CDATA[<p>前段时间心血来潮写了个练手项目，本来只是个随便玩的玩具，但写完后强迫症犯了，非要测测 Lighthouse 跑分，看看能不能搞个“秒开”。结果在优化过程中踩了不少坑，这里随便记录一下。</p>
<p>这里记录一下我在这场“白屏歼灭战”中踩过的坑和积累的经验。</p>
<h1>换掉 Webpack 之后，Vite 真的开箱即快吗？</h1>
<p>在打包工具的选择上，最开始我还是按照老习惯，直接用 Webpack 搭建了基础配置。（毕竟生态成熟，省一点事是一点事）。
Webpack 也确实很稳，各种 Loader 和 Plugin 往上一堆，基本搞定了自己想要的代码转译和打包功能。但是！作为个人项目，它的体验真的越来越让人心累。虽然代码量不算庞大，不至于每次 HMR 卡死，但每次修改一行代码，按保存后都要盯着终端等上好几秒，太打断思路了。</p>
<p>忍无可忍之下，我花了半天把构建工具整体迁移到了 Vite。</p>
<p>得益于浏览器原生的 ES Module 支持和底层的 ESBuild，Vite 在本地开发时的冷启动和热更新速度简直堪称魔法，几乎做到了真正的“毫秒级响应”。</p>
<p>但这有个很容易踩的坑：<strong>本地开发快，不代表打包出来的线上代码也快。</strong></p>
<p>Vite 在打包生产环境代码时，底层依赖的依然是 Rollup。对于个人项目来说，即使你自己写的代码量不大，但如果你图省事，把所有的页面组件、甚至偶尔用到的第三方依赖（比如图表库、动画库）全部默认打包进同一个 index.js 里，产物依然会变成一个略显臃肿的包。</p>
<p>在千兆宽带下，这点体积或许连零点几秒的白屏都感觉不到。但一跑 Lighthouse 的模拟弱网测试，这就成了扣分的重灾区。</p>
<p>只要你一进首页，浏览器就不得不去下载和解析那些属于其他子页面的、“当前毫无用处”的 JS 代码。Lighthouse 的报告上毫不留情地给你标红：“Reduce unused JavaScript”（减少未使用的 JS） 以及 “Minimize main-thread work”（最小化主线程工作）。白屏时间并不会因为你用了 Vite 而凭空消失。</p>
<p>最终，我还是老老实实地按路由进行了代码分割（Code Splitting），才算把首屏不需要的 JS 负担卸下来。</p>
<h1>毁掉首屏的罪魁祸首：那张 5MB 的 Hero 大图</h1>
<p>解决了代码体积，加载速度依然不达标。排查 Network 面板后，我找到了真正的“性能杀手”。</p>
<p>为了让网站一打开就具备极强的视觉冲击力，我在首屏放置了一张全屏的 Hero 高清视觉图。当时图省事，直接拿了一张高达 5MB 的原图，用原生的 <code>&lt;img&gt;</code> 标签硬塞进了页面。</p>
<p>在弱网环境下，这张大图直接把浏览器的下载通道占满了，导致后续的关键文件都被堵在后面排队。</p>
<p>既然找到了病因，接下来就是对症下药。针对图片，我主要做了两件事：</p>
<p><strong>第一步：引入 WebP/AVIF 格式与优雅降级。</strong></p>
<p>提到现代图片格式，很多人第一反应是 WebP，大家都知道用 WebP 压缩图片，但我这次试了一下 AVIF。</p>
<p>作为基于 AV1 视频编码技术的新一代图像格式，AVIF 的压缩率简直令人发指。相比于已经很优秀的 WebP，它还能再缩小 20%~30% 的体积。</p>
<p>然而，享受极限性能是要付出代价的。AVIF 虽然美好，却有两个极其扎眼的缺点：</p>
<p>首先是高昂的编码成本。由于压缩算法极其复杂，我在本地用脚本将大图转码为 AVIF 时，电脑的 CPU 风扇直接起飞，转换一张图片的耗时比生成 WebP 慢了好几倍。如果项目里有海量图片，构建时间的延长会是个大麻烦。</p>
<p>其次是残酷的兼容性断层。虽然 Chrome 和较新的 Safari 都已支持 AVIF，但在一些老旧设备（比如 iOS 16 以下的 iPhone）或非主流浏览器上，它依然是一片无法解析的空白。</p>
<p>为了在“极致跑分”和“稳健展示”之间找到完美的平衡，我放弃了单一的 <code>&lt;img&gt;</code> 标签，果断采用了 HTML5 的 <code>&lt;picture&gt;</code> 标签来实现多级优雅降级：首选极速且极致压缩的 AVIF，备选体积稍大的 WebP，这样一来，支持 AVIF 的现代浏览器能享受到秒开的快感，而老旧设备则会乖乖回退加载常规图片。</p>
<p><strong>第二步：祭出按需加载（Lazy Loading）。</strong>
虽然首屏的 Hero 图必须立刻加载，但页面下方（不在首屏可视区域内）的大量配图完全没必要在第一时间抢占带宽。我利用浏览器的 <code>Intersection Observer</code> API 封装了一个自定义 Hook，监听这些 DOM 元素是否进入视口，只有当用户滚动到附近时，才将真实的图片 URL 赋给 <code>src</code> 属性。这一步直接将首屏的并发请求数砍掉了一大半。</p>
<p>（不过为了追求极致的跑分，我最后还是忍痛把首屏那张 5MB 的背景图给删了，QAQ）</p>
<h1>顺手做一下产物压缩</h1>
<p>搞定了最棘手的大图和冗余 JS，Lighthouse 的跑分其实已经非常好看了。但这依然满足不了我的强迫症，在 Network 面板里，我盯上了那堆打包出来的 HTML、CSS 和 JS 小文件。</p>
<p>纯文本文件在网络传输中其实有着巨大的压缩潜力。为此，我在 Vite 配置中引入了体积优化：</p>
<p><strong>1. 开启 Gzip 与 Brotli 文本压缩</strong></p>
<p>我通过配置 <code>vite-plugin-compression</code> 插件，在打包时直接对文本产物进行高强度压缩。除了常规的 Gzip，我还特意开启了压缩率更高的 Brotli（<code>.br</code>）格式：</p>
<pre><code class="language-javascript">// vite.config.js
plugins: [
  viteCompression({ algorithm: 'gzip' }),
  viteCompression({ algorithm: 'brotliCompress' })
]
</code></pre>
<p>这样做的好处是，Vite 会在打包时直接生成打包好的 <code>.gz</code> 和 <code>.br</code> 文件。当配置好 Nginx 服务器后，面对用户的访问，服务器不再需要实时消耗 CPU 去动态压缩文件，而是直接把提前准备好的 <code>.gz</code> 或 <code>.br</code> 文件顺着网线扔给浏览器，浏览器再利用原生能力瞬间解压渲染。这一步，让文本资源的传输体积再次暴降了 70% 左右。</p>
<p><strong>2. 静态碎图的自动化瘦身</strong></p>
<p>除了那张被干掉的 Hero 大图，项目里其实还散落着不少 UI 切图、Logo 和占位小图。为了避免以后每次加新图都要手动去 TinyPNG 压一遍，我直接接上了 <code>vite-plugin-imagemin</code> 插件。每次执行 <code>build</code> 构建时，它会在本地默默把所有的图片素材进行无损或微损的批量极限压缩，彻底杜绝了因疏忽导致大图上线的可能。</p>
<h1>解决拆包带来的新问题：拥抱 HTTP/2</h1>
<p>把前面这些都做完后，我再看 Network 面板，发现 Waterfall（请求瀑布流）还是有一点阶梯感。</p>
<p>仔细一想，这其实是我前面做的代码分割带来的副作用：拆包固然减小了单个 JS 的体积，但也导致首屏需要同时请求多个小 Chunk 文件。而在传统的 HTTP/1.1 协议下，浏览器对同一个域名的并发请求数通常限制在 6 个左右，超过的数量只能在队列里傻等（队头阻塞）。</p>
<p>资源再小，排队也要花时间。为了解决这个问题，我给服务器配置了 HTTPS（这是使用现代 Web 协议的先决条件），并开启了 HTTP/2。</p>
<p>HTTP/2 最大的优势就是支持 <strong>多路复用</strong>。在升级之后，浏览器和服务器之间只需要建立一个 TCP 连接，在这个连接里可以同时并行下载多个文件，不用再一个个等了。开启之后，加载速度果然提上去了。</p>
<h1>最后</h1>
<p>全部搞完之后，重新跑了一遍 Lighthouse，各项指标终于全绿，冲破了 95 大关。</p>
<p>性能调优从来没有一劳永逸的银弹，纯粹是个抠细节的活儿，一点一点把能省的地方都省下来罢了。</p>
]]></content:encoded>
      <media:content url="https://yuhhhy.cn/images/20260308-web-app.avif" medium="image" type="image/avif"/>
    </item>
    <item>
      <title>本站更新 RSS 以及好用的 VSCode 插件推荐</title>
      <link>https://yuhhhy.cn/blog/mjllqqs1-26</link>
      <guid isPermaLink="true">https://yuhhhy.cn/blog/mjllqqs1-26</guid>
      <pubDate>Thu, 25 Dec 2025 15:33:03 GMT</pubDate>
      <dc:creator><![CDATA[一曝十寒]]></dc:creator>
      <description><![CDATA[<p>封面图片来源于我跟使用 FLUX 生图插件的 gemini，以及豆包的对话。
<img src="https://yuhhhy.cn/images/20251225-rss-0.avif" alt="失败的AI生图1">
<img src="https://yuhhhy.cn/images/20251225-rss-1.avif" alt="失败的AI生图2"></p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-2.avif" alt="失败的AI生图3"></p>
<p>只能说是失败……</p>
<h1>什么是 RSS</h1>
<p>知道的都知道，RSS 是一个聚合订阅神器。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-3.avif" alt="RSS"></p>
<h2>为何 RSS？</h2>
<p>对我这样的懒人来说，我是绝不愿意一个一个软件打开，去看更新了什么内容的，更何况更多时候我连手机弹出的消息通知都不愿意看……这时候我就幻想：要是有一个助手，可以帮我浏览所有通知，摘取其中的关键信息后再呈递给我就好了。</p>
<p>而 RSS 就是……哦不，RSS 不是这样的东西，RSS 只是这样一个工具，它方便我们在一个地方，阅览各个平台和网站的内容。</p>
<p>从技术方面来说，拥有 RSS 服务的网站，会提供一个 XML 文件，包含我们订阅的内容。 平台的更新，也会同步到这个 XML 文件上。而 RSS 软件可以定期抓取这些更新，并且呈现出来。</p>
<h2>我使用的 RSS 工具</h2>
<p>对于原生提供 RSS 的网页，可以直接订阅，而对于没有 RSS 链接的网页，我用的是浏览器 RSSHub 插件 + RSSHub + Folo 的流。</p>
<p>浏览器插件负责捕获网页内容并匹配对应的 RSS 规则，然后生成对应的 RSSHub 订阅链接，用 Folo 订阅这些 RSS 链接并呈现。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-4.avif" alt="RSSHub Radar"></p>
<p>因为 RSSHub 的官方订阅链接经常被一些反爬严格的网站挡在外面，所以我用 <a href="https://zeabur.com/">Zeabur</a> 部署了自己的 RSSHub，Zeabur 只要有 Google 账号就可以注册，每个月送 5 刀的额度，完全够用。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-5.avif" alt="zeabur"></p>
<p>部署完成后，只需要将原本的 rsshub.app 改为你自己部署好的域名，比如我的是 yuhhhyrss.zeabur.app，同样地在 RSSHub Radar 中配置好，那么浏览器插件捕获到的 RSS 链接，就会优先使用自己部署的实例链接。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-24.avif" alt="RSSHub设置"></p>
<h2>本站 RSS</h2>
<p>经过一个晚上的努力，我终于在 2025年12月12日，为网站添加了 RSS 订阅功能，现在可以通过 RSS 订阅我的博客啦。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-6.avif" alt="本站RSS"></p>
<h1>好用的 VSCode 插件推荐</h1>
<p>顺便介绍一下我一直以来使用的 &amp; 觉得好用的 VSCode 插件，每一款都是我无法放弃的存在，是我使用 VSCode 的很大程度上的理由。</p>
<h2>必备插件</h2>
<p>这是作为一名 VSCoder，不得不下的插件。</p>
<h3>1. Chinese (Simplified)</h3>
<p>VSCode 的简体中文语言包。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-7.avif" alt="简体中文插件"></p>
<h3>2. vscode-icons</h3>
<p>VSCode 文件图标主题，没有它，在 VSCode 中浏览文件索然无味。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-8.avif" alt="文件图标主题插件"></p>
<p>实际效果展示：</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-9.avif" alt="文件图标主题插件展示"></p>
<h3>3. FiraCode font</h3>
<p>超好看的编程专用字体。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-10.avif" alt="FiraCodeFont"></p>
<p>实际效果展示：</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-11.avif" alt="FiraCodeFont效果展示"></p>
<h3>4. One Dark Pro</h3>
<p>最受欢迎主题插件，不必多说。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-12.avif" alt="One Dark Pro"></p>
<p>效果展示：</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-13.avif" alt="One Dark Pro效果展示1"></p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-14.avif" alt="One Dark Pro效果展示2"></p>
<h2>开发插件</h2>
<p>提升开发者使用体验的插件。</p>
<h3>1. Prettier</h3>
<p>代码格式化插件。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-15.avif" alt="Prettier"></p>
<h3>2. Better Comments</h3>
<p>增强注释。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-16.avif" alt="Better Comments"></p>
<h3>3. CodeSnap</h3>
<p>代码截图。适合用于展示你的代码。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-17.avif" alt="CodeSnap"></p>
<p>举例：</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-18.avif" alt="CodeSnap效果展示"></p>
<p>这截屏莫名装逼的感觉是怎么回事。</p>
<h3>4. Live Server</h3>
<p>HTML 实时预览。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-19.avif" alt="Live Server"></p>
<h3>5. CSS Peek</h3>
<p>CSS 快速查看。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-20.avif" alt="CSS Peek"></p>
<h3>6. <a href="http://draw.io/">Draw.io</a> Integration</h3>
<p>流程图绘制。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-21.avif" alt="Draw.io"></p>
<p>可以画出这样的图：</p>
<p>![[20251225-rss-22.png]]</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-22.avif" alt="Draw.io效果展示"></p>
<h3>7. Code Spell Checker</h3>
<p>检查你的单词拼写错误。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-23.avif" alt="Code Spell Checker"></p>
<p>以上就是所有内容了，谢谢看到这里的你，圣诞快乐🎄。</p>
]]></description>
      <content:encoded><![CDATA[<p>封面图片来源于我跟使用 FLUX 生图插件的 gemini，以及豆包的对话。
<img src="https://yuhhhy.cn/images/20251225-rss-0.avif" alt="失败的AI生图1">
<img src="https://yuhhhy.cn/images/20251225-rss-1.avif" alt="失败的AI生图2"></p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-2.avif" alt="失败的AI生图3"></p>
<p>只能说是失败……</p>
<h1>什么是 RSS</h1>
<p>知道的都知道，RSS 是一个聚合订阅神器。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-3.avif" alt="RSS"></p>
<h2>为何 RSS？</h2>
<p>对我这样的懒人来说，我是绝不愿意一个一个软件打开，去看更新了什么内容的，更何况更多时候我连手机弹出的消息通知都不愿意看……这时候我就幻想：要是有一个助手，可以帮我浏览所有通知，摘取其中的关键信息后再呈递给我就好了。</p>
<p>而 RSS 就是……哦不，RSS 不是这样的东西，RSS 只是这样一个工具，它方便我们在一个地方，阅览各个平台和网站的内容。</p>
<p>从技术方面来说，拥有 RSS 服务的网站，会提供一个 XML 文件，包含我们订阅的内容。 平台的更新，也会同步到这个 XML 文件上。而 RSS 软件可以定期抓取这些更新，并且呈现出来。</p>
<h2>我使用的 RSS 工具</h2>
<p>对于原生提供 RSS 的网页，可以直接订阅，而对于没有 RSS 链接的网页，我用的是浏览器 RSSHub 插件 + RSSHub + Folo 的流。</p>
<p>浏览器插件负责捕获网页内容并匹配对应的 RSS 规则，然后生成对应的 RSSHub 订阅链接，用 Folo 订阅这些 RSS 链接并呈现。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-4.avif" alt="RSSHub Radar"></p>
<p>因为 RSSHub 的官方订阅链接经常被一些反爬严格的网站挡在外面，所以我用 <a href="https://zeabur.com/">Zeabur</a> 部署了自己的 RSSHub，Zeabur 只要有 Google 账号就可以注册，每个月送 5 刀的额度，完全够用。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-5.avif" alt="zeabur"></p>
<p>部署完成后，只需要将原本的 rsshub.app 改为你自己部署好的域名，比如我的是 yuhhhyrss.zeabur.app，同样地在 RSSHub Radar 中配置好，那么浏览器插件捕获到的 RSS 链接，就会优先使用自己部署的实例链接。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-24.avif" alt="RSSHub设置"></p>
<h2>本站 RSS</h2>
<p>经过一个晚上的努力，我终于在 2025年12月12日，为网站添加了 RSS 订阅功能，现在可以通过 RSS 订阅我的博客啦。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-6.avif" alt="本站RSS"></p>
<h1>好用的 VSCode 插件推荐</h1>
<p>顺便介绍一下我一直以来使用的 &amp; 觉得好用的 VSCode 插件，每一款都是我无法放弃的存在，是我使用 VSCode 的很大程度上的理由。</p>
<h2>必备插件</h2>
<p>这是作为一名 VSCoder，不得不下的插件。</p>
<h3>1. Chinese (Simplified)</h3>
<p>VSCode 的简体中文语言包。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-7.avif" alt="简体中文插件"></p>
<h3>2. vscode-icons</h3>
<p>VSCode 文件图标主题，没有它，在 VSCode 中浏览文件索然无味。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-8.avif" alt="文件图标主题插件"></p>
<p>实际效果展示：</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-9.avif" alt="文件图标主题插件展示"></p>
<h3>3. FiraCode font</h3>
<p>超好看的编程专用字体。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-10.avif" alt="FiraCodeFont"></p>
<p>实际效果展示：</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-11.avif" alt="FiraCodeFont效果展示"></p>
<h3>4. One Dark Pro</h3>
<p>最受欢迎主题插件，不必多说。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-12.avif" alt="One Dark Pro"></p>
<p>效果展示：</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-13.avif" alt="One Dark Pro效果展示1"></p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-14.avif" alt="One Dark Pro效果展示2"></p>
<h2>开发插件</h2>
<p>提升开发者使用体验的插件。</p>
<h3>1. Prettier</h3>
<p>代码格式化插件。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-15.avif" alt="Prettier"></p>
<h3>2. Better Comments</h3>
<p>增强注释。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-16.avif" alt="Better Comments"></p>
<h3>3. CodeSnap</h3>
<p>代码截图。适合用于展示你的代码。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-17.avif" alt="CodeSnap"></p>
<p>举例：</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-18.avif" alt="CodeSnap效果展示"></p>
<p>这截屏莫名装逼的感觉是怎么回事。</p>
<h3>4. Live Server</h3>
<p>HTML 实时预览。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-19.avif" alt="Live Server"></p>
<h3>5. CSS Peek</h3>
<p>CSS 快速查看。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-20.avif" alt="CSS Peek"></p>
<h3>6. <a href="http://draw.io/">Draw.io</a> Integration</h3>
<p>流程图绘制。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-21.avif" alt="Draw.io"></p>
<p>可以画出这样的图：</p>
<p>![[20251225-rss-22.png]]</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-22.avif" alt="Draw.io效果展示"></p>
<h3>7. Code Spell Checker</h3>
<p>检查你的单词拼写错误。</p>
<p><img src="https://yuhhhy.cn/images/20251225-rss-23.avif" alt="Code Spell Checker"></p>
<p>以上就是所有内容了，谢谢看到这里的你，圣诞快乐🎄。</p>
]]></content:encoded>
      <media:content url="https://yuhhhy.cn/images/20251225-rss-0.avif" medium="image" type="image/avif"/>
    </item>
    <item>
      <title>我之游戏观</title>
      <link>https://yuhhhy.cn/blog/mizqz6fl-2x</link>
      <guid isPermaLink="true">https://yuhhhy.cn/blog/mizqz6fl-2x</guid>
      <pubDate>Wed, 10 Dec 2025 08:28:39 GMT</pubDate>
      <dc:creator><![CDATA[若文Roving]]></dc:creator>
      <description><![CDATA[<p>本文为转载。</p>
<p>作者(Author)：若文</p>
<p>来源(Source)：<a href="https://www.bilibili.com/opus/543643710355183918?spm_id_from=333.1387.0.0">https://www.bilibili.com/opus/543643710355183918?spm_id_from=333.1387.0.0</a></p>
<hr>
<p>游戏二字，原意为游乐嬉戏，随着时代发展为了“与生活和劳动技能有关的、能够促进体力和智力发展的娱乐性活动”之意，而我在此想要谈的，仅是电子游戏，即在英文和西方游戏界语境下包含了影像游戏（video game）和听觉游戏（audio game）等语义的“游戏”。以我观之，游戏（电子游戏）是一个融文学、音乐、戏剧、绘画等于一体的、与电影接近但不相仿的、本质是以低成本方式带给人视觉、听觉、情绪、精神上感受的娱乐商品。对于如今正在游戏设计上学习的现在的我，游戏观即是游戏是什么，游戏的本质是什么，什么是好的游戏，游戏在个人生活和整个社会中的位置和价值是什么这四个主要问题。在此做出这般称述是因为既然要谈游戏观，那么就先得分清什么是游戏观不可，不然就只能像一些青年人，嘴上谈着爱情观，却不知爱情观到底是什么，难免落个贻笑大方的结果。</p>
<p>对于游戏是什么和游戏的本质是什么两个问题，我已稍稍做出了一些自己的、微不足道的论述，但什么是好的游戏？好游戏的标准是什么？这个问题就如同“好的电影是什么”的问题一样，是很难得出一个客观的、标准的答案的，与文化市场息息相关的复杂的问题，我就此也只能说些主观的、个人的，并且大部分是有关单机游戏的想法，同时我也不得不将一些我所见到的误区拿来一同谈论。</p>
<p>要谈好的游戏是什么，我以为要从游戏设计中的三个概念出发，一是游戏机制（Game Mechanics），二是游戏玩法（Gameplay），三是内容设计（Content Design）。</p>
<blockquote>
<p>游戏机制（Game Mechanics）是构成一个电子游戏的基本元素之一，可以理解为游戏的规则；这些基本规则相互搭配，构成了一个模型，决定了玩家和游戏之间如何进行互动，这个互动形式就叫“玩法（Gameplay）”；除去机制和玩法之外的其他艺术表现，称为“内容（Content Design）”。</p>
</blockquote>
<p>游戏机制和游戏玩法实际上是游戏设计中的一体两面，游戏玩法是游戏机制的外在表现，游戏机制是游戏玩法的内在根基。内容设计可以理解为除去机制和玩法以外的囊括了故事设计、音乐、画面等的艺术表现，展开到游戏中就是游戏叙事、美术风格、音乐风格、故事背景等。从这三方面来评判一款游戏，我认为是具有其科学性的，我同时认为，游戏玩法、游戏机制是玩家与游戏之间最直接的联系，是游戏的内在灵魂，游戏内容是游戏的外在灵魂，二者缺一不可。在这长时间的网上冲浪里，长时间作为玩家的我常常能见到许多对游戏是否是一个好游戏的误区，这并不只玩家，游戏设计者、游戏厂商也在其列。其一，对游戏玩法好的理解是画面是否逼真，“精细到毛孔都可以看清”，“可以左手右手分别拿左边右边的东西”（此条有特指某个单机游戏）；其二，对游戏难度的理解是粗暴堆数值；其三，对游戏叙事、游戏剧情好的理解是频繁的CG播片，电影运镜，剧本是否有深度，是否富含哲学思考；其四，对运营好的理解是送的东西是不是够多，对用户粘性的理解是每日任务是不是够多，等等。游戏说到底终究是个娱乐品，对其中概念的不了解、认识偏差是理所当然的，但像这般的认识跑偏，甚至是完全的曲解则是令人不得不出来指正的，不这样去指正，不这样去让玩家，让设计者明白自己认识中的错误，好的作品是绝不可能诞生的。</p>
<p>好的游戏玩法，好的游戏机制绝不是纯粹的拟真，绝不仅是人物角色能够做出和现实一样的动作。好的叙事，绝不是几个角色在那讳莫如深地长聊，绝不是满篇哲学与隐喻，绝不是在那将角色之身化为撰写者单调的、麻木的，甚至偏激的符号。好的运营，绝不是成天考虑如何提高用户粘度，绝不是成天钻研用户数据，设计各式各样的氪金礼包。游戏好玩，绝不应该只用眼睛去看，必须要真正玩才可以得出答案，游戏也万不可成为电影的替代品。我以为的好的游戏玩法，不是非要创新，非要从零开始开创一个全新的玩法才能叫好的玩法，这样的要求无论对谁都太苛刻了，而且并非新的就是好的，如何将现有的玩法结合起来，加法也好，减法也罢，从中取舍，从机制、从玩法出发，创造出新的、具备足够可玩性的体验，就是一个合格的，也能说是好的玩法。内容设计也应该围绕玩法展开，设计者当然可以在玩法和内容中有所偏重，但一定不能忽视玩法对于一款游戏的核心地位。游戏中的故事，展现出来就是游戏叙事，现在的游戏想要做好，想要大卖，除非纯粹的全游戏都灌注于可玩性的游戏，不然就必须有叙事，必须考虑叙事的问题，我以为优秀的游戏叙事，不结合玩法展开是不行的，当然我并不是说纯粹地讲一个好故事是不好的，优秀的故事无论在何处都是极好的，但作为拥有着其他任何叙事方式都无法拥有的极高互动性的游戏，不将叙事和玩法结合起来是极为可惜的，所以我以为，优秀的游戏叙事，除了故事本身的素质之外，也要考虑和游戏玩法的结合。美术风格、音乐风格和故事一起作为游戏的外在灵魂，自有个人的评判标准，并会随着时代不断变化，一千个人有一千个哈姆雷特，我亦自认无法提出何种见解。笼统来说，唯有将游戏的内外灵魂统一起来，一款游戏才能称得上好游戏。</p>
<p>好游戏的问题就到这儿，最后聊聊我眼中游戏和个人、和社会的关系，也即游戏的文化和社会价值。游戏作为融会了文学、音乐、绘画的娱乐品，先天就带有了这三门艺术的文化属性，也自然存在这三门艺术所拥有的部分文化价值和社会价值，像是游戏剧情之于小说，对于那些注重剧情的游戏，其必然能对玩游戏的个人产生与小说相近的影响，现代人的生活是不能不缺乏娱乐的，又因游戏这一娱乐的种类多样性、互动性、通俗性，它势必会在年轻大众中传播，势必会对年轻大众的个人成长造成影响，也势必能成为向年轻的大众传播文化思想的平台。但同样的，因为游戏其娱乐和商品的本质，游戏在中短时间内绝无可能成为传播文化的主战场，也同样因为其娱乐性，游戏不可避免地存在成为统治阶层愚弄人民的工具，成为转移人民对社会矛盾的焦点的工具这一可能，即成为奶头乐理论中所描述的现象的可能，也因为商品的本质，游戏在真正成为一门艺术的道路上，仍有数不清的社会、文化障碍。</p>
<p>现今的游戏已经凭借其繁多的品类（RPG、MOBA、MMORPG、FPS等），宽广的平台（主机平台，windows平台，移动端平台），廉价的成本占据了以青少年、青年为主的，年龄范围极广的人群的娱乐时间，成为了他们的娱乐用品，甚至成为了部分人的必需品，随着社会压力的继续增加，因为游戏的特性，人们对游戏的娱乐需求也会相应增加，游戏也必将进一步发展，虽不知游戏未来将发展成何种样貌，社会地位又将如何变化，但不可忘记的是游戏既是产业，也是文化。</p>
<p>这篇文字到此就差不多为止了，但在结束前仍有一事不得不明，即我写下这些文字究竟为何这个问题。简单来说我的想法有二，一是将我现在的所思所想留存下来，递交给今后的我，如若今后真耕耘于游戏行业，此篇文字做笑料亦做警醒，二是希望能将我这微弱的声响传递出去，不求回响，只要能传递出去就好。在此种思绪之下，我便不以任何游戏为事例来展开整篇文字了。</p>
<hr>
<p>[1] 游戏机制（Game Mechanics）是让游戏得以正常运作的设计及系统的统称。《游戏机制：高级游戏设计技术》（Ernest Adams、Joris Dormans著，人民邮电出版社，2014年）一书中写道：“在游戏设计中，游戏机制居于核心地位。”</p>
<p>另注：核心机制（Core Mechanics）是指让玩家觉得一款游戏又去的游戏系统其核心部分的机制。《游戏机制：高级游戏设计技术》（Ernest Adams、Joris Dormans著，人民邮电出版社，2014年）中写道：“核心机制这个术语经常用于指代那些具有影响力的机制。这些机制能够影响游戏的许多方面，并与其他重要性较低的机制（比如控制某一个游戏元素的机制）相互作用。”本文中所讲的游戏机制在语义上有核心机制的含义。</p>
<p>[2] Gameplay is the interactive aspects of video game design. Gameplay involves player interaction with the game, usually for the purpose of gameplay is entertainment, education or training.</p>
<p>[3] Content design is the creation of characters, items, puzzles and missions.</p>
<p>[4] Playability is the ease by which the game can be played or the quantity or duration that a game can be played and is a common measure of the quality of gameplay.[18] Playability evaluative methods target games to improve design while player experience evaluative methods target players to improve gaming.&quot;This is not to be confused with the ability to control (or play) characters in multi-character games such as role playing games or fighting games, or factions in real-time strategy games.</p>
<p>参考文献</p>
<p>[1]（美）Ernest Adams,(美)Joris Dormans.游戏机制：高级游戏设计技术[M].人民邮电出版社.2014</p>
<p>[2]Bates, Bob. Game Design (2nd ed.)[M]. Thomson Course Technology. 2004.</p>
<p>[3]Wikipedia英文站Video game design、Gameplay词条</p>
]]></description>
      <content:encoded><![CDATA[<p>本文为转载。</p>
<p>作者(Author)：若文</p>
<p>来源(Source)：<a href="https://www.bilibili.com/opus/543643710355183918?spm_id_from=333.1387.0.0">https://www.bilibili.com/opus/543643710355183918?spm_id_from=333.1387.0.0</a></p>
<hr>
<p>游戏二字，原意为游乐嬉戏，随着时代发展为了“与生活和劳动技能有关的、能够促进体力和智力发展的娱乐性活动”之意，而我在此想要谈的，仅是电子游戏，即在英文和西方游戏界语境下包含了影像游戏（video game）和听觉游戏（audio game）等语义的“游戏”。以我观之，游戏（电子游戏）是一个融文学、音乐、戏剧、绘画等于一体的、与电影接近但不相仿的、本质是以低成本方式带给人视觉、听觉、情绪、精神上感受的娱乐商品。对于如今正在游戏设计上学习的现在的我，游戏观即是游戏是什么，游戏的本质是什么，什么是好的游戏，游戏在个人生活和整个社会中的位置和价值是什么这四个主要问题。在此做出这般称述是因为既然要谈游戏观，那么就先得分清什么是游戏观不可，不然就只能像一些青年人，嘴上谈着爱情观，却不知爱情观到底是什么，难免落个贻笑大方的结果。</p>
<p>对于游戏是什么和游戏的本质是什么两个问题，我已稍稍做出了一些自己的、微不足道的论述，但什么是好的游戏？好游戏的标准是什么？这个问题就如同“好的电影是什么”的问题一样，是很难得出一个客观的、标准的答案的，与文化市场息息相关的复杂的问题，我就此也只能说些主观的、个人的，并且大部分是有关单机游戏的想法，同时我也不得不将一些我所见到的误区拿来一同谈论。</p>
<p>要谈好的游戏是什么，我以为要从游戏设计中的三个概念出发，一是游戏机制（Game Mechanics），二是游戏玩法（Gameplay），三是内容设计（Content Design）。</p>
<blockquote>
<p>游戏机制（Game Mechanics）是构成一个电子游戏的基本元素之一，可以理解为游戏的规则；这些基本规则相互搭配，构成了一个模型，决定了玩家和游戏之间如何进行互动，这个互动形式就叫“玩法（Gameplay）”；除去机制和玩法之外的其他艺术表现，称为“内容（Content Design）”。</p>
</blockquote>
<p>游戏机制和游戏玩法实际上是游戏设计中的一体两面，游戏玩法是游戏机制的外在表现，游戏机制是游戏玩法的内在根基。内容设计可以理解为除去机制和玩法以外的囊括了故事设计、音乐、画面等的艺术表现，展开到游戏中就是游戏叙事、美术风格、音乐风格、故事背景等。从这三方面来评判一款游戏，我认为是具有其科学性的，我同时认为，游戏玩法、游戏机制是玩家与游戏之间最直接的联系，是游戏的内在灵魂，游戏内容是游戏的外在灵魂，二者缺一不可。在这长时间的网上冲浪里，长时间作为玩家的我常常能见到许多对游戏是否是一个好游戏的误区，这并不只玩家，游戏设计者、游戏厂商也在其列。其一，对游戏玩法好的理解是画面是否逼真，“精细到毛孔都可以看清”，“可以左手右手分别拿左边右边的东西”（此条有特指某个单机游戏）；其二，对游戏难度的理解是粗暴堆数值；其三，对游戏叙事、游戏剧情好的理解是频繁的CG播片，电影运镜，剧本是否有深度，是否富含哲学思考；其四，对运营好的理解是送的东西是不是够多，对用户粘性的理解是每日任务是不是够多，等等。游戏说到底终究是个娱乐品，对其中概念的不了解、认识偏差是理所当然的，但像这般的认识跑偏，甚至是完全的曲解则是令人不得不出来指正的，不这样去指正，不这样去让玩家，让设计者明白自己认识中的错误，好的作品是绝不可能诞生的。</p>
<p>好的游戏玩法，好的游戏机制绝不是纯粹的拟真，绝不仅是人物角色能够做出和现实一样的动作。好的叙事，绝不是几个角色在那讳莫如深地长聊，绝不是满篇哲学与隐喻，绝不是在那将角色之身化为撰写者单调的、麻木的，甚至偏激的符号。好的运营，绝不是成天考虑如何提高用户粘度，绝不是成天钻研用户数据，设计各式各样的氪金礼包。游戏好玩，绝不应该只用眼睛去看，必须要真正玩才可以得出答案，游戏也万不可成为电影的替代品。我以为的好的游戏玩法，不是非要创新，非要从零开始开创一个全新的玩法才能叫好的玩法，这样的要求无论对谁都太苛刻了，而且并非新的就是好的，如何将现有的玩法结合起来，加法也好，减法也罢，从中取舍，从机制、从玩法出发，创造出新的、具备足够可玩性的体验，就是一个合格的，也能说是好的玩法。内容设计也应该围绕玩法展开，设计者当然可以在玩法和内容中有所偏重，但一定不能忽视玩法对于一款游戏的核心地位。游戏中的故事，展现出来就是游戏叙事，现在的游戏想要做好，想要大卖，除非纯粹的全游戏都灌注于可玩性的游戏，不然就必须有叙事，必须考虑叙事的问题，我以为优秀的游戏叙事，不结合玩法展开是不行的，当然我并不是说纯粹地讲一个好故事是不好的，优秀的故事无论在何处都是极好的，但作为拥有着其他任何叙事方式都无法拥有的极高互动性的游戏，不将叙事和玩法结合起来是极为可惜的，所以我以为，优秀的游戏叙事，除了故事本身的素质之外，也要考虑和游戏玩法的结合。美术风格、音乐风格和故事一起作为游戏的外在灵魂，自有个人的评判标准，并会随着时代不断变化，一千个人有一千个哈姆雷特，我亦自认无法提出何种见解。笼统来说，唯有将游戏的内外灵魂统一起来，一款游戏才能称得上好游戏。</p>
<p>好游戏的问题就到这儿，最后聊聊我眼中游戏和个人、和社会的关系，也即游戏的文化和社会价值。游戏作为融会了文学、音乐、绘画的娱乐品，先天就带有了这三门艺术的文化属性，也自然存在这三门艺术所拥有的部分文化价值和社会价值，像是游戏剧情之于小说，对于那些注重剧情的游戏，其必然能对玩游戏的个人产生与小说相近的影响，现代人的生活是不能不缺乏娱乐的，又因游戏这一娱乐的种类多样性、互动性、通俗性，它势必会在年轻大众中传播，势必会对年轻大众的个人成长造成影响，也势必能成为向年轻的大众传播文化思想的平台。但同样的，因为游戏其娱乐和商品的本质，游戏在中短时间内绝无可能成为传播文化的主战场，也同样因为其娱乐性，游戏不可避免地存在成为统治阶层愚弄人民的工具，成为转移人民对社会矛盾的焦点的工具这一可能，即成为奶头乐理论中所描述的现象的可能，也因为商品的本质，游戏在真正成为一门艺术的道路上，仍有数不清的社会、文化障碍。</p>
<p>现今的游戏已经凭借其繁多的品类（RPG、MOBA、MMORPG、FPS等），宽广的平台（主机平台，windows平台，移动端平台），廉价的成本占据了以青少年、青年为主的，年龄范围极广的人群的娱乐时间，成为了他们的娱乐用品，甚至成为了部分人的必需品，随着社会压力的继续增加，因为游戏的特性，人们对游戏的娱乐需求也会相应增加，游戏也必将进一步发展，虽不知游戏未来将发展成何种样貌，社会地位又将如何变化，但不可忘记的是游戏既是产业，也是文化。</p>
<p>这篇文字到此就差不多为止了，但在结束前仍有一事不得不明，即我写下这些文字究竟为何这个问题。简单来说我的想法有二，一是将我现在的所思所想留存下来，递交给今后的我，如若今后真耕耘于游戏行业，此篇文字做笑料亦做警醒，二是希望能将我这微弱的声响传递出去，不求回响，只要能传递出去就好。在此种思绪之下，我便不以任何游戏为事例来展开整篇文字了。</p>
<hr>
<p>[1] 游戏机制（Game Mechanics）是让游戏得以正常运作的设计及系统的统称。《游戏机制：高级游戏设计技术》（Ernest Adams、Joris Dormans著，人民邮电出版社，2014年）一书中写道：“在游戏设计中，游戏机制居于核心地位。”</p>
<p>另注：核心机制（Core Mechanics）是指让玩家觉得一款游戏又去的游戏系统其核心部分的机制。《游戏机制：高级游戏设计技术》（Ernest Adams、Joris Dormans著，人民邮电出版社，2014年）中写道：“核心机制这个术语经常用于指代那些具有影响力的机制。这些机制能够影响游戏的许多方面，并与其他重要性较低的机制（比如控制某一个游戏元素的机制）相互作用。”本文中所讲的游戏机制在语义上有核心机制的含义。</p>
<p>[2] Gameplay is the interactive aspects of video game design. Gameplay involves player interaction with the game, usually for the purpose of gameplay is entertainment, education or training.</p>
<p>[3] Content design is the creation of characters, items, puzzles and missions.</p>
<p>[4] Playability is the ease by which the game can be played or the quantity or duration that a game can be played and is a common measure of the quality of gameplay.[18] Playability evaluative methods target games to improve design while player experience evaluative methods target players to improve gaming.&quot;This is not to be confused with the ability to control (or play) characters in multi-character games such as role playing games or fighting games, or factions in real-time strategy games.</p>
<p>参考文献</p>
<p>[1]（美）Ernest Adams,(美)Joris Dormans.游戏机制：高级游戏设计技术[M].人民邮电出版社.2014</p>
<p>[2]Bates, Bob. Game Design (2nd ed.)[M]. Thomson Course Technology. 2004.</p>
<p>[3]Wikipedia英文站Video game design、Gameplay词条</p>
]]></content:encoded>
      <media:content url="https://yuhhhy.cn/images/game-0.avif" medium="image" type="image/avif"/>
    </item>
    <item>
      <title>网站性能优化记录</title>
      <link>https://yuhhhy.cn/blog/mfjs2frb-fo</link>
      <guid isPermaLink="true">https://yuhhhy.cn/blog/mfjs2frb-fo</guid>
      <pubDate>Sun, 14 Sep 2025 14:15:45 GMT</pubDate>
      <dc:creator><![CDATA[一曝十寒]]></dc:creator>
      <description><![CDATA[<h1>开始</h1>
<p>今天我们来尝试如何用浏览器自带的开发者工具 Lighthouse 进行网站的优化。</p>
<p>打开开发者工具的方法有很多种，一种是 F12，一种是 Ctrl + Shift + I 组合键，还有一种是鼠标右键然后选择检查。</p>
<p>打开后我们首先看到如下内容：</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-1.avif" alt="最开始Lighthouse提示的性能图"></p>
<h2>LCP</h2>
<p>接着往下看，第一个被提示要优化的要素就是 LCP。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-2.avif" alt="最开始Lighthouse显示的LCP时长"></p>
<p>LCP：Largest Contentful Paint，指的是我们首屏页面，并且是视口中（可以被用户看见）的最大的内容的渲染时间有多久。</p>
<p>可以看到我这里的 LCP 元素是一张博客封面的图片，对于页面图片加载的优化，我们首先就想到了懒加载。</p>
<p>但是其实我的这些图片已经添加了懒加载的功能，图片渲染时间过久，不是加载过多图片引起的。</p>
<p>我们暂且继续看下面的提示</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-3.avif" alt="Lighthouse提示图片可以采用更新的格式"></p>
<p>这里的提示是说，WebP 和 AVIF 的图像格式通常比 PNG 或 JPEG 有更高的压缩率。</p>
<p>那么把博客所有的 PNG / JPEG 图片 改为 WebP / AVIF 格式</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-4.avif" alt="博客所有的图片采用AVIF格式"></p>
<p>我这里使用的是名为 <a href="https://squoosh.app/">Squoosh</a> 的 Web 应用，不用下载十分方便。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-5.avif" alt="Squoosh的界面"></p>
<p>它说我的站点有 13 个请求没有使用 HTTP/2 协议，而是使用的 HTTP/1.1，</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-6.avif" alt="Lighthouse提示请求没有使用HTTP/2协议"></p>
<p>那么采用 HTTP/2 协议，并且使用 HTTPS 加密。只需要在我们站点的反向代理服务器 Nginx 配置：</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-7.avif" alt="我的Nginx服务器HTTPS配置"></p>
<p>主要是
<code>listen 443 ssl http2;</code>
<code>ssl_certificate /etc/nginx/yuhhhy.cn.crt;</code>
<code>ssl_certificate_key /etc/nginx/yuhhhy.cn.key;</code>
这三行，第一行是表示启用 SSL 加密和 HTTP/2 协议。后面两行是 SSL/TLS 证书文件在本机上的路径，建议放在与 <code>nginx.conf</code> 同一个文件夹下。
证书可以找 CA 机构免费签发，我是在腾讯云申请的，申请之后下载证书文件，里面会包含这两个文件，放在 <code>nginx.conf</code> 文件夹下，然后这样配置后重启 Nginx 应该就好了。</p>
<p>采用 HTTPS 之后一些浏览器中，在你的网站地址栏左侧会有一个锁的图标（取代原来的感叹号），代表链接是由 HTTPS 加密过的。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-8.avif" alt="网站地址栏左侧会有一个锁的图标"></p>
<h1>Accessibility</h1>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-9.avif" alt="Lighthouse提示网站的Accessibility只有87分"></p>
<p>Accessibility 只有87分，指的是网站的无障碍功能。一般情况下，我们给图片加 alt 属性，使用语义化的 HTML 标签，使用 <code>button</code> 作为按钮，<code>a</code> 作为导航，而不是 <code>div</code> 一把梭，这样是好的无障碍实践。</p>
<h2>button 按钮</h2>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-10.avif" alt="Lighthouse提示给网站的button按钮提供一个可以理解的名字"></p>
<p>第一条警告。它说的是，我的 button 按钮没有一个可以理解的名字，这不利于无障碍阅读……虽然我估计我的网站大概不会有无障碍的需求，但是我们还是修改一下好了。</p>
<p><code>aria-label</code> 属性用来在 button 元素不包含文本内容时，提供提供屏幕阅读器可读的名称。而相应地， img 元素中用 <code>alt</code> 属性提供名称，form 表单用相关联的 <code>&lt;label&gt;</code> 元素提供名称。</p>
<p>我的这个切换主题的 button 元素，内部确实没有文本，而是 icon 图标，所以可以为它添加一个 <code>aria-label=&quot;theme-toggle-button&quot;</code>，代码如下：</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-11.avif" alt="添加了aria-label=&quot;theme-toggle-button&quot;的代码"></p>
<h2>Color Contrast 对比色</h2>
<p>第二条警告，说我必须确保所有的文本与背景颜色之间有足够的颜色对比。并且给出了实际的例子</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-12.avif" alt="Lighthouse提示文本与背景颜色之间需要足够的颜色对比"></p>
<p>很显然，这个蓝色是我博客全局通用的一个颜色变量，它现在竟然说颜色的对比太弱，还给了我提供了一个对比色分析器的网址。</p>
<p>点进去看，好吧，警告全红。虽然测试全是红色，但是我感觉这示例文本看得也挺清晰啊。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-13.avif" alt="Lighthouse推荐的对比色分析器的网址，显示日间模式测试结果"></p>
<p>不过目前的颜色倒是在夜间打分挺高。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-14.avif" alt="显示夜间模式测试结果"></p>
<p>把亮色主题的文本颜色色更改之后：</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-15.avif" alt="更改后的日间模式测试结果"></p>
<p>有看着更舒服了吗……</p>
<h1>SEO 搜索引擎优化</h1>
<p>SEO（Search Engine Optimization，搜索引擎优化）是指通过优化网站，使网站在搜索引擎（如 Google、百度、Bing）的自然搜索结果（非广告）中排名更高，从而获得更多免费流量的过程。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-16.avif" alt="Lighthouse提示原先的SEO分数"></p>
<p>那么看看 SEO 有哪些内容可以提升。</p>
<h2>没有 meta 描述</h2>
<p>这里说的是我的文档缺失 meta 描述信息。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-17.avif" alt="Lighthouse提示文档缺失meta描述信息"></p>
<p>首先在我们的 <code>index.html</code> 页面添加 meta 头信息</p>
<pre><code class="language-html">    &lt;!-- 示例 --&gt;
    &lt;meta name=&quot;author&quot; content=&quot;作者名&quot; /&gt;
    &lt;meta name=&quot;description&quot; content=&quot;网站介绍&quot; /&gt;
    &lt;meta name=&quot;keywords&quot; content=&quot;关键词&quot; /&gt;
</code></pre>
<p>对于 Vue ，还可以按照需要动态添加元信息</p>
<pre><code class="language-js">// router/index.js 示例
const routes = [
  {
    path: '/home',
    name: 'Home',
    component: () =&gt; import('@/views/Home/index.vue'),
    meta: {
        title: '[博客名]'
    }
  },
  {
    path: 'archive/,
    name: 'Archive',
    component: () =&gt; import('@/views/Archive/index.vue'),
    meta: {
        title: '归档 - [博客名]'
    }
  },
  // ...
]
</code></pre>
<p>然后，添加一个全局前置守卫来完成动态的标题设置</p>
<pre><code class="language-js">router.beforeEach((to, from, next) =&gt; {
  // existing code... 
  if (to.meta?.title) {
        document.title = to.meta.title  || '[博客名]'
    }
})
</code></pre>
<p>对于所有博客文章页，可以通过 <code>@vueuse/head</code> 为每个文章添加不同的动态头信息。</p>
<p>首先安装 @vueuse/head</p>
<pre><code class="language-bash">npm install @vueuse/head
</code></pre>
<p>然后使用</p>
<pre><code class="language-js">// Blog/index.js
import { useHead } from '@vueuse/head'
import { computed } from 'vue'
import config from '@/config/index.js'

// 其它代码... 
// 先定义好pageTitle、pageDescription、pageKeywords等变量...

useHead({
    title: pageTitle,
    meta: [
        {
            name: 'description',
            content: pageDescription
        },
        {
            name: 'keywords',
            content: pageKeywords
        },
        // Open Graph标签 - 用于社交媒体分享
        {
            property: 'og:title',
            content: pageTitle
        },
        {
            property: 'og:description',
            content: pageDescription
        },
        {
            property: 'og:image',
            content: computed(() =&gt; config.baseUrl + blogData.value?.coverImage || '')
        },
        {
            property: 'og:url',
            content: computed(() =&gt; window.location.href)
        },
        {
            property: 'og:type',
            content: 'article'
        }
    ]
})
</code></pre>
<p>这样就得到动态生成的元信息了！</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-18.avif" alt="动态生成的网站元信息"></p>
<h2>没有 robots.txt 文件</h2>
<p>这是对于爬虫应该爬取网站哪些内容的说明书，如果没有 robots.txt 文件的话，似乎会直接使用主页面 index.html 作为 robots.txt</p>
<p>所以我们直接在 public 文件夹中创建 robots.txt，内容如下：</p>
<pre><code class="language-txt">echo &quot;User-agent: *
Allow: /
</code></pre>
<h1>Waterfall 瀑布图</h1>
<p>什么是瀑布图？瀑布图是浏览器的开发者工具中，Network面板里的 Waterfall 项，是一个用于展示每个网络请求的加载时间线的可视化工具。我们可以看到每一个请求从发起到响应的时间是怎么样的，以及每一个不同的请求之间的时间顺序与时间间隔。</p>
<p>瀑布图对于性能优化非常有用，特别是首屏加载的性能优化有奇效。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-19.avif" alt="Network面板里的Waterfall瀑布图"></p>
<p>在查看了瀑布流之后，Y 发现对本站而言最影响首屏加载速度的有两个，一个是网站图标 avatar.ico，它不仅仅在最开始就发起了请求，并且因为图片的体积够大（100k 以上），响应的时间很久，严重阻塞了其他资源的加载。二是和用来统计数据的请求 visit、view 等，也阻塞了其他资源。</p>
<p>那么首先，对网站图标 avatar.ico 进行压缩，最终将体积压缩到了 4.3 kb，还进行了缓存，可以说这大大地提高了首屏速度。</p>
<p>其次将数据统计请求延时，来先等待页面渲染完成，毕竟使用的体验才是最重要的！</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-20.avif" alt="延迟统计请求的代码"></p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-21.avif" alt="最终优化完成Lighthouse提示的性能图"></p>
<p>最终将 FCP 控制在了 0.4s，LCP 控制在 0.6s，还算是蛮成功的。</p>
]]></description>
      <content:encoded><![CDATA[<h1>开始</h1>
<p>今天我们来尝试如何用浏览器自带的开发者工具 Lighthouse 进行网站的优化。</p>
<p>打开开发者工具的方法有很多种，一种是 F12，一种是 Ctrl + Shift + I 组合键，还有一种是鼠标右键然后选择检查。</p>
<p>打开后我们首先看到如下内容：</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-1.avif" alt="最开始Lighthouse提示的性能图"></p>
<h2>LCP</h2>
<p>接着往下看，第一个被提示要优化的要素就是 LCP。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-2.avif" alt="最开始Lighthouse显示的LCP时长"></p>
<p>LCP：Largest Contentful Paint，指的是我们首屏页面，并且是视口中（可以被用户看见）的最大的内容的渲染时间有多久。</p>
<p>可以看到我这里的 LCP 元素是一张博客封面的图片，对于页面图片加载的优化，我们首先就想到了懒加载。</p>
<p>但是其实我的这些图片已经添加了懒加载的功能，图片渲染时间过久，不是加载过多图片引起的。</p>
<p>我们暂且继续看下面的提示</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-3.avif" alt="Lighthouse提示图片可以采用更新的格式"></p>
<p>这里的提示是说，WebP 和 AVIF 的图像格式通常比 PNG 或 JPEG 有更高的压缩率。</p>
<p>那么把博客所有的 PNG / JPEG 图片 改为 WebP / AVIF 格式</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-4.avif" alt="博客所有的图片采用AVIF格式"></p>
<p>我这里使用的是名为 <a href="https://squoosh.app/">Squoosh</a> 的 Web 应用，不用下载十分方便。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-5.avif" alt="Squoosh的界面"></p>
<p>它说我的站点有 13 个请求没有使用 HTTP/2 协议，而是使用的 HTTP/1.1，</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-6.avif" alt="Lighthouse提示请求没有使用HTTP/2协议"></p>
<p>那么采用 HTTP/2 协议，并且使用 HTTPS 加密。只需要在我们站点的反向代理服务器 Nginx 配置：</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-7.avif" alt="我的Nginx服务器HTTPS配置"></p>
<p>主要是
<code>listen 443 ssl http2;</code>
<code>ssl_certificate /etc/nginx/yuhhhy.cn.crt;</code>
<code>ssl_certificate_key /etc/nginx/yuhhhy.cn.key;</code>
这三行，第一行是表示启用 SSL 加密和 HTTP/2 协议。后面两行是 SSL/TLS 证书文件在本机上的路径，建议放在与 <code>nginx.conf</code> 同一个文件夹下。
证书可以找 CA 机构免费签发，我是在腾讯云申请的，申请之后下载证书文件，里面会包含这两个文件，放在 <code>nginx.conf</code> 文件夹下，然后这样配置后重启 Nginx 应该就好了。</p>
<p>采用 HTTPS 之后一些浏览器中，在你的网站地址栏左侧会有一个锁的图标（取代原来的感叹号），代表链接是由 HTTPS 加密过的。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-8.avif" alt="网站地址栏左侧会有一个锁的图标"></p>
<h1>Accessibility</h1>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-9.avif" alt="Lighthouse提示网站的Accessibility只有87分"></p>
<p>Accessibility 只有87分，指的是网站的无障碍功能。一般情况下，我们给图片加 alt 属性，使用语义化的 HTML 标签，使用 <code>button</code> 作为按钮，<code>a</code> 作为导航，而不是 <code>div</code> 一把梭，这样是好的无障碍实践。</p>
<h2>button 按钮</h2>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-10.avif" alt="Lighthouse提示给网站的button按钮提供一个可以理解的名字"></p>
<p>第一条警告。它说的是，我的 button 按钮没有一个可以理解的名字，这不利于无障碍阅读……虽然我估计我的网站大概不会有无障碍的需求，但是我们还是修改一下好了。</p>
<p><code>aria-label</code> 属性用来在 button 元素不包含文本内容时，提供提供屏幕阅读器可读的名称。而相应地， img 元素中用 <code>alt</code> 属性提供名称，form 表单用相关联的 <code>&lt;label&gt;</code> 元素提供名称。</p>
<p>我的这个切换主题的 button 元素，内部确实没有文本，而是 icon 图标，所以可以为它添加一个 <code>aria-label=&quot;theme-toggle-button&quot;</code>，代码如下：</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-11.avif" alt="添加了aria-label=&quot;theme-toggle-button&quot;的代码"></p>
<h2>Color Contrast 对比色</h2>
<p>第二条警告，说我必须确保所有的文本与背景颜色之间有足够的颜色对比。并且给出了实际的例子</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-12.avif" alt="Lighthouse提示文本与背景颜色之间需要足够的颜色对比"></p>
<p>很显然，这个蓝色是我博客全局通用的一个颜色变量，它现在竟然说颜色的对比太弱，还给了我提供了一个对比色分析器的网址。</p>
<p>点进去看，好吧，警告全红。虽然测试全是红色，但是我感觉这示例文本看得也挺清晰啊。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-13.avif" alt="Lighthouse推荐的对比色分析器的网址，显示日间模式测试结果"></p>
<p>不过目前的颜色倒是在夜间打分挺高。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-14.avif" alt="显示夜间模式测试结果"></p>
<p>把亮色主题的文本颜色色更改之后：</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-15.avif" alt="更改后的日间模式测试结果"></p>
<p>有看着更舒服了吗……</p>
<h1>SEO 搜索引擎优化</h1>
<p>SEO（Search Engine Optimization，搜索引擎优化）是指通过优化网站，使网站在搜索引擎（如 Google、百度、Bing）的自然搜索结果（非广告）中排名更高，从而获得更多免费流量的过程。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-16.avif" alt="Lighthouse提示原先的SEO分数"></p>
<p>那么看看 SEO 有哪些内容可以提升。</p>
<h2>没有 meta 描述</h2>
<p>这里说的是我的文档缺失 meta 描述信息。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-17.avif" alt="Lighthouse提示文档缺失meta描述信息"></p>
<p>首先在我们的 <code>index.html</code> 页面添加 meta 头信息</p>
<pre><code class="language-html">    &lt;!-- 示例 --&gt;
    &lt;meta name=&quot;author&quot; content=&quot;作者名&quot; /&gt;
    &lt;meta name=&quot;description&quot; content=&quot;网站介绍&quot; /&gt;
    &lt;meta name=&quot;keywords&quot; content=&quot;关键词&quot; /&gt;
</code></pre>
<p>对于 Vue ，还可以按照需要动态添加元信息</p>
<pre><code class="language-js">// router/index.js 示例
const routes = [
  {
    path: '/home',
    name: 'Home',
    component: () =&gt; import('@/views/Home/index.vue'),
    meta: {
        title: '[博客名]'
    }
  },
  {
    path: 'archive/,
    name: 'Archive',
    component: () =&gt; import('@/views/Archive/index.vue'),
    meta: {
        title: '归档 - [博客名]'
    }
  },
  // ...
]
</code></pre>
<p>然后，添加一个全局前置守卫来完成动态的标题设置</p>
<pre><code class="language-js">router.beforeEach((to, from, next) =&gt; {
  // existing code... 
  if (to.meta?.title) {
        document.title = to.meta.title  || '[博客名]'
    }
})
</code></pre>
<p>对于所有博客文章页，可以通过 <code>@vueuse/head</code> 为每个文章添加不同的动态头信息。</p>
<p>首先安装 @vueuse/head</p>
<pre><code class="language-bash">npm install @vueuse/head
</code></pre>
<p>然后使用</p>
<pre><code class="language-js">// Blog/index.js
import { useHead } from '@vueuse/head'
import { computed } from 'vue'
import config from '@/config/index.js'

// 其它代码... 
// 先定义好pageTitle、pageDescription、pageKeywords等变量...

useHead({
    title: pageTitle,
    meta: [
        {
            name: 'description',
            content: pageDescription
        },
        {
            name: 'keywords',
            content: pageKeywords
        },
        // Open Graph标签 - 用于社交媒体分享
        {
            property: 'og:title',
            content: pageTitle
        },
        {
            property: 'og:description',
            content: pageDescription
        },
        {
            property: 'og:image',
            content: computed(() =&gt; config.baseUrl + blogData.value?.coverImage || '')
        },
        {
            property: 'og:url',
            content: computed(() =&gt; window.location.href)
        },
        {
            property: 'og:type',
            content: 'article'
        }
    ]
})
</code></pre>
<p>这样就得到动态生成的元信息了！</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-18.avif" alt="动态生成的网站元信息"></p>
<h2>没有 robots.txt 文件</h2>
<p>这是对于爬虫应该爬取网站哪些内容的说明书，如果没有 robots.txt 文件的话，似乎会直接使用主页面 index.html 作为 robots.txt</p>
<p>所以我们直接在 public 文件夹中创建 robots.txt，内容如下：</p>
<pre><code class="language-txt">echo &quot;User-agent: *
Allow: /
</code></pre>
<h1>Waterfall 瀑布图</h1>
<p>什么是瀑布图？瀑布图是浏览器的开发者工具中，Network面板里的 Waterfall 项，是一个用于展示每个网络请求的加载时间线的可视化工具。我们可以看到每一个请求从发起到响应的时间是怎么样的，以及每一个不同的请求之间的时间顺序与时间间隔。</p>
<p>瀑布图对于性能优化非常有用，特别是首屏加载的性能优化有奇效。</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-19.avif" alt="Network面板里的Waterfall瀑布图"></p>
<p>在查看了瀑布流之后，Y 发现对本站而言最影响首屏加载速度的有两个，一个是网站图标 avatar.ico，它不仅仅在最开始就发起了请求，并且因为图片的体积够大（100k 以上），响应的时间很久，严重阻塞了其他资源的加载。二是和用来统计数据的请求 visit、view 等，也阻塞了其他资源。</p>
<p>那么首先，对网站图标 avatar.ico 进行压缩，最终将体积压缩到了 4.3 kb，还进行了缓存，可以说这大大地提高了首屏速度。</p>
<p>其次将数据统计请求延时，来先等待页面渲染完成，毕竟使用的体验才是最重要的！</p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-20.avif" alt="延迟统计请求的代码"></p>
<p><img src="https://yuhhhy.cn/images/20250914-optimize-21.avif" alt="最终优化完成Lighthouse提示的性能图"></p>
<p>最终将 FCP 控制在了 0.4s，LCP 控制在 0.6s，还算是蛮成功的。</p>
]]></content:encoded>
      <media:content url="https://yuhhhy.cn/images/20250914-optimize-0.avif" medium="image" type="image/avif"/>
    </item>
    <item>
      <title>短视频自救指南</title>
      <link>https://yuhhhy.cn/blog/mewzwmgb-ql</link>
      <guid isPermaLink="true">https://yuhhhy.cn/blog/mewzwmgb-ql</guid>
      <pubDate>Fri, 29 Aug 2025 15:36:29 GMT</pubDate>
      <dc:creator><![CDATA[一曝十寒]]></dc:creator>
      <description><![CDATA[<h1>短视频？原罪？</h1>
<p>当数万年前的人类在洞穴的岩壁上，用木炭和矿物颜料刻画出围捕猎物的情景时，也许不会想到未来的图画，能同时兼具形、色、声的丰富形式，这就是视频。最早的视频，不过是一张张照片拼凑在一起，进行快速切换所呈现的画面，而今视频所带给我们的，是精准复现的情景信息，是五光十色的直觉感受，是可以配合文字稿增进观众理解的工具。</p>
<p>看视频本来可以是一件浪漫的事情。草原、森林、雪山、海岸、澈如碧玉的湖泊；银河、星系、黑洞，那些勾起你的向往，本是此生未见的场景，也顷刻间呈现在眼前。然而，对于视频，以及短视频的沉迷却成了一种现象，当我们越刷短视频，越感到空虚和时光被浪费，甚至有一个词脑腐（brain rot）来形容它：我们越看短视频，我们越感到自己的思维变得迟钝，越发难以接受别的信息获取方式，比如看书。</p>
<p>网络视频兴起的时间比我们想象的短，而大部短视频的载体短视频平台的出现，和其他网络视频平台相差无几。早期的秒拍、美拍，现在的快手、抖音，以至于b站推荐流也有竖屏的短视频模式。虽然说短视频本质上还是视频，但是短视频在人们生活中占比的上升，使得它可以被当作一个独立的部分——由于平台的推送机制。</p>
<h2>我们真的在“看视频”吗</h2>
<p>虽然说“短视频”本身不是罪魁祸首，但是它背后的平台推流以及推荐算法，却不得不对当下的“脑腐”内容承担一定的责任。看似我们在使用手机，实际上却是手机上的应用在控制着我们。</p>
<p>首先就拿电影这种媒介来对比，在看电影之前，至少我们还能选择自己要看什么，筛选出高质量的影片。但是视频、短视频随机的程度就大得多，每次滑动都是新的未知的内容。这样子，刷视频的成瘾性就相对于看电影更大一点。</p>
<p>其次，刷视频所需要的思考，是完全不及看电影，从而也完全不及阅读的。阅读需要一个将连贯的文字转为画面的思考过程，而电影还能保证在一两个小时内，叙事是有逻辑的连续的。这两件事若没有经过头脑的主动理解与记忆，常常就会导致我们看完一本书，或看完一部电影，却记不起大部分的情节了。然而短视频在不断的滑动与刷新中，是很难从中获取一些有价值的信息的，即使你主动想要去记忆，也容易看完即忘，因为看完一个还有下一个，下下一个，你很难停止。</p>
<p>心理学家将记忆分为感觉记忆、短期记忆与长期记忆，那些一瞬间为我们所感知的东西，就是感觉记忆，如果我们不对感觉记忆加以关注，它就不会留存在我们的脑海里。而被注意力筛选后的，就成为短期记忆，短期注意可以持续几十秒到几分钟，如果不复习很容易遗忘。真正被我们记下来的东西则是长久记忆，可以持续数十年，在任何需要的时候随时被取出。</p>
<p>即使我们可以把视频放入收藏夹，但是它们不像书籍、电影一样制作精致，再放一遍能看到更多细节，对于短视频我们可能除了第一次观看，就很难有重放的冲动，这也意味着难以形成记忆。</p>
<h2>视频之后的影响</h2>
<p>就我个人而言，除了极少数可以忽略不计的，我觉得精彩的视频，大多视频我看完是不会留下任何记忆的，即便回看封面和标题我也往往回忆不起内容的细节，而且时长越短越没有印象。我不禁想，如果不是受到推荐算法的推送，我还会有打开那个视频的想法吗？</p>
<p>其实我们都在一种心理效应中挣扎，一种总是茫然，不知道自己想要什么的心理。或者虽然知道自己想要什么，但是却不断被各种干扰所分心。“这件事好难啊，但是我又不想搞得太累所以看下视频吧……”于是我们就不断陷入无用消息的包围圈。对于那些有用的，能实际帮助自己的视频，虽然收藏了很多，又嫌点开太麻烦，时长太长。这完全不是出于慎重的考虑之后得出的结论，而是养成习惯以后下意识的行为。</p>
<p>特别是现在的某些平台，简直成了某种不良文化的温床和聚集地。当代短视频平台对青少年的影响不可谓不深远，害处不仅仅在于养成这种下意识的懒惰、空耗，更是对思维的入侵。</p>
<p>在各个有评论区的视频网站上看一看，就他们的发言来说，你其实很难分得清年龄。不同于书本，一些书籍对于儿童可能会有阅读的障碍，然而视频这种媒介，其受众是全年龄的，其内容却不一定是适合全年龄的。</p>
<p>尼尔·波兹曼在《童年的消逝》中提出的“儿童成人化，成人儿童化”，这种可怕的未来恐怕正在逐渐成为现实，孩子过早地接触到手机，在形成自己的价值观之前就浸染了社会的价值观，而成人呢，反而没有成熟的观念和品格，依旧容易被各种言论带动和洗脑。</p>
<p>如果把看长视频当做一种兴趣与思维拓展，那么刷短视频只能算作一种无谓的消遣，这也是我越来越不爱点开任何短视频或短视频软件的原因。</p>
<h1>为什么我们离不开短视频？</h1>
<p>但是为何很多人即使对短视频抱有负面的看法，却又离不开短视频？</p>
<p>这和我们的大脑机制有关，长时间使用手机确实会改变我们的大脑结构，形成类似“成瘾性”的特征。这背后是视频平台对我们大脑机制的利用。</p>
<h2>生物学原理</h2>
<p>多巴胺是欲望的信使，是驱动我们起身去做某件事的推手。有时候我们感到没动力不想做一件事，可能是分泌的多巴胺水平不足，而产生了强烈的欲望做某件事，大概率此时大脑正在猛猛分泌多巴胺。</p>
<p>而刷手机无疑是大脑分泌多巴胺最轻松的方式，唯一要做的就是动动手指，然后等待大脑被灌输各式各样的内容。那些进入我们首页的都是什么样的视频？它们的目的真的是要教会我们什么吗？恐怕未必，然而不良的习惯却实实在在地养成了，对多巴胺的渴求，会让我们在干别的事的时候，也产生打开手机重复刻板的行为的渴望。屏幕上一个个未读消息的红点，手机的振动与提示音，无时无刻不在促使大脑下意识的行为。往往明明并没有多重要的消息，甚至是软件推送的垃圾消息，却占据了我们屏幕最醒目的位置，我们不断期待着消息，即使消息的内容并没有什么特别的。</p>
<p>到后来甚至发展到一种程度，在看不那么“有趣”的视频的时候，也会将注意力转移到另外的视频上。不仅仅是看到某些东西，甚至当我只是想到那些软件的画面时，大脑也会自己分泌多巴胺，诱导我去点开。造成的严重后果便是，现在做各种事情，比如看书，总是会走神想到另一件事，然后放下手中的事，不知道什么时候才会回来了。或许可以把它称为神经元的恶性病变，大脑被短视频改造，失去了连续思考的能力而变得跳跃。</p>
<h2>可塑性</h2>
<p>但是好在我们知道，神经元是有可塑性的，那么通过痛苦的改变，通过“戒断”，改变思考方式和下意识行为，恐怕会是有效的。</p>
<p>也有人说，要从身边的环境开始。我也承认，环境对我们的影响是巨大的，我们塑造周围的环境，周围的环境也在塑造着我们。那么，一个更符合我们期待的环境——比如说对于想成为作家的人，是一间布置合适的书房，对应一个想搞音乐的人，是摆满了乐器、监听音箱和耳机、MIDI键盘等。相比于物品杂乱，随意摆放的房间，这样的环境，或许会促使我们下意识做出不一样的行为，也是更符合期待的行为。</p>
<p>就像那句话说的一样，“没有人在小时候的梦想就是赚钱”，同样没有人从一开始的理想就是每天躺着刷视频，我们都不过是迷子（まいご）而已。</p>
<h1>政经</h1>
<p>让我们从理论的角度，再深入看看短视频背后的原理。</p>
<p>我们可以将人所拥有的的时间分为三部分：</p>
<ul>
<li>劳动时间（Labor Time）：这是指个人为了维持生计而从事物质生产活动的时间。在资本主义条件下，工人出卖劳动力，人们一边挣得自己的生活资料，一边为资本家创造剩余价值。</li>
<li>自由时间（Free Time）：这是指可以自由支配的时间，是用于休闲和自身发展的时间。在休闲时间里，人们娱乐、学习、创造，从事知识、科学、艺术等活动。</li>
<li>生理时间（Physiological Time）：用于满足基本生理需求的时间，如吃饭、睡觉等，是维持和再生产劳动力所必需的时间。这部分时间虽然不直接用于劳动或自由活动，但它是人类生存和一切活动的基础。</li>
</ul>
<p>在马克思的视角下，由于资本主义制度下实现了对时间的异化（对工人剩余劳动时间的无偿占有），劳动时间与自由时间是对立的。劳动时间越多，我们可以自由支配的时间就越少，自由时间越多，劳动时间久越少。</p>
<p>所以资本家为了榨取更多剩余价值，有多种延长劳动时间的方法。第一种是竭力延长工作日，比如周六不放假，采用“调休”的方式减少假期的数量。第二种是延长每个工作日的工作时间，比如996制度中的9-9、加班文化。前两种都可以归为显性地延长劳动时间，而最后一种间接延长劳动时间的方法，是提高劳动强度。提高劳动强度，会造成我们不得不耗费更多时间用来回复精力，而它并不会计算到我们的劳动时间中，而是从我们的自由时间中抽出的。</p>
<h2>短视频的位置</h2>
<p>原本劳动时间和活动时间互不交叉，互不侵犯，上下班是界限，剥削只存在于劳动时间里。然而后来的西方马克思主义学者，进一步发展了马克思的批判。他们指出，即使在工人获得更多“闲暇时间”的现代消费社会，资本的力量也通过“景观”和“消费主义”等方式殖民了自由时间。人们的闲暇时间被引导至消费和娱乐，这些活动常常继续为资本创造利润，而非用于人的自由全面发展，自由时间再次被异化。</p>
<p>那么，我如果仅仅是刷视频，从来不消费不购买什么商品，也不花钱给主播不花钱订阅，就不会受到剥削吗？</p>
<p>并非如此。原来，作为观众的我们，我们的每一次点击和观看都是被明码标价的。广告机构按照视频的点击量计算广告费，而传媒机构把观众（或者观看行为）作为商品出卖给广告商，广告费就是我们作为观众付出观看所卖出的的售价，所谓的“免费午餐”是通过付出了自己的休闲时间、注意力换来的。并且不同观众作为商品，因其人口统计特征（性别、职业、收入、居住地、手机型号）不同，从而卖出的价格不同，一般财经频道的广告费就会比娱乐频道的广告费高，这就是所谓的“受众商品理论”。</p>
<h2>异化了的时间</h2>
<p>这种异化的剥削方式，不但突破了劳动时间和自由时间的界限，更是突破了年龄的限制，一个7岁的小孩在传统的劳动市场中不具备劳动价值，因为他既没有劳动身份，也不掌握劳动技能，然而他依然可以贡献流量和数据，这是我们所看不见的。在成瘾严重的情况下，甚至还会挤占我们的生理时间，比如睡眠时间。</p>
<p>视频传播机构如果想提高一个视频售出的价值，有两个方法，一是增加观众数量，二是增加观看时间。所谓的“注意力经济”也就是如此。</p>
<p>这决定了它所呈现的形式。首先内容的长度，必然是短、平、快的，其次在视频推荐的算法方面，即使存在着一些优质视频被官方推荐的行为，然而在大部分情况下，它根本不会依据视频内容的质量高低来推荐，而是内容有多离奇可怕，多能吸引用户的注意力，就被推荐给越多的人。</p>
<p>究其背后的商业机制，是科技厂商所代表的资本，为了更多的广告收入、电商收入，在推动着我们上瘾。虽然这也离不开一个事实，那就是当下社会节奏越来越快，人们的压力越来越大，短视频的确能在短时间内给人带来大量的刺激，满足解压宣泄的需求。但是在长远来看，短视频并不会给我们带来真正的娱乐，也不会带来真正的解压，就像其他“物品”一样，它不会解决任何问题，对短视频的成瘾反而会阻碍我们解决真正的问题。</p>
<h1>建议</h1>
<p>每个人的时间都是公平的，但每个人的可支配时间不都是相同的。如何增加“真正的”自由时间，才是真正关乎人类福祉的问题。</p>
<p>要彻底解决这样一个社会性问题，当然不可能有那么容易地给出答案。但是我们还是可以给出一些个人的建议。</p>
<p>我自己想的是：</p>
<ol>
<li>改变环境。由于习惯的作用，大脑在相同的环境中会做出那些已经习惯了的行为，所以如果想要养成新的习惯，从创造出一个新的环境开始，</li>
<li>远离算法，普通订阅。</li>
<li>即使想放松，即使不想动脑子的时候，也不要刷视频，试试找别的成瘾性低的方式，或者成瘾了反而更好的事情做。</li>
</ol>
<h1>P.S.</h1>
<p>聊聊游戏、音乐与看书。</p>
<p>游戏尤其是对抗竞技性的游戏，我觉得也好像有一定的成瘾性。音乐，我现在每天都要听音乐，一天不听我也难受。这些东西根本不需要我们做任何心理建设才去行动，完完全全是习惯养成后下意识的行为。</p>
<p>根据习惯，我们当然也可以培养读书的习惯。一般我们打游戏时，都要买更好的的键盘、鼠标、大大的屏幕，听音乐的时候买更好的设备，为什么读书的时候不也做一些努力让我们更舒服呢？一个舒适的、有读书氛围的环境，是会有正向的作用的。</p>
<p>其次是要改变自己的生活状态，如果你想改变自己的习惯，比如把看短视频改为看书，一定一定一定要给自己留出空闲的时间。人如果有一种很忙碌的心理，那么一定是看不进去书的，特别是大部分的书。因为读书这件事，事实上大部分时候对你的生活的实际影响，你是说不出来的，特别是在短期之内。</p>
<p>另外培养阅读的爱好也是需要循序渐进的。如果要养成每日阅读的习惯，是一定要根据我们的能力来的。</p>
<p>也可以简略分成三部分：</p>
<ol>
<li>一开始的时候是一些简短的片段，只要是文字相关的都可以算入，比如一些个人博客，比如知乎、Reddit的帖子，或者是一些很短的书。为什么不建议看的篇幅太长？因为这会容易让我们感到枯燥和无聊，而且局限我们对个人偏好的探索，不利于我们养成阅读习惯。要从满足感入手，一本读完的满足感驱动着我们去读下一个，来养成爱好。</li>
<li>再后来可以是一些通俗读物，当下比较流行的小说是不会错的，这种书能保证你读起来不觉得难读（当然前提是自己感兴趣）。如果你觉得这种书读起来没什么意思的时候，那么也许你可以达到最后一个阶段。</li>
<li>到了最后的阶段，达到这个阶段的时间可长可短，标准就按照前面两个阶段的读物，能够连续阅读1个小时不被打断来吧。这个最后的阶段就可以是任何想要阅读的书籍、文本，这个阶段是最后一个，也是学无止境的阶段。重要的是你已经养成了每日阅读的习惯，就和打游戏、听音乐一样，如果你每天不看书，你就会难受得慌。</li>
</ol>
<h1>回望：短视频是真正的问题吗？</h1>
<p>个人的空闲时间是有价值的，无论是选择锻炼自己的能力，还是选择享受美好时光，只有离开千篇一律的时间被支配，才有可能实现人的自由全面发展。</p>
<p>短视频充斥着各种模糊的、失真的信息，可以说对我们几乎无意义，但是又对资本充满了意义。更多的视频是难以描述的情绪和一些想法，而非真正的思考，这样我们也很难表达我们从短视频中收获了什么。</p>
<ul>
<li>人们为什么要在空闲时间刷短视频？</li>
<li>最初可能是各种理由，然而最后肯定是习惯。</li>
</ul>
]]></description>
      <content:encoded><![CDATA[<h1>短视频？原罪？</h1>
<p>当数万年前的人类在洞穴的岩壁上，用木炭和矿物颜料刻画出围捕猎物的情景时，也许不会想到未来的图画，能同时兼具形、色、声的丰富形式，这就是视频。最早的视频，不过是一张张照片拼凑在一起，进行快速切换所呈现的画面，而今视频所带给我们的，是精准复现的情景信息，是五光十色的直觉感受，是可以配合文字稿增进观众理解的工具。</p>
<p>看视频本来可以是一件浪漫的事情。草原、森林、雪山、海岸、澈如碧玉的湖泊；银河、星系、黑洞，那些勾起你的向往，本是此生未见的场景，也顷刻间呈现在眼前。然而，对于视频，以及短视频的沉迷却成了一种现象，当我们越刷短视频，越感到空虚和时光被浪费，甚至有一个词脑腐（brain rot）来形容它：我们越看短视频，我们越感到自己的思维变得迟钝，越发难以接受别的信息获取方式，比如看书。</p>
<p>网络视频兴起的时间比我们想象的短，而大部短视频的载体短视频平台的出现，和其他网络视频平台相差无几。早期的秒拍、美拍，现在的快手、抖音，以至于b站推荐流也有竖屏的短视频模式。虽然说短视频本质上还是视频，但是短视频在人们生活中占比的上升，使得它可以被当作一个独立的部分——由于平台的推送机制。</p>
<h2>我们真的在“看视频”吗</h2>
<p>虽然说“短视频”本身不是罪魁祸首，但是它背后的平台推流以及推荐算法，却不得不对当下的“脑腐”内容承担一定的责任。看似我们在使用手机，实际上却是手机上的应用在控制着我们。</p>
<p>首先就拿电影这种媒介来对比，在看电影之前，至少我们还能选择自己要看什么，筛选出高质量的影片。但是视频、短视频随机的程度就大得多，每次滑动都是新的未知的内容。这样子，刷视频的成瘾性就相对于看电影更大一点。</p>
<p>其次，刷视频所需要的思考，是完全不及看电影，从而也完全不及阅读的。阅读需要一个将连贯的文字转为画面的思考过程，而电影还能保证在一两个小时内，叙事是有逻辑的连续的。这两件事若没有经过头脑的主动理解与记忆，常常就会导致我们看完一本书，或看完一部电影，却记不起大部分的情节了。然而短视频在不断的滑动与刷新中，是很难从中获取一些有价值的信息的，即使你主动想要去记忆，也容易看完即忘，因为看完一个还有下一个，下下一个，你很难停止。</p>
<p>心理学家将记忆分为感觉记忆、短期记忆与长期记忆，那些一瞬间为我们所感知的东西，就是感觉记忆，如果我们不对感觉记忆加以关注，它就不会留存在我们的脑海里。而被注意力筛选后的，就成为短期记忆，短期注意可以持续几十秒到几分钟，如果不复习很容易遗忘。真正被我们记下来的东西则是长久记忆，可以持续数十年，在任何需要的时候随时被取出。</p>
<p>即使我们可以把视频放入收藏夹，但是它们不像书籍、电影一样制作精致，再放一遍能看到更多细节，对于短视频我们可能除了第一次观看，就很难有重放的冲动，这也意味着难以形成记忆。</p>
<h2>视频之后的影响</h2>
<p>就我个人而言，除了极少数可以忽略不计的，我觉得精彩的视频，大多视频我看完是不会留下任何记忆的，即便回看封面和标题我也往往回忆不起内容的细节，而且时长越短越没有印象。我不禁想，如果不是受到推荐算法的推送，我还会有打开那个视频的想法吗？</p>
<p>其实我们都在一种心理效应中挣扎，一种总是茫然，不知道自己想要什么的心理。或者虽然知道自己想要什么，但是却不断被各种干扰所分心。“这件事好难啊，但是我又不想搞得太累所以看下视频吧……”于是我们就不断陷入无用消息的包围圈。对于那些有用的，能实际帮助自己的视频，虽然收藏了很多，又嫌点开太麻烦，时长太长。这完全不是出于慎重的考虑之后得出的结论，而是养成习惯以后下意识的行为。</p>
<p>特别是现在的某些平台，简直成了某种不良文化的温床和聚集地。当代短视频平台对青少年的影响不可谓不深远，害处不仅仅在于养成这种下意识的懒惰、空耗，更是对思维的入侵。</p>
<p>在各个有评论区的视频网站上看一看，就他们的发言来说，你其实很难分得清年龄。不同于书本，一些书籍对于儿童可能会有阅读的障碍，然而视频这种媒介，其受众是全年龄的，其内容却不一定是适合全年龄的。</p>
<p>尼尔·波兹曼在《童年的消逝》中提出的“儿童成人化，成人儿童化”，这种可怕的未来恐怕正在逐渐成为现实，孩子过早地接触到手机，在形成自己的价值观之前就浸染了社会的价值观，而成人呢，反而没有成熟的观念和品格，依旧容易被各种言论带动和洗脑。</p>
<p>如果把看长视频当做一种兴趣与思维拓展，那么刷短视频只能算作一种无谓的消遣，这也是我越来越不爱点开任何短视频或短视频软件的原因。</p>
<h1>为什么我们离不开短视频？</h1>
<p>但是为何很多人即使对短视频抱有负面的看法，却又离不开短视频？</p>
<p>这和我们的大脑机制有关，长时间使用手机确实会改变我们的大脑结构，形成类似“成瘾性”的特征。这背后是视频平台对我们大脑机制的利用。</p>
<h2>生物学原理</h2>
<p>多巴胺是欲望的信使，是驱动我们起身去做某件事的推手。有时候我们感到没动力不想做一件事，可能是分泌的多巴胺水平不足，而产生了强烈的欲望做某件事，大概率此时大脑正在猛猛分泌多巴胺。</p>
<p>而刷手机无疑是大脑分泌多巴胺最轻松的方式，唯一要做的就是动动手指，然后等待大脑被灌输各式各样的内容。那些进入我们首页的都是什么样的视频？它们的目的真的是要教会我们什么吗？恐怕未必，然而不良的习惯却实实在在地养成了，对多巴胺的渴求，会让我们在干别的事的时候，也产生打开手机重复刻板的行为的渴望。屏幕上一个个未读消息的红点，手机的振动与提示音，无时无刻不在促使大脑下意识的行为。往往明明并没有多重要的消息，甚至是软件推送的垃圾消息，却占据了我们屏幕最醒目的位置，我们不断期待着消息，即使消息的内容并没有什么特别的。</p>
<p>到后来甚至发展到一种程度，在看不那么“有趣”的视频的时候，也会将注意力转移到另外的视频上。不仅仅是看到某些东西，甚至当我只是想到那些软件的画面时，大脑也会自己分泌多巴胺，诱导我去点开。造成的严重后果便是，现在做各种事情，比如看书，总是会走神想到另一件事，然后放下手中的事，不知道什么时候才会回来了。或许可以把它称为神经元的恶性病变，大脑被短视频改造，失去了连续思考的能力而变得跳跃。</p>
<h2>可塑性</h2>
<p>但是好在我们知道，神经元是有可塑性的，那么通过痛苦的改变，通过“戒断”，改变思考方式和下意识行为，恐怕会是有效的。</p>
<p>也有人说，要从身边的环境开始。我也承认，环境对我们的影响是巨大的，我们塑造周围的环境，周围的环境也在塑造着我们。那么，一个更符合我们期待的环境——比如说对于想成为作家的人，是一间布置合适的书房，对应一个想搞音乐的人，是摆满了乐器、监听音箱和耳机、MIDI键盘等。相比于物品杂乱，随意摆放的房间，这样的环境，或许会促使我们下意识做出不一样的行为，也是更符合期待的行为。</p>
<p>就像那句话说的一样，“没有人在小时候的梦想就是赚钱”，同样没有人从一开始的理想就是每天躺着刷视频，我们都不过是迷子（まいご）而已。</p>
<h1>政经</h1>
<p>让我们从理论的角度，再深入看看短视频背后的原理。</p>
<p>我们可以将人所拥有的的时间分为三部分：</p>
<ul>
<li>劳动时间（Labor Time）：这是指个人为了维持生计而从事物质生产活动的时间。在资本主义条件下，工人出卖劳动力，人们一边挣得自己的生活资料，一边为资本家创造剩余价值。</li>
<li>自由时间（Free Time）：这是指可以自由支配的时间，是用于休闲和自身发展的时间。在休闲时间里，人们娱乐、学习、创造，从事知识、科学、艺术等活动。</li>
<li>生理时间（Physiological Time）：用于满足基本生理需求的时间，如吃饭、睡觉等，是维持和再生产劳动力所必需的时间。这部分时间虽然不直接用于劳动或自由活动，但它是人类生存和一切活动的基础。</li>
</ul>
<p>在马克思的视角下，由于资本主义制度下实现了对时间的异化（对工人剩余劳动时间的无偿占有），劳动时间与自由时间是对立的。劳动时间越多，我们可以自由支配的时间就越少，自由时间越多，劳动时间久越少。</p>
<p>所以资本家为了榨取更多剩余价值，有多种延长劳动时间的方法。第一种是竭力延长工作日，比如周六不放假，采用“调休”的方式减少假期的数量。第二种是延长每个工作日的工作时间，比如996制度中的9-9、加班文化。前两种都可以归为显性地延长劳动时间，而最后一种间接延长劳动时间的方法，是提高劳动强度。提高劳动强度，会造成我们不得不耗费更多时间用来回复精力，而它并不会计算到我们的劳动时间中，而是从我们的自由时间中抽出的。</p>
<h2>短视频的位置</h2>
<p>原本劳动时间和活动时间互不交叉，互不侵犯，上下班是界限，剥削只存在于劳动时间里。然而后来的西方马克思主义学者，进一步发展了马克思的批判。他们指出，即使在工人获得更多“闲暇时间”的现代消费社会，资本的力量也通过“景观”和“消费主义”等方式殖民了自由时间。人们的闲暇时间被引导至消费和娱乐，这些活动常常继续为资本创造利润，而非用于人的自由全面发展，自由时间再次被异化。</p>
<p>那么，我如果仅仅是刷视频，从来不消费不购买什么商品，也不花钱给主播不花钱订阅，就不会受到剥削吗？</p>
<p>并非如此。原来，作为观众的我们，我们的每一次点击和观看都是被明码标价的。广告机构按照视频的点击量计算广告费，而传媒机构把观众（或者观看行为）作为商品出卖给广告商，广告费就是我们作为观众付出观看所卖出的的售价，所谓的“免费午餐”是通过付出了自己的休闲时间、注意力换来的。并且不同观众作为商品，因其人口统计特征（性别、职业、收入、居住地、手机型号）不同，从而卖出的价格不同，一般财经频道的广告费就会比娱乐频道的广告费高，这就是所谓的“受众商品理论”。</p>
<h2>异化了的时间</h2>
<p>这种异化的剥削方式，不但突破了劳动时间和自由时间的界限，更是突破了年龄的限制，一个7岁的小孩在传统的劳动市场中不具备劳动价值，因为他既没有劳动身份，也不掌握劳动技能，然而他依然可以贡献流量和数据，这是我们所看不见的。在成瘾严重的情况下，甚至还会挤占我们的生理时间，比如睡眠时间。</p>
<p>视频传播机构如果想提高一个视频售出的价值，有两个方法，一是增加观众数量，二是增加观看时间。所谓的“注意力经济”也就是如此。</p>
<p>这决定了它所呈现的形式。首先内容的长度，必然是短、平、快的，其次在视频推荐的算法方面，即使存在着一些优质视频被官方推荐的行为，然而在大部分情况下，它根本不会依据视频内容的质量高低来推荐，而是内容有多离奇可怕，多能吸引用户的注意力，就被推荐给越多的人。</p>
<p>究其背后的商业机制，是科技厂商所代表的资本，为了更多的广告收入、电商收入，在推动着我们上瘾。虽然这也离不开一个事实，那就是当下社会节奏越来越快，人们的压力越来越大，短视频的确能在短时间内给人带来大量的刺激，满足解压宣泄的需求。但是在长远来看，短视频并不会给我们带来真正的娱乐，也不会带来真正的解压，就像其他“物品”一样，它不会解决任何问题，对短视频的成瘾反而会阻碍我们解决真正的问题。</p>
<h1>建议</h1>
<p>每个人的时间都是公平的，但每个人的可支配时间不都是相同的。如何增加“真正的”自由时间，才是真正关乎人类福祉的问题。</p>
<p>要彻底解决这样一个社会性问题，当然不可能有那么容易地给出答案。但是我们还是可以给出一些个人的建议。</p>
<p>我自己想的是：</p>
<ol>
<li>改变环境。由于习惯的作用，大脑在相同的环境中会做出那些已经习惯了的行为，所以如果想要养成新的习惯，从创造出一个新的环境开始，</li>
<li>远离算法，普通订阅。</li>
<li>即使想放松，即使不想动脑子的时候，也不要刷视频，试试找别的成瘾性低的方式，或者成瘾了反而更好的事情做。</li>
</ol>
<h1>P.S.</h1>
<p>聊聊游戏、音乐与看书。</p>
<p>游戏尤其是对抗竞技性的游戏，我觉得也好像有一定的成瘾性。音乐，我现在每天都要听音乐，一天不听我也难受。这些东西根本不需要我们做任何心理建设才去行动，完完全全是习惯养成后下意识的行为。</p>
<p>根据习惯，我们当然也可以培养读书的习惯。一般我们打游戏时，都要买更好的的键盘、鼠标、大大的屏幕，听音乐的时候买更好的设备，为什么读书的时候不也做一些努力让我们更舒服呢？一个舒适的、有读书氛围的环境，是会有正向的作用的。</p>
<p>其次是要改变自己的生活状态，如果你想改变自己的习惯，比如把看短视频改为看书，一定一定一定要给自己留出空闲的时间。人如果有一种很忙碌的心理，那么一定是看不进去书的，特别是大部分的书。因为读书这件事，事实上大部分时候对你的生活的实际影响，你是说不出来的，特别是在短期之内。</p>
<p>另外培养阅读的爱好也是需要循序渐进的。如果要养成每日阅读的习惯，是一定要根据我们的能力来的。</p>
<p>也可以简略分成三部分：</p>
<ol>
<li>一开始的时候是一些简短的片段，只要是文字相关的都可以算入，比如一些个人博客，比如知乎、Reddit的帖子，或者是一些很短的书。为什么不建议看的篇幅太长？因为这会容易让我们感到枯燥和无聊，而且局限我们对个人偏好的探索，不利于我们养成阅读习惯。要从满足感入手，一本读完的满足感驱动着我们去读下一个，来养成爱好。</li>
<li>再后来可以是一些通俗读物，当下比较流行的小说是不会错的，这种书能保证你读起来不觉得难读（当然前提是自己感兴趣）。如果你觉得这种书读起来没什么意思的时候，那么也许你可以达到最后一个阶段。</li>
<li>到了最后的阶段，达到这个阶段的时间可长可短，标准就按照前面两个阶段的读物，能够连续阅读1个小时不被打断来吧。这个最后的阶段就可以是任何想要阅读的书籍、文本，这个阶段是最后一个，也是学无止境的阶段。重要的是你已经养成了每日阅读的习惯，就和打游戏、听音乐一样，如果你每天不看书，你就会难受得慌。</li>
</ol>
<h1>回望：短视频是真正的问题吗？</h1>
<p>个人的空闲时间是有价值的，无论是选择锻炼自己的能力，还是选择享受美好时光，只有离开千篇一律的时间被支配，才有可能实现人的自由全面发展。</p>
<p>短视频充斥着各种模糊的、失真的信息，可以说对我们几乎无意义，但是又对资本充满了意义。更多的视频是难以描述的情绪和一些想法，而非真正的思考，这样我们也很难表达我们从短视频中收获了什么。</p>
<ul>
<li>人们为什么要在空闲时间刷短视频？</li>
<li>最初可能是各种理由，然而最后肯定是习惯。</li>
</ul>
]]></content:encoded>
      <media:content url="https://yuhhhy.cn/images/20250829-shortvideo-0.avif" medium="image" type="image/avif"/>
    </item>
    <item>
      <title>React 学习笔记（3）</title>
      <link>https://yuhhhy.cn/blog/mewz7040-ea</link>
      <guid isPermaLink="true">https://yuhhhy.cn/blog/mewz7040-ea</guid>
      <pubDate>Fri, 29 Aug 2025 15:16:33 GMT</pubDate>
      <dc:creator><![CDATA[一曝十寒]]></dc:creator>
      <description><![CDATA[<h1>Reducer &amp; Redux</h1>
<p>用于状态管理。有点类似于 useState，但是 Reducer 是声明式的而非命令式的。我们给 useReducer 一个初识状态和处理函数 reducer，后续只需要通过 dispatch 一个 action，就可以完成 state 的更新，返回一个新的状态。</p>
<pre><code class="language-JavaScript">const [state, dispatch] = useReducer(messageReducer, initialState);
</code></pre>
<p>而 Redux 则是将这种模式应用到了一个全局的状态容器中。任何状态的改变都必须通过 dispatch 一个 action 来触发，再由 reducer 纯函数来处理这个 action 并返回新的状态。</p>
<p>特点：纯函数、单向数据流。</p>
<h1>Context</h1>
<p>这是 React 中一种跨组件通信的方法，使用 Context 时会一层一层向外寻找，组件会使用 UI 树中在它上层最近的那个 <code>&lt;Context&gt;</code> 传递过来的值。</p>
<h1>脱围机制</h1>
<p>有些组件可能需要控制和同步 React 之外的系统。例如使用浏览器 API 聚焦输入框，或者在没有 React 的情况下实现视频播放器，或者连接并监听远程服务器的消息 —— 脱围机制可以让 React 连接到外部系统。大多数应用逻辑和数据流不应该依赖这些功能。</p>
<h2>useRef</h2>
<p>Ref 应该是与渲染无关的。通过设置 <code>myRef = useRef(val)</code> 之后，useRef 会返回 <code>{ current: val }</code> 这样一个对象，通过 <code>ref.current</code>访问值。我们也可以通过 ref 访问 DOM 元素，通过给元素添加属性 <code>ref={myRef}</code> 可以将元素绑定到 ref 上。</p>
<p>只有在组件渲染完成（即 DOM 挂载后）才能安全访问 <code>ref.current</code>。一般放在一些回调函数内。</p>
<h2>flushSync</h2>
<p>指示 React 当封装在 <code>flushSync</code> 中的代码执行后，立即同步更新 DOM。</p>
<pre><code class="language-JavaScript">flushSync(() =&gt; {
  setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
</code></pre>
<h1>Effect</h1>
<p>Effect 允许你指定由渲染自身，而不是特定事件引起的副作用。</p>
<pre><code class="language-JavaScript">function MyComponent() {
  useEffect(() =&gt; {
    // 每次渲染后都会执行此处的代码
  });
  return &lt;div /&gt;;
}
</code></pre>
<p>useEffect 的代码会延迟在渲染后执行，如果没有指定第二个参数，即依赖项，就会在每次渲染之后执行。</p>
<p>因为渲染期间不能修改 DOM，也就是说函数组件的顶层代码不能有副作用，所以需要 useEffect 让副作用分离到渲染逻辑的计算过程之外。</p>
<pre><code class="language-JavaScript">useEffect(() =&gt; {
    if (isPlaying) {
      // ...
    } else {
      // ...
    }
  }, [isPlaying]);
</code></pre>
<p>指定 <code>[isPlaying]</code> 作为依赖数组会告诉 React：如果 <code>isPlaying</code> 与上次渲染时相同，就跳过重新运行 Effect。</p>
<pre><code class="language-JavaScript">useEffect(() =&gt; {
  // 这里的代码会在每次渲染后运行
});

useEffect(() =&gt; {
  // 这里的代码只会在组件挂载（首次出现）时运行
}, []);

useEffect(() =&gt; {
  // 这里的代码不但会在组件挂载时运行，而且当 a 或 b 的值自上次渲染后发生变化后也会运行
}, [a, b]);
</code></pre>
<h2>clean up 清理函数</h2>
<pre><code class="language-JavaScript">  useEffect(() =&gt; {
      const connection = createConnection();
    connection.connect();
    return () =&gt; {
      connection.disconnect();
    };
  }, []);
</code></pre>
<p>React 会在每次 Effect 重新运行之前调用清理函数，并在组件卸载（被移除）时最后一次调用清理函数。清理函数应该停止或撤销 Effect 所做的一切。原则是用户不应该感受到 Effect 只执行一次（在生产环境中）和连续执行“挂载 → 清理 → 挂载”（在开发环境中）之间的区别。</p>
<p>这类 bug 在没有大量手动测试的情况下很容易被忽略。为了帮助开发者快速发现它们，在开发环境中，React 会在组件首次挂载后立即重新挂载一次。</p>
<pre><code class="language-JavaScript">// 如果你的 Effect 订阅了某些事件，清理函数应退订这些事件。
useEffect(() =&gt; {
  function handleScroll(e) {
    console.log(window.scrollX, window.scrollY);
  }
  window.addEventListener('scroll', handleScroll);
  return () =&gt; window.removeEventListener('scroll', handleScroll);
}, []);

// 如果你的 Effect 触发了一些动画，清理函数应将动画重置为初始状态。
useEffect(() =&gt; {
  const node = ref.current;
  node.style.opacity = 1;
  return () =&gt; {
    node.style.opacity = 0;
  };
}, []);

// 如果你的 Effect 需要获取数据，清理函数应中止请求或忽略其结果。
useEffect(() =&gt; {
  let ignore = false;
  async function startFetching() {
    const json = await fetchTodos(userId);
    if (!ignore) {
      setTodos(json);
    }
  }
  startFetching();
  return () =&gt; {
    ignore = true;
  };
}, [userId]);
</code></pre>
<p>为什么要用 useMemo 代替 useEffect？useEffect 只适合用来包含副作用，会在渲染之后执行，不适包含渲染逻辑，如果包含渲染逻辑，会导致原本可以一次渲染处理完的行为，现在需要两次渲染。</p>
]]></description>
      <content:encoded><![CDATA[<h1>Reducer &amp; Redux</h1>
<p>用于状态管理。有点类似于 useState，但是 Reducer 是声明式的而非命令式的。我们给 useReducer 一个初识状态和处理函数 reducer，后续只需要通过 dispatch 一个 action，就可以完成 state 的更新，返回一个新的状态。</p>
<pre><code class="language-JavaScript">const [state, dispatch] = useReducer(messageReducer, initialState);
</code></pre>
<p>而 Redux 则是将这种模式应用到了一个全局的状态容器中。任何状态的改变都必须通过 dispatch 一个 action 来触发，再由 reducer 纯函数来处理这个 action 并返回新的状态。</p>
<p>特点：纯函数、单向数据流。</p>
<h1>Context</h1>
<p>这是 React 中一种跨组件通信的方法，使用 Context 时会一层一层向外寻找，组件会使用 UI 树中在它上层最近的那个 <code>&lt;Context&gt;</code> 传递过来的值。</p>
<h1>脱围机制</h1>
<p>有些组件可能需要控制和同步 React 之外的系统。例如使用浏览器 API 聚焦输入框，或者在没有 React 的情况下实现视频播放器，或者连接并监听远程服务器的消息 —— 脱围机制可以让 React 连接到外部系统。大多数应用逻辑和数据流不应该依赖这些功能。</p>
<h2>useRef</h2>
<p>Ref 应该是与渲染无关的。通过设置 <code>myRef = useRef(val)</code> 之后，useRef 会返回 <code>{ current: val }</code> 这样一个对象，通过 <code>ref.current</code>访问值。我们也可以通过 ref 访问 DOM 元素，通过给元素添加属性 <code>ref={myRef}</code> 可以将元素绑定到 ref 上。</p>
<p>只有在组件渲染完成（即 DOM 挂载后）才能安全访问 <code>ref.current</code>。一般放在一些回调函数内。</p>
<h2>flushSync</h2>
<p>指示 React 当封装在 <code>flushSync</code> 中的代码执行后，立即同步更新 DOM。</p>
<pre><code class="language-JavaScript">flushSync(() =&gt; {
  setTodos([ ...todos, newTodo]);
});
listRef.current.lastChild.scrollIntoView();
</code></pre>
<h1>Effect</h1>
<p>Effect 允许你指定由渲染自身，而不是特定事件引起的副作用。</p>
<pre><code class="language-JavaScript">function MyComponent() {
  useEffect(() =&gt; {
    // 每次渲染后都会执行此处的代码
  });
  return &lt;div /&gt;;
}
</code></pre>
<p>useEffect 的代码会延迟在渲染后执行，如果没有指定第二个参数，即依赖项，就会在每次渲染之后执行。</p>
<p>因为渲染期间不能修改 DOM，也就是说函数组件的顶层代码不能有副作用，所以需要 useEffect 让副作用分离到渲染逻辑的计算过程之外。</p>
<pre><code class="language-JavaScript">useEffect(() =&gt; {
    if (isPlaying) {
      // ...
    } else {
      // ...
    }
  }, [isPlaying]);
</code></pre>
<p>指定 <code>[isPlaying]</code> 作为依赖数组会告诉 React：如果 <code>isPlaying</code> 与上次渲染时相同，就跳过重新运行 Effect。</p>
<pre><code class="language-JavaScript">useEffect(() =&gt; {
  // 这里的代码会在每次渲染后运行
});

useEffect(() =&gt; {
  // 这里的代码只会在组件挂载（首次出现）时运行
}, []);

useEffect(() =&gt; {
  // 这里的代码不但会在组件挂载时运行，而且当 a 或 b 的值自上次渲染后发生变化后也会运行
}, [a, b]);
</code></pre>
<h2>clean up 清理函数</h2>
<pre><code class="language-JavaScript">  useEffect(() =&gt; {
      const connection = createConnection();
    connection.connect();
    return () =&gt; {
      connection.disconnect();
    };
  }, []);
</code></pre>
<p>React 会在每次 Effect 重新运行之前调用清理函数，并在组件卸载（被移除）时最后一次调用清理函数。清理函数应该停止或撤销 Effect 所做的一切。原则是用户不应该感受到 Effect 只执行一次（在生产环境中）和连续执行“挂载 → 清理 → 挂载”（在开发环境中）之间的区别。</p>
<p>这类 bug 在没有大量手动测试的情况下很容易被忽略。为了帮助开发者快速发现它们，在开发环境中，React 会在组件首次挂载后立即重新挂载一次。</p>
<pre><code class="language-JavaScript">// 如果你的 Effect 订阅了某些事件，清理函数应退订这些事件。
useEffect(() =&gt; {
  function handleScroll(e) {
    console.log(window.scrollX, window.scrollY);
  }
  window.addEventListener('scroll', handleScroll);
  return () =&gt; window.removeEventListener('scroll', handleScroll);
}, []);

// 如果你的 Effect 触发了一些动画，清理函数应将动画重置为初始状态。
useEffect(() =&gt; {
  const node = ref.current;
  node.style.opacity = 1;
  return () =&gt; {
    node.style.opacity = 0;
  };
}, []);

// 如果你的 Effect 需要获取数据，清理函数应中止请求或忽略其结果。
useEffect(() =&gt; {
  let ignore = false;
  async function startFetching() {
    const json = await fetchTodos(userId);
    if (!ignore) {
      setTodos(json);
    }
  }
  startFetching();
  return () =&gt; {
    ignore = true;
  };
}, [userId]);
</code></pre>
<p>为什么要用 useMemo 代替 useEffect？useEffect 只适合用来包含副作用，会在渲染之后执行，不适包含渲染逻辑，如果包含渲染逻辑，会导致原本可以一次渲染处理完的行为，现在需要两次渲染。</p>
]]></content:encoded>
      <media:content url="https://yuhhhy.cn/images/20250728-reactjs-0.avif" medium="image" type="image/avif"/>
    </item>
    <item>
      <title>React 学习笔记（2）</title>
      <link>https://yuhhhy.cn/blog/memz1tt0-ai</link>
      <guid isPermaLink="true">https://yuhhhy.cn/blog/memz1tt0-ai</guid>
      <pubDate>Fri, 22 Aug 2025 15:14:50 GMT</pubDate>
      <dc:creator><![CDATA[一曝十寒]]></dc:creator>
      <description><![CDATA[<p>原本的学习笔记写得太杂，在我用了这一段时间的 React 之后，我重写了一下 React 学习笔记，所以这个可以当做 <a href="https://yuhhhy.cn/blog/mdmlf2y4-de">《React 学习笔记（1）》</a> 的新版本。</p>
<h1>什么是 React</h1>
<p>React 是一个开源的 JavaScript 库，由 Facebook 的 Jordan Walke 开发，用来提升 Facebook 广告系统开发效率的，13年开源。其核心是组件化和 JSX。组件化提高了复用性、可维护性，JSX 简化了描述 UI 的操作。</p>
<h2>JSX</h2>
<p>JSX（JavaScript Xml）是 JavaScript 的一种语法扩展，它允许在 JavaScript 文件中描述 UI。</p>
<pre><code class="language-JavaScript">const element = (
  &lt;div className=&quot;greeting&quot;&gt;
    &lt;h1&gt;你好, 世界!&lt;/h1&gt;
    &lt;p&gt;这是一个段落。&lt;/p&gt;
  &lt;/div&gt;
);
</code></pre>
<p>我们使用 JSX 编写的代码，最终被编译成普通的 JavaScript 代码，编译后的代码描述了一个重要的对象，即 JSX 元素，或者叫做 React 元素，<strong>JSX 元素本质上还是 JavaScript 对象</strong>。编译器（如 babel、tsc）通过调用 <code>React.createElement()</code>，17之后是<code>_jsx()</code>，就可以生成这个对象。</p>
<pre><code class="language-JavaScript">const element = React.createElement(
  'div',
  {className: 'greeting'},
  React.createElement('h1', null, '你好, 世界!'),
  React.createElement('p', null, '这是一个段落。')
);
</code></pre>
<pre><code class="language-JavaScript">{
  type: 'div',
  props: {
    className: 'greeting',
    children: [
      {
        type: 'h1',
        props: {
          children: '你好, 世界!'
        }
      },
      {
        type: 'p',
        props: {
          children: '这是一个段落。'
        }
      }
    ]
  },
}
</code></pre>
<p><strong>这些相互嵌套的 React 元素会形成一个树形结构 —— 一个 JavaScript 对象树，这就叫做虚拟 DOM。</strong></p>
<p>虚拟 DOM 和渲染息息相关，虚拟 DOM 是作为数据操作和实际渲染之间的中间层而存在的，以<strong>避免直接操作 DOM 引起频繁的重排重绘</strong>。</p>
<p><img src="https://zcnejflewpus.feishu.cn/space/api/box/stream/download/asynccode/?code=YjMyOTA1MjRkOGFjNDI4ZTNmMzRhNzQ1OTU0MmZmNTZfY29oUEF2SlJFTlRXc2xnemh4QkwyWUw1SllFRnIxelZfVG9rZW46RjJxR2JMNU4wb1hnc054UkVjaGN3QVhSbm1nXzE3NTU4NzQ5OTU6MTc1NTg3ODU5NV9WNA" alt=""></p>
<h2>React 的更新过程</h2>
<p><strong>每当组件的状态 (state) 或属性 (props) 发生变化时，也就是说需要进行渲染时，React 会调用组件的 render() 方法，render() 方法会返回 JSX，通过<code>React.createElement()</code>再转换为 React 元素，形成一个 React 元素树，也就是新的虚拟 DOM。经过新旧虚拟 DOM 树的对比（Diff）之后，找出更新真实 DOM 的最小化操作。</strong></p>
<h2>JSX 潜规则</h2>
<ul>
<li>因为 JSX 本身是 JavaScript 语法糖，所以可以使用任何 JS 表达式。</li>
<li>JSX 使用 <code>()</code> 包裹，可以防止 JavaScript 在 return 后自动插入分号，return 和 JSX 在同一行时，可以不使用<code>()</code>。</li>
<li><strong>因为 JSX 元素底层是 JavaScript 对象，而在 JS 中，你不能在一个函数中返回多个对象，除非用一个数组把他们包装起来。所以多个 JSX 标签，必须用一个父元素或者空标签 <code>&lt;&gt;&lt;/&gt;</code>（Fragment）来包裹。</strong></li>
<li>JSX 不仅可以在函数中作为返回值，也可以给一个变量赋值。</li>
<li>因为 JSX 元素最终会被转化为 JavaScript 对象，所以 JSX 元素的属性也会变成 JavaScript 对象中的键值对。因为 JavaScript 变量名称不能包含 <code>-</code> 或 <code>class</code> 这样的保留字，所以属性命名统一使用驼峰命名，<code>class</code> 要用 <code>className</code> 代替。</li>
<li>标签必须正确闭合。像 <code>&lt;img&gt;</code> 这样的自闭合标签必须书写成 <code>&lt;img /&gt;</code>。</li>
</ul>
<h2>JSX 表达式</h2>
<p>因为 JSX 是类似 HTML 的语法，有时我们又想在 JSX 中使用 JavaScript 表达式语法来是我们的代码更加精简，那么就需要用到 JSX 表达式。</p>
<p>JSX 表达式是在 JSX 中插入的用 <code>{}</code> 包裹的 JavaScript 表达式。</p>
<p>元素标签内的文本以及属性值都可以使用 <code>{}</code>，<code>{{}}</code> 是在 JSX 表达式中插入了一个对象，不是什么特殊的语法（注意对象属性也要驼峰命名）。</p>
<pre><code class="language-Plain">&lt;&gt;
    &lt;h1&gt;{name}'s To Do List&lt;/h1&gt;
    &lt;img src={avatar} /&gt;
    &lt;div style={{ backgroundColor: 'black', color: 'pink' }}&gt;&lt;/div&gt;
&lt;/&gt;
</code></pre>
<h1>组件</h1>
<p>React 组件，或者叫 JSX 组件，就是一个返回 JSX 的 JavaScript 函数或类。它定义了一段可复用的代码片段。</p>
<h2>props</h2>
<p>函数组件就是一个普通的 JavaScript 函数，它接收一个名为 props 的对象作为参数。</p>
<p>在声明 props 时， 可以在函数的参数处用一对花括号 <code>{</code> 和 <code>}</code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Unpacking_fields_from_objects_passed_as_a_function_parameter">解构</a>。</p>
<pre><code class="language-JavaScript">function Avatar({ person, size }) {
  // ...
}
// 等价于
function Avatar(props) {
  let person = props.person;
  let size = props.size;
  // ...
}
</code></pre>
<p>props 是只读的，并且会随着父组件传入的数据变化而变化。</p>
<h2>children</h2>
<p><code>children</code> 是一个特殊的 prop，用于声明接收一个 JSX 作为参数，而非一般的 JS 变量。就像一个「插槽」。</p>
<pre><code class="language-JavaScript">function Card({ children }) {
    return (
        &lt;div className=&quot;card&quot;&gt;
            {children}
        &lt;/div&gt;
    );
}

export default function Profile() {
    return (
        &lt;Card&gt;
            {/* 可以插入任何 JSX 内容，会显示在&lt;Card&gt;的{children}处*/}
        &lt;/Card&gt;
    );
}
</code></pre>
<h2>条件渲染</h2>
<ol>
<li><code>if/else</code> 语句</li>
<li>三元运算符 <code>? :</code></li>
<li>逻辑与运算符 <code>&amp;&amp;</code></li>
<li>阻止组件渲染 返回 <code>null</code></li>
<li><code>switch</code> 语句</li>
<li>使用枚举对象或映射</li>
<li>高阶组件</li>
</ol>
<blockquote>
<p>React 不会渲染 <code>false</code>、<code>null</code>、<code>undefined</code> 、<code>&quot;&quot;</code>、<code>[]</code>，会渲染 <code>0</code> ，所以不要将 <code>0</code> 放在 &amp;&amp; 表达式左侧。渲染 <code>{}</code> 会控制台警告。</p>
</blockquote>
<h2>列表渲染</h2>
<p>列表渲染的每一个列表项必须要有 key 属性，这样 React 才能在重新渲染时维持组件的状态。</p>
<p>key 属性的值必须是唯一且不变的，因此不要使用数组的索引作为 key。生成 key 值的方法有：直接使用数据的主键、<a data-lark-is-custom="true" href="https://www.npmjs.com/package/uuid">uuid</a>、<a data-lark-is-custom="true" href="https://developer.mozilla.org/zh-CN/docs/Web/API/Crypto/randomUUID">crypto.randomUUID()</a>。</p>
<p>如果想要列表的每一项都有多个元素，使用 <code>Fragment</code> 包裹每一项，<code>Fragment</code> 不会出现在最终的 DOM 上，并且可以接受 key。但是 <code>&lt;Fragment&gt;&lt;/Fragment&gt;</code> 的简写形式 <code>&lt;&gt;&lt;/&gt;</code>无法接受 key 值。</p>
<h1>State</h1>
<h2>State 有什么用？</h2>
<p>普通变量的局限：</p>
<ol>
<li>更改局部变量不会触发重新渲染，也就是说 UI 并不会响应变量更新。</li>
<li>发生重新渲染时，变量的状态会被重置为初始化时的值。</li>
</ol>
<p>State：</p>
<ol>
<li>State 变量 <strong>保存</strong> 渲染间的数据。</li>
<li>State setter 函数更新变量并 <strong>触发 <strong><strong>React</strong></strong> 再次渲染</strong> 组件。</li>
</ol>
<pre><code class="language-JavaScript">const [index, setIndex] = useState(0);
</code></pre>
<h2>State 如何保证不变？</h2>
<p>虽然每次渲染都会重新执行组件函数，但是这并不会重置 State，因为 State 并不是存储在组件函数中，而是由 React 在全局中统一储存管理。</p>
<pre><code class="language-JavaScript">// 两个独立的 Counter 实例，各自维护独立的 count state
&lt;Counter /&gt;  // Instance A: count = 0
&lt;Counter /&gt;  // Instance B: count = 5
</code></pre>
<p>各个组件的 state 都是独立的，如果在两个地方渲染同一个组件，则每个副本都有独属于自己的 state。</p>
<h2>何时 State 会被重置？</h2>
<p><strong>React 在内部为每个组件实例创建了一个 Fiber 节点</strong>，每个 Fiber 节点 对应一个组件实例。它永久地存储着组件的 State、Props、副作用和 Hook 链表。当组件函数代码执行到 useState 时，React 从该组件 Fiber 节点的 Hook 链表中提供 state 的当前值。</p>
<p><strong>State 是否重置，取决于对应的 Fiber 节点是否被复用。</strong></p>
<p><strong>React 的 Diffing 算法遵循一个原则：如果两个连续渲染中，相同位置的元素是同一类型且具有相同的 <code>key</code>，React 会复用现有的 Fiber 节点（从而保留其 state）；否则，它会销毁旧的 Fiber 节点并创建一个新的（从而重置 state）。</strong></p>
<pre><code class="language-JavaScript">// 切换时 state 会重置
{isEditing ? &lt;EditForm /&gt; : &lt;ViewForm /&gt;}
// 切换时 state 会重置
{isShow ? &lt;ShowList /&gt; : null}
// 切换时 state 会重置
{isPlayerA ? (
  &lt;Counter key=&quot;Taylor&quot; person=&quot;Taylor&quot; /&gt;
) : (
  &lt;Counter key=&quot;Sarah&quot; person=&quot;Sarah&quot; /&gt;
)}

// 没有 key，切换时 state 不会被重置
{isPlayerA ? (
  &lt;Counter person=&quot;Taylor&quot; /&gt;
) : (
  &lt;Counter person=&quot;Sarah&quot; /&gt;
)}
</code></pre>
<p>但如果是同一个组件，即使传入的参数改变了，Fiber 节点不变，State 也会被保留。</p>
<blockquote>
<p>Fiber 节点和 JSX 对象的不同之处在于，JSX 对象在每次执行渲染函数时都会重新创建，而 Fiber 节点主要在首次渲染时创建，更新时尽量复用。</p>
</blockquote>
<h1>Hooks</h1>
<p>Hooks 是 React 提供的一系列特殊的 API 函数，只能在<strong>组件的最顶层使用</strong>。</p>
<p>Hooks 是用来代替 Class 组件的，没有 Hooks 以前的函数组件无状态、无法使用生命周期，只是接收 props 并返回 JSX。只有 Class 组件才有状态和生命周期，但是逻辑较为复杂。于是引入了 Hooks，Hooks 是组件的内部状态。</p>
<h3>Hook 的存储方式：链表</h3>
<p><strong>React 为每个组件维护了维护了一个单向链表，这个链表上的每个节点都对应组件中的一次 Hook 调用。</strong></p>
<pre><code class="language-JavaScript">// 所有 Hook 对象的基类（伪代码）
class Hook {
  memoizedState: any;     // 这个 Hook 记忆的主要值
  baseState: any;         // 用于计算新状态的基础状态
  baseQueue: Update | null; // 待处理的更新队列
  queue: UpdateQueue | null; // 更新队列（用于状态Hook）
  next: Hook | null;      // 指向下一个 Hook 的指针
}

// 一个组件的 Hooks（伪代码）
Hook1(name) -&gt; Hook2(age) -&gt; Hook3(color) -&gt; null
</code></pre>
<p>第一次渲染时，每调用一次 Hook（<code>useState('yuhhhy')</code>，<code>useEffect(() =&gt;{})</code>），React 就会在链表中创建一个新的 Hook 对象。这个 Hook 对象保存了当前 Hook 的相关信息以及指向下一个 Hook 的指针。</p>
<p><strong>后续的每一次渲染，React 会沿着这个链表按顺序取出每个 Hook 对应的状态。这就是为什么不能将 Hook 调用放在条件语句或循环中的原因 —— 这会打乱 Hook 的调用顺序，导致 React 无法正确匹配链表中的每个 Hook 与其状态。</strong></p>
]]></description>
      <content:encoded><![CDATA[<p>原本的学习笔记写得太杂，在我用了这一段时间的 React 之后，我重写了一下 React 学习笔记，所以这个可以当做 <a href="https://yuhhhy.cn/blog/mdmlf2y4-de">《React 学习笔记（1）》</a> 的新版本。</p>
<h1>什么是 React</h1>
<p>React 是一个开源的 JavaScript 库，由 Facebook 的 Jordan Walke 开发，用来提升 Facebook 广告系统开发效率的，13年开源。其核心是组件化和 JSX。组件化提高了复用性、可维护性，JSX 简化了描述 UI 的操作。</p>
<h2>JSX</h2>
<p>JSX（JavaScript Xml）是 JavaScript 的一种语法扩展，它允许在 JavaScript 文件中描述 UI。</p>
<pre><code class="language-JavaScript">const element = (
  &lt;div className=&quot;greeting&quot;&gt;
    &lt;h1&gt;你好, 世界!&lt;/h1&gt;
    &lt;p&gt;这是一个段落。&lt;/p&gt;
  &lt;/div&gt;
);
</code></pre>
<p>我们使用 JSX 编写的代码，最终被编译成普通的 JavaScript 代码，编译后的代码描述了一个重要的对象，即 JSX 元素，或者叫做 React 元素，<strong>JSX 元素本质上还是 JavaScript 对象</strong>。编译器（如 babel、tsc）通过调用 <code>React.createElement()</code>，17之后是<code>_jsx()</code>，就可以生成这个对象。</p>
<pre><code class="language-JavaScript">const element = React.createElement(
  'div',
  {className: 'greeting'},
  React.createElement('h1', null, '你好, 世界!'),
  React.createElement('p', null, '这是一个段落。')
);
</code></pre>
<pre><code class="language-JavaScript">{
  type: 'div',
  props: {
    className: 'greeting',
    children: [
      {
        type: 'h1',
        props: {
          children: '你好, 世界!'
        }
      },
      {
        type: 'p',
        props: {
          children: '这是一个段落。'
        }
      }
    ]
  },
}
</code></pre>
<p><strong>这些相互嵌套的 React 元素会形成一个树形结构 —— 一个 JavaScript 对象树，这就叫做虚拟 DOM。</strong></p>
<p>虚拟 DOM 和渲染息息相关，虚拟 DOM 是作为数据操作和实际渲染之间的中间层而存在的，以<strong>避免直接操作 DOM 引起频繁的重排重绘</strong>。</p>
<p><img src="https://zcnejflewpus.feishu.cn/space/api/box/stream/download/asynccode/?code=YjMyOTA1MjRkOGFjNDI4ZTNmMzRhNzQ1OTU0MmZmNTZfY29oUEF2SlJFTlRXc2xnemh4QkwyWUw1SllFRnIxelZfVG9rZW46RjJxR2JMNU4wb1hnc054UkVjaGN3QVhSbm1nXzE3NTU4NzQ5OTU6MTc1NTg3ODU5NV9WNA" alt=""></p>
<h2>React 的更新过程</h2>
<p><strong>每当组件的状态 (state) 或属性 (props) 发生变化时，也就是说需要进行渲染时，React 会调用组件的 render() 方法，render() 方法会返回 JSX，通过<code>React.createElement()</code>再转换为 React 元素，形成一个 React 元素树，也就是新的虚拟 DOM。经过新旧虚拟 DOM 树的对比（Diff）之后，找出更新真实 DOM 的最小化操作。</strong></p>
<h2>JSX 潜规则</h2>
<ul>
<li>因为 JSX 本身是 JavaScript 语法糖，所以可以使用任何 JS 表达式。</li>
<li>JSX 使用 <code>()</code> 包裹，可以防止 JavaScript 在 return 后自动插入分号，return 和 JSX 在同一行时，可以不使用<code>()</code>。</li>
<li><strong>因为 JSX 元素底层是 JavaScript 对象，而在 JS 中，你不能在一个函数中返回多个对象，除非用一个数组把他们包装起来。所以多个 JSX 标签，必须用一个父元素或者空标签 <code>&lt;&gt;&lt;/&gt;</code>（Fragment）来包裹。</strong></li>
<li>JSX 不仅可以在函数中作为返回值，也可以给一个变量赋值。</li>
<li>因为 JSX 元素最终会被转化为 JavaScript 对象，所以 JSX 元素的属性也会变成 JavaScript 对象中的键值对。因为 JavaScript 变量名称不能包含 <code>-</code> 或 <code>class</code> 这样的保留字，所以属性命名统一使用驼峰命名，<code>class</code> 要用 <code>className</code> 代替。</li>
<li>标签必须正确闭合。像 <code>&lt;img&gt;</code> 这样的自闭合标签必须书写成 <code>&lt;img /&gt;</code>。</li>
</ul>
<h2>JSX 表达式</h2>
<p>因为 JSX 是类似 HTML 的语法，有时我们又想在 JSX 中使用 JavaScript 表达式语法来是我们的代码更加精简，那么就需要用到 JSX 表达式。</p>
<p>JSX 表达式是在 JSX 中插入的用 <code>{}</code> 包裹的 JavaScript 表达式。</p>
<p>元素标签内的文本以及属性值都可以使用 <code>{}</code>，<code>{{}}</code> 是在 JSX 表达式中插入了一个对象，不是什么特殊的语法（注意对象属性也要驼峰命名）。</p>
<pre><code class="language-Plain">&lt;&gt;
    &lt;h1&gt;{name}'s To Do List&lt;/h1&gt;
    &lt;img src={avatar} /&gt;
    &lt;div style={{ backgroundColor: 'black', color: 'pink' }}&gt;&lt;/div&gt;
&lt;/&gt;
</code></pre>
<h1>组件</h1>
<p>React 组件，或者叫 JSX 组件，就是一个返回 JSX 的 JavaScript 函数或类。它定义了一段可复用的代码片段。</p>
<h2>props</h2>
<p>函数组件就是一个普通的 JavaScript 函数，它接收一个名为 props 的对象作为参数。</p>
<p>在声明 props 时， 可以在函数的参数处用一对花括号 <code>{</code> 和 <code>}</code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Unpacking_fields_from_objects_passed_as_a_function_parameter">解构</a>。</p>
<pre><code class="language-JavaScript">function Avatar({ person, size }) {
  // ...
}
// 等价于
function Avatar(props) {
  let person = props.person;
  let size = props.size;
  // ...
}
</code></pre>
<p>props 是只读的，并且会随着父组件传入的数据变化而变化。</p>
<h2>children</h2>
<p><code>children</code> 是一个特殊的 prop，用于声明接收一个 JSX 作为参数，而非一般的 JS 变量。就像一个「插槽」。</p>
<pre><code class="language-JavaScript">function Card({ children }) {
    return (
        &lt;div className=&quot;card&quot;&gt;
            {children}
        &lt;/div&gt;
    );
}

export default function Profile() {
    return (
        &lt;Card&gt;
            {/* 可以插入任何 JSX 内容，会显示在&lt;Card&gt;的{children}处*/}
        &lt;/Card&gt;
    );
}
</code></pre>
<h2>条件渲染</h2>
<ol>
<li><code>if/else</code> 语句</li>
<li>三元运算符 <code>? :</code></li>
<li>逻辑与运算符 <code>&amp;&amp;</code></li>
<li>阻止组件渲染 返回 <code>null</code></li>
<li><code>switch</code> 语句</li>
<li>使用枚举对象或映射</li>
<li>高阶组件</li>
</ol>
<blockquote>
<p>React 不会渲染 <code>false</code>、<code>null</code>、<code>undefined</code> 、<code>&quot;&quot;</code>、<code>[]</code>，会渲染 <code>0</code> ，所以不要将 <code>0</code> 放在 &amp;&amp; 表达式左侧。渲染 <code>{}</code> 会控制台警告。</p>
</blockquote>
<h2>列表渲染</h2>
<p>列表渲染的每一个列表项必须要有 key 属性，这样 React 才能在重新渲染时维持组件的状态。</p>
<p>key 属性的值必须是唯一且不变的，因此不要使用数组的索引作为 key。生成 key 值的方法有：直接使用数据的主键、<a data-lark-is-custom="true" href="https://www.npmjs.com/package/uuid">uuid</a>、<a data-lark-is-custom="true" href="https://developer.mozilla.org/zh-CN/docs/Web/API/Crypto/randomUUID">crypto.randomUUID()</a>。</p>
<p>如果想要列表的每一项都有多个元素，使用 <code>Fragment</code> 包裹每一项，<code>Fragment</code> 不会出现在最终的 DOM 上，并且可以接受 key。但是 <code>&lt;Fragment&gt;&lt;/Fragment&gt;</code> 的简写形式 <code>&lt;&gt;&lt;/&gt;</code>无法接受 key 值。</p>
<h1>State</h1>
<h2>State 有什么用？</h2>
<p>普通变量的局限：</p>
<ol>
<li>更改局部变量不会触发重新渲染，也就是说 UI 并不会响应变量更新。</li>
<li>发生重新渲染时，变量的状态会被重置为初始化时的值。</li>
</ol>
<p>State：</p>
<ol>
<li>State 变量 <strong>保存</strong> 渲染间的数据。</li>
<li>State setter 函数更新变量并 <strong>触发 <strong><strong>React</strong></strong> 再次渲染</strong> 组件。</li>
</ol>
<pre><code class="language-JavaScript">const [index, setIndex] = useState(0);
</code></pre>
<h2>State 如何保证不变？</h2>
<p>虽然每次渲染都会重新执行组件函数，但是这并不会重置 State，因为 State 并不是存储在组件函数中，而是由 React 在全局中统一储存管理。</p>
<pre><code class="language-JavaScript">// 两个独立的 Counter 实例，各自维护独立的 count state
&lt;Counter /&gt;  // Instance A: count = 0
&lt;Counter /&gt;  // Instance B: count = 5
</code></pre>
<p>各个组件的 state 都是独立的，如果在两个地方渲染同一个组件，则每个副本都有独属于自己的 state。</p>
<h2>何时 State 会被重置？</h2>
<p><strong>React 在内部为每个组件实例创建了一个 Fiber 节点</strong>，每个 Fiber 节点 对应一个组件实例。它永久地存储着组件的 State、Props、副作用和 Hook 链表。当组件函数代码执行到 useState 时，React 从该组件 Fiber 节点的 Hook 链表中提供 state 的当前值。</p>
<p><strong>State 是否重置，取决于对应的 Fiber 节点是否被复用。</strong></p>
<p><strong>React 的 Diffing 算法遵循一个原则：如果两个连续渲染中，相同位置的元素是同一类型且具有相同的 <code>key</code>，React 会复用现有的 Fiber 节点（从而保留其 state）；否则，它会销毁旧的 Fiber 节点并创建一个新的（从而重置 state）。</strong></p>
<pre><code class="language-JavaScript">// 切换时 state 会重置
{isEditing ? &lt;EditForm /&gt; : &lt;ViewForm /&gt;}
// 切换时 state 会重置
{isShow ? &lt;ShowList /&gt; : null}
// 切换时 state 会重置
{isPlayerA ? (
  &lt;Counter key=&quot;Taylor&quot; person=&quot;Taylor&quot; /&gt;
) : (
  &lt;Counter key=&quot;Sarah&quot; person=&quot;Sarah&quot; /&gt;
)}

// 没有 key，切换时 state 不会被重置
{isPlayerA ? (
  &lt;Counter person=&quot;Taylor&quot; /&gt;
) : (
  &lt;Counter person=&quot;Sarah&quot; /&gt;
)}
</code></pre>
<p>但如果是同一个组件，即使传入的参数改变了，Fiber 节点不变，State 也会被保留。</p>
<blockquote>
<p>Fiber 节点和 JSX 对象的不同之处在于，JSX 对象在每次执行渲染函数时都会重新创建，而 Fiber 节点主要在首次渲染时创建，更新时尽量复用。</p>
</blockquote>
<h1>Hooks</h1>
<p>Hooks 是 React 提供的一系列特殊的 API 函数，只能在<strong>组件的最顶层使用</strong>。</p>
<p>Hooks 是用来代替 Class 组件的，没有 Hooks 以前的函数组件无状态、无法使用生命周期，只是接收 props 并返回 JSX。只有 Class 组件才有状态和生命周期，但是逻辑较为复杂。于是引入了 Hooks，Hooks 是组件的内部状态。</p>
<h3>Hook 的存储方式：链表</h3>
<p><strong>React 为每个组件维护了维护了一个单向链表，这个链表上的每个节点都对应组件中的一次 Hook 调用。</strong></p>
<pre><code class="language-JavaScript">// 所有 Hook 对象的基类（伪代码）
class Hook {
  memoizedState: any;     // 这个 Hook 记忆的主要值
  baseState: any;         // 用于计算新状态的基础状态
  baseQueue: Update | null; // 待处理的更新队列
  queue: UpdateQueue | null; // 更新队列（用于状态Hook）
  next: Hook | null;      // 指向下一个 Hook 的指针
}

// 一个组件的 Hooks（伪代码）
Hook1(name) -&gt; Hook2(age) -&gt; Hook3(color) -&gt; null
</code></pre>
<p>第一次渲染时，每调用一次 Hook（<code>useState('yuhhhy')</code>，<code>useEffect(() =&gt;{})</code>），React 就会在链表中创建一个新的 Hook 对象。这个 Hook 对象保存了当前 Hook 的相关信息以及指向下一个 Hook 的指针。</p>
<p><strong>后续的每一次渲染，React 会沿着这个链表按顺序取出每个 Hook 对应的状态。这就是为什么不能将 Hook 调用放在条件语句或循环中的原因 —— 这会打乱 Hook 的调用顺序，导致 React 无法正确匹配链表中的每个 Hook 与其状态。</strong></p>
]]></content:encoded>
      <media:content url="https://yuhhhy.cn/images/20250728-reactjs-0.avif" medium="image" type="image/avif"/>
    </item>
    <item>
      <title>React 学习笔记（1）</title>
      <link>https://yuhhhy.cn/blog/mdmlf2y4-de</link>
      <guid isPermaLink="true">https://yuhhhy.cn/blog/mdmlf2y4-de</guid>
      <pubDate>Mon, 28 Jul 2025 04:13:31 GMT</pubDate>
      <dc:creator><![CDATA[一曝十寒]]></dc:creator>
      <description><![CDATA[<h1>JSX（JavaScript Xml）</h1>
<p>JSX and React 是相互独立的东西。但它们经常一起使用，JSX 是一种语法扩展，而 React 则是一个 JavaScript 的库。</p>
<p>JSX 虽然看起来很像 HTML，但在底层其实被转<strong>化为了 <strong><strong>JavaScript</strong></strong> 对象</strong>，你不能在一个函数中返回多个对象，除非用一个数组把他们包装起来。这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。</p>
<p>JSX 最终会被转化为 JavaScript，而 <strong>JSX 中的属性</strong>也会变成 JavaScript 对象中的键值对。</p>
<pre><code class="language-js">(
    &lt;div&gt;
      &lt;h1&gt;欢迎来到我的应用&lt;/h1&gt;
      &lt;MyButton /&gt;
      &lt;img
         className=&quot;avatar&quot;
         src={user.imageUrl}
      /&gt;
    &lt;/div&gt;
  );
</code></pre>
<h2>JSX 规则</h2>
<ol>
<li>只能返回一个根元素，可以用 <code>&lt;&gt;</code> 和 <code>&lt;/&gt;</code> 元素来代替，这个空标签被称作 Fragment</li>
<li>标签必须正确闭合。像 <code>&lt;img&gt;</code> 这样的自闭合标签必须书写成 <code>&lt;img /&gt;</code></li>
<li>属性命名要使用驼峰命名。因为 JavaScript 变量名称不能包含 <code>-</code> 符号或者像 <code>class</code> 这样的保留字，所以 JSX 属性也不能以此命名， <code>class</code> 要用 <code>className</code> 代替</li>
</ol>
<h2>JSX 元素</h2>
<ol>
<li>JSX 的精髓在于 JSX 元素。不同于 DOM 元素，JSX 元素不是真正的元素，而是 JavaScript 对象</li>
<li>JSX 元素可以书写在 <code>.jsx</code> 文件的组件返回值中，以及变量赋值、函数参数、条件判断等任何<strong>JavaScript 表达式</strong> 能出现的地方</li>
<li>JSX 元素使用  <code>()</code> 包裹，并且只能有一个根元素</li>
<li>单行的 JSX 元素<strong>可以省略 <code>()</code></strong> ，但是要写在同一行</li>
</ol>
<h2>大括号（JSX 表达式）</h2>
<p>可以使用大括号的地方：</p>
<ol>
<li>属性=后的值 <code>src={avatar}</code></li>
<li>元素标签内的文本 <code>&lt;h1&gt;{name}'s To Do List&lt;/h1&gt;</code></li>
</ol>
<h2>双大括号</h2>
<ol>
<li>双大括号用于传递对象。</li>
</ol>
<p><code>person={{ name: &quot;Hedy Lamarr&quot;, inventions: 5 }}</code></p>
<ol start="2">
<li>设置元素内联样式需要用到双大括号，内联 style 属性 使用驼峰命名法编写。</li>
</ol>
<p><code>&lt;ul style={{ backgroundColor: 'black', color: 'pink' }}&gt;</code></p>
<h2>Props</h2>
<p>在声明 props 时， 不要忘记 <code>(</code> 和 <code>)</code> 之间的一对花括号 <code>{</code> 和 <code>}</code>  ：</p>
<pre><code class="language-JavaScript">function Avatar({ person, size }) {
  // ...
}
</code></pre>
<p>这种语法被称为 <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Unpacking_fields_from_objects_passed_as_a_function_parameter">解构</a>，等价于于从函数参数中读取属性：</p>
<pre><code class="language-JavaScript">function Avatar(props) {
  let person = props.person;
  let size = props.size;
  // ...
}
</code></pre>
<p>给 prop 指定默认值：</p>
<pre><code class="language-JavaScript">function Avatar({ person, size = 100 }) {
  // ...
}
</code></pre>
<p><strong>关于 Props 要注意的点：</strong></p>
<ol>
<li>Props 并不总是静态的，一个组件可能会随着时间的推移收到不同的 props。props 反映了组件在任何时间点的数据，并不仅仅是在开始时。</li>
<li>Prop 是只读的，如果需要能响应用户操作变化的 porp，请使用 State。</li>
</ol>
<h1>JSX 组件</h1>
<h2>为什么不应该嵌套组件定义？</h2>
<pre><code class="language-JavaScript">export default function Gallery() {
  // 永远不要在组件中定义组件
    function Profile() {
        // ...
    }
  // ...
}
</code></pre>
<p>上面这段代码非常慢，并且会导致 bug 产生。因此，应该在顶层定义每个组件：</p>
<pre><code class="language-JavaScript">export default function Gallery() {
  // ...
}
//  在顶层声明组件
function Profile() {
  // ...
}
</code></pre>
<h2>组件如何嵌套？</h2>
<p>嵌套浏览器内置标签是很常见的：</p>
<pre><code class="language-HTML">&lt;div&gt;
  &lt;img /&gt;
&lt;/div&gt;
</code></pre>
<p>有时你会希望以相同的方式嵌套自己的组件：</p>
<pre><code class="language-HTML">&lt;Card&gt;
  &lt;Avatar /&gt;
&lt;/Card&gt;
</code></pre>
<p>当你将内容嵌套在 JSX 标签中时，父组件将在名为 <code>children</code> 的 <strong>prop</strong> 中接收到该内容。</p>
<pre><code class="language-JavaScript">function Card({ children }) {
    return (
        &lt;div className=&quot;card&quot;&gt;
          {children}
        &lt;/div&gt;
      );
    }

export default function Profile() {
  return (
    &lt;Card&gt;
      {/* Any Content */}
    &lt;/Card&gt;
  );
}
</code></pre>
<p>就像一个「插槽」：</p>
<p><img src="https://zcnejflewpus.feishu.cn/space/api/box/stream/download/asynccode/?code=NzQyYzZkZjJhOGNjN2FiMjU2MmZiZjdhODhiZmEzMDdfcnVPdXd2ekFhRjkxTHdlbUR2RGdOTHpqd1lpalpyMlhfVG9rZW46QnBDOWJ2OHUxb0RXNjR4QXoweGNBR1RCbmhjXzE3NTM2NzU3MDQ6MTc1MzY3OTMwNF9WNA" alt=""></p>
<h1>条件渲染</h1>
<h2>1. 多 return</h2>
<p>当条件为真时, 渲染 <code>&lt;A /&gt;</code>，否则 <code>&lt;B /&gt;</code>。</p>
<pre><code class="language-JavaScript">function Item({ name, isPacked }) {
  if (isPacked) {
    return &lt;li className=&quot;item&quot;&gt;{name} ✅&lt;/li&gt;;
  }
  return &lt;li className=&quot;item&quot;&gt;{name}&lt;/li&gt;;
}
</code></pre>
<h2>2. 嵌套 return + 三目运算符</h2>
<p>当条件为真时, 渲染 <code>A</code>，否则 <code>B</code>。</p>
<pre><code class="language-JavaScript">function Item({ name, isPacked }) {
  return (
    &lt;li className=&quot;item&quot;&gt;
      {isPacked ? (
        &lt;del&gt;
          {name + ' ✅'}
        &lt;/del&gt;
      ) : (
        name
      )}
    &lt;/li&gt;
  );
}
</code></pre>
<ul>
<li>用 <code>()</code> 包裹起来的是 JSX 元素，用 <code>{}</code> 包裹起来的是 JavaScript 表达式。</li>
<li><code>()</code> 与 <code>{}</code> 可以相互嵌套。</li>
</ul>
<h2>3. 与运算符 &amp;&amp;</h2>
<p>当条件为真时渲染，否则不渲染任何东西。</p>
<pre><code class="language-JavaScript">return (
  &lt;li className=&quot;item&quot;&gt;
    {name} {isPacked &amp;&amp; '✅'}
  &lt;/li&gt;
);
</code></pre>
<p>仅当 <code>isPacked</code> 为 <code>true</code> 时，渲染勾选符号。</p>
<blockquote>
<p>当 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Logical_AND">JavaScript &amp;&amp; 表达式</a> 的左侧为 <code>true</code> 时，它则返回其右侧的值。当左侧的结果是 <code>false</code>，则整个表达式会变成 <code>false</code>。在 JSX 里，React 会将 <code>false</code> 视为一个“空值”，就像 <code>null</code> 或者 <code>undefined</code>，这样 React 就不会在这里进行任何渲染。</p>
<p>切勿将数字放在 <code>&amp;&amp;</code> 左侧，如果左侧是 <code>0</code>，整个表达式将变成左侧的值（<code>0</code>），React 此时则会渲染 <code>0</code> 而不是不进行渲染。</p>
</blockquote>
<p>React 不会渲染 <code>false</code>、<code>null</code>、<code>undefined</code> 、<code>&quot;&quot;</code>、<code>[]</code>，会渲染 <code>0</code> ，渲染 <code>{}</code> 会控制台警告。</p>
<h2>4. 作为变量的方式</h2>
<p>当条件为真时, 渲染 <code>A</code>，否则 <code>B</code>。</p>
<pre><code class="language-JavaScript">function Item({ name, isPacked }) {
  let itemContent = name;
  if (isPacked) {
    itemContent = (
      &lt;del&gt;
        {name + &quot; ✅&quot;}
      &lt;/del&gt;
    );
  }
  return (
    &lt;li&gt;
      {itemContent}
    &lt;/li&gt;
  );
}
</code></pre>
<ul>
<li>普通 JS 表达式可以被赋值给变量变量</li>
<li>用 <code>()</code> 包裹的 JSX 元素也可以被赋值给一个变量</li>
</ul>
<blockquote>
<p><code>&lt;del&gt;...&lt;/del&gt;</code> 是一个 JSX 元素，编译后会转换为 <code>React.createElement()</code> 调用。</p>
</blockquote>
<h1>列表渲染</h1>
<h2>Fragment</h2>
<p>Fragment 语法的简写形式是 <code>&lt;&gt; &lt;/&gt;</code>，但 <code>&lt;&gt; &lt;/&gt;</code> 无法接受 key 值。</p>
<p>使用 Fragment 语法让每个列表项都输出多个 DOM 节点，<code>Fragment</code> 标签本身并不会出现在 DOM 上。</p>
<pre><code class="language-JavaScript">import { Fragment } from 'react';

// ...

const listItems = people.map(person =&gt;
  &lt;Fragment key={person.id}&gt;
    &lt;h1&gt;{person.name}&lt;/h1&gt;
    &lt;p&gt;{person.bio}&lt;/p&gt;
  &lt;/Fragment&gt;
);
</code></pre>
<h2>如何设定 Key 值</h2>
<p>列表项必须有 key 值，key 值必须是唯一且不变的。</p>
<p>不同来源的数据往往对应不同的 key 值获取方式：</p>
<ul>
<li>来自数据库的数据： 如果你的数据是从数据库中获取的，那你可以直接使用数据表中的主键，因为它们天然具有唯一性。</li>
<li>本地产生数据： 如果你数据的产生和保存都在本地（例如笔记软件里的笔记），那么你可以使用一个自增计数器，<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Crypto/randomUUID" data-lark-is-custom="true">crypto.randomUUID()</a> 或者一个类似 <a href="https://www.npmjs.com/package/uuid" data-lark-is-custom="true">uuid</a> 的库来生成 key（<strong>仅用于数据初始化而非动态生成</strong>）。</li>
</ul>
<blockquote>
<p>组件每次重新渲染都会重新检查所有的 key 值，所以不要在运行过程中动态地产生 key，像是 <code>key={Math.random()}</code> 这种方式。这会导致每次重新渲染后的 key 值都不一样。</p>
</blockquote>
<h1>副作用</h1>
<h2>什么是纯函数？</h2>
<p>理想情况下，React 组件应当是纯函数。</p>
<p>纯函数：</p>
<ul>
<li>只负责自己的任务。它不会更改在该函数调用前就已存在的对象或变量。</li>
<li>输入相同，则输出相同。给定相同的输入，纯函数应总是返回相同的结果。</li>
</ul>
<p>更改组件内部的对象或变量是允许的。</p>
<h2>什么是副作用？</h2>
<ul>
<li>在计算机科学中，副作用是指函数在 <strong>执行过程</strong> 中对 <strong>外部环境</strong>（函数作用域之外）的可观察影响。也就是说，在函数内修改外部变量的行为是副作用。</li>
<li>在 React 语境下，副作用是 <strong>渲染之外</strong> 的操作，是不可预测的。副作用让组件的行为不再仅由输入决定。</li>
</ul>
<h2>副作用有哪些？</h2>
<p>通常包括：</p>
<ul>
<li>发起网络请求（<code>fetch()</code>）</li>
<li>定时器（<code>setTimeout()</code>、<code>setInterval()</code>）</li>
<li>直接修改 DOM（<code>document.title=&quot;&quot;</code>、<code>document.getElementById.style=&quot;&quot;</code>）</li>
</ul>
<h2>如果渲染过程触发副作用，会造成什么错误？</h2>
<p>React 的渲染过程是 React 根据当前 <code>props</code> 和 <code>state</code> 计算并生成虚拟 DOM 的操作。</p>
<p>React 的渲染机制基于 <strong>纯函数式更新</strong>，其核心假设是：</p>
<blockquote>
<p>给定相同的 <code>props</code> 和 <code>state</code>，组件必须返回相同的 JSX。、</p>
</blockquote>
<p>如果不满足这个假设，可能会导致一系列问题，其中最严重就是无限渲染循环的问题：</p>
<p>渲染 -&gt; 触发副作用 -&gt; 改变了渲染的初始条件 -&gt; 重新渲染 -&gt; 再次触发副作用……</p>
<h2>React 如何管理副作用？</h2>
<ol>
<li>让副作用属于<strong>事件处理程序</strong>：比如 <code>onClick</code>、<code>onChange</code>，这样即使事件处理程序是在组件内部定义的，它们也不会在渲染期间运行！</li>
<li>通过 Hook 将副作用与渲染逻辑隔离：比如调用 <code>useEffect</code> 将副作用 <strong>延迟到渲染完成后执行</strong>，避免阻塞渲染。</li>
</ol>
<h1>模块树、依赖树</h1>
<ul>
<li>渲染树表示单次渲染中 React 组件之间的嵌套关系。</li>
<li>构建 React 应用程序时，通常会有一个构建步骤。此步骤中，<a href="https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Extensions/Client-side_tools/Overview#%E7%8E%B0%E4%BB%A3%E5%B7%A5%E5%85%B7%E7%B3%BB%E7%BB%9F">bundler（捆绑器）</a>将捆绑所有必要的 JavaScript 以供客户端使用。bundler 使用依赖树来确定应包含哪些模块。</li>
</ul>
<h1>事件处理函数</h1>
<pre><code class="language-JavaScript">// 这个 alert 在组件渲染时触发，而不是点击时触发！
&lt;button onClick={alert('你点击了我！')}&gt;
</code></pre>
<p>如果想要定义内联事件处理函数，将其包装在匿名函数中，创建一个稍后调用的函数，而不会在每次渲染时执行其内部代码。如下所示：</p>
<pre><code class="language-JavaScript">&lt;button onClick={() =&gt; alert('你点击了我！')}&gt;
</code></pre>
<p>在 React 中所有事件都会传播（向上冒泡），除了 <code>onScroll</code>，它仅适用于你附加到的 JSX 标签。</p>
<h2>stopPropagation() 和 preventDefault()</h2>
<p>如果你想阻止一个事件到达父组件，调用 <code>e.stopPropagation()</code> ：</p>
<pre><code class="language-JavaScript">function Button({ handleClick }) {
  return (
    &lt;button onClick={e =&gt; {
      e.stopPropagation();
      handleClick ();
    }}&gt;
    &lt;/button&gt;
  );
}
</code></pre>
<p>通过在事件名称末尾添加 <code>Capture</code>，即便阻止了传播，也可以捕获子元素上的事件：</p>
<pre><code class="language-JavaScript">&lt;div onClickCapture={() =&gt; { /* 这会首先执行 */ }}&gt;
  &lt;button onClick={e =&gt; e.stopPropagation()} /&gt;
  &lt;button onClick={e =&gt; e.stopPropagation()} /&gt;
&lt;/div&gt;
</code></pre>
<p><code>onClickCapture</code> 处理函数是在事件 <strong>向下传递</strong> 的期间执行的。</p>
<p>使用 <code>e.preventDefault()</code> 来阻止默认行为。</p>
<h1>State</h1>
<h2>Hook 是什么？</h2>
<p>Hook 是 React 中以 use 开头的特殊的函数，在组件 <strong>渲染</strong> 中使用，而 State 是 React Hooks 之一。</p>
<blockquote>
<p>Hooks 只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数，但可将它们视为你希望组件<strong>做点什么</strong>的声明。在组件顶部 “use” React 特性，类似于在文件顶部“导入”模块。</p>
</blockquote>
<h2>State有什么用？</h2>
<p>普通变量的局限：</p>
<ol>
<li>更改局部变量不会触发重新渲染。</li>
<li>发生重新渲染时，局部变量更改不会被保存。</li>
</ol>
<p>State</p>
<ol>
<li>State 变量 <strong>保存</strong> 渲染间的数据。</li>
<li>State setter 函数更新变量并 <strong>触发 React 再次渲染</strong> 组件。</li>
</ol>
<pre><code class="language-JavaScript">const [index, setIndex] = useState(0);
</code></pre>
<h2>React 如何知道返回哪个 state ？</h2>
<p>是按照组件渲染中 Hooks <strong>调用的顺序</strong> 进行区分的。这也是为什么不能在条件语句、循环语句或其他嵌套函数内调用 Hook。</p>
<p>在 React 内部，为每个组件保存了一个数组，其中每一项都是一个 state 对。每次渲染前将索引设为0，每次调用 useState 时，React 都会返回一个 state 对，并增加索引值以指向下一个 state 对。</p>
<pre><code class="language-JavaScript">[
    [xState, usexState],
    [yState, useyState],
    [zState, usezState]
]
</code></pre>
<h1>React 渲染</h1>
<h2>触发渲染的时机</h2>
<ol>
<li>组件的初次渲染。</li>
</ol>
<pre><code class="language-JavaScript">import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(&lt;App /&gt;);
</code></pre>
<ol start="2">
<li>组件（或者其祖先之一）的 <strong>状态发生了改变</strong>。主要是通过 setState 触发，……</li>
</ol>
<h2>进行渲染</h2>
<p>在触发渲染后，React 会调用组件来确定要在屏幕上显示的内容。</p>
<ul>
<li>在进行初次渲染时, React 会调用根组件。</li>
<li>对于后续的渲染, React 会调用内部状态 <strong>更新</strong> 触发了渲染的函数组件。</li>
</ul>
<h2>修改 DOM</h2>
<p>在渲染（调用）你的组件之后，React 将会修改 DOM。</p>
<ul>
<li><strong>对于初次渲染</strong>，React 会使用 <code>appendChild()</code>将其创建的所有 DOM 节点放在屏幕上。</li>
<li><strong>对于重渲染</strong>，在渲染时计算更改操作，React 只更改 **存在差异 **的 DOM 节点。</li>
</ul>
<h1>State 和渲染之间的关系</h1>
<p>渲染出来的组件「快照」就像是静态的（它的 props、事件处理函数和内部变量都是根据当前渲染时的 state 被计算出来的），直到下次渲染，React 会重新调用组件的函数，生成新的「快照」。</p>
<p>一次渲染中一个 state 变量的值只能有一个，不论怎么设置都是不变的。</p>
<h2>重新渲染的时机</h2>
<p>React 并不会在 set State 之后立即渲染，而是批处理。</p>
<h2>更新函数</h2>
<p>更新函数会被放入队列，下一次渲染期间被独立地执行。</p>
<p>通过更新函数，解决在一个渲染期间多次触发 set State 和预期的结果不同的问题（因为 state 是「静态」的）.</p>
<pre><code class="language-JavaScript">setNumber(n =&gt; n + 1); // state: 0 -&gt; 1
setNumber(n =&gt; n + 1); // state: 1 -&gt; 2
setNumber(n =&gt; n + 1); // state: 2 -&gt; 3
</code></pre>
<p>通常可以通过相应 state 变量的第一个字母来命名更新函数的参数：</p>
<pre><code class="language-JavaScript">setEnabled(e =&gt; !e);
setLastName(ln =&gt; ln.reverse());
setFriendCount(fc =&gt; fc * 2);
</code></pre>
<h1>Mutation</h1>
<p>Mutation 就是直接改变 state 对象/数组。</p>
<p>React 强烈建议避免这种操作。应该将 React 中所有的 state 都视为不可直接修改的，修改要使用 set。</p>
<h2>更新对象</h2>
<p>不能直接改变 state 对象，而是应该 set 一个新对象，如果修改对象的深层属性，可能要进行多重展开（连 Proxy 都要借助 Immer 库来实现，React 修改对象的操作也太麻烦了）。</p>
<blockquote>
<p>React 官方： React  并不依赖于 mutation ，所以你不需要对对象进行任何特殊操作。它不需要像很多「响应式」的解决方案一样去劫持对象的属性、总是用代理把对象包裹起来，或者在初始化时做其他工作。这也是 React 允许你把任何对象存放在 state 中——不管对象有多大——而不会造成有任何额外的性能或正确性问题的原因。</p>
</blockquote>
<h2>更新数组</h2>
<ul>
<li>不能使用 <code>arr[0] = 'bird'</code> 给数组元素赋值。</li>
<li>不能使用会 <strong>更改原始数组</strong> 的方法，例如 <code>push()</code> 和 <code>pop()</code>。</li>
</ul>
<table>
<thead>
<tr>
<th><br/></th>
<th>避免使用 (会改变原始数组)</th>
<th>推荐使用 (会返回一个新数组）</th>
</tr>
</thead>
<tbody>
<tr>
<td>添加元素</td>
<td>push，unshift</td>
<td>concat，[…arr] 展开语法</td>
</tr>
<tr>
<td>删除元素</td>
<td>pop，shift，splice</td>
<td>filter，slice</td>
</tr>
<tr>
<td>替换元素</td>
<td>splice，arr[i] = … 赋值</td>
<td>map</td>
</tr>
<tr>
<td>排序</td>
<td>reverse，sort</td>
<td>先将数组复制一份</td>
</tr>
</tbody>
</table>
<blockquote>
<p>不幸的是，虽然 <a data-lark-is-custom="true" href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/slice">slice 和 splice</a> 的名字相似，但作用却迥然不同：</p>
<ul>
<li><code>slice</code> 让你可以拷贝数组或是数组的一部分，返回值是留下的部分。</li>
<li><code>splice</code> 会直接修改原始数组（删除，插入），返回删掉元素的数组。</li>
</ul>
</blockquote>
<p><strong>拷贝要注意</strong>：然而，数组的拷贝是浅拷贝。因此，如果你修改了拷贝数组内部的某个对象，其实你正在直接修改当前的 state。</p>
]]></description>
      <content:encoded><![CDATA[<h1>JSX（JavaScript Xml）</h1>
<p>JSX and React 是相互独立的东西。但它们经常一起使用，JSX 是一种语法扩展，而 React 则是一个 JavaScript 的库。</p>
<p>JSX 虽然看起来很像 HTML，但在底层其实被转<strong>化为了 <strong><strong>JavaScript</strong></strong> 对象</strong>，你不能在一个函数中返回多个对象，除非用一个数组把他们包装起来。这就是为什么多个 JSX 标签必须要用一个父元素或者 Fragment 来包裹。</p>
<p>JSX 最终会被转化为 JavaScript，而 <strong>JSX 中的属性</strong>也会变成 JavaScript 对象中的键值对。</p>
<pre><code class="language-js">(
    &lt;div&gt;
      &lt;h1&gt;欢迎来到我的应用&lt;/h1&gt;
      &lt;MyButton /&gt;
      &lt;img
         className=&quot;avatar&quot;
         src={user.imageUrl}
      /&gt;
    &lt;/div&gt;
  );
</code></pre>
<h2>JSX 规则</h2>
<ol>
<li>只能返回一个根元素，可以用 <code>&lt;&gt;</code> 和 <code>&lt;/&gt;</code> 元素来代替，这个空标签被称作 Fragment</li>
<li>标签必须正确闭合。像 <code>&lt;img&gt;</code> 这样的自闭合标签必须书写成 <code>&lt;img /&gt;</code></li>
<li>属性命名要使用驼峰命名。因为 JavaScript 变量名称不能包含 <code>-</code> 符号或者像 <code>class</code> 这样的保留字，所以 JSX 属性也不能以此命名， <code>class</code> 要用 <code>className</code> 代替</li>
</ol>
<h2>JSX 元素</h2>
<ol>
<li>JSX 的精髓在于 JSX 元素。不同于 DOM 元素，JSX 元素不是真正的元素，而是 JavaScript 对象</li>
<li>JSX 元素可以书写在 <code>.jsx</code> 文件的组件返回值中，以及变量赋值、函数参数、条件判断等任何<strong>JavaScript 表达式</strong> 能出现的地方</li>
<li>JSX 元素使用  <code>()</code> 包裹，并且只能有一个根元素</li>
<li>单行的 JSX 元素<strong>可以省略 <code>()</code></strong> ，但是要写在同一行</li>
</ol>
<h2>大括号（JSX 表达式）</h2>
<p>可以使用大括号的地方：</p>
<ol>
<li>属性=后的值 <code>src={avatar}</code></li>
<li>元素标签内的文本 <code>&lt;h1&gt;{name}'s To Do List&lt;/h1&gt;</code></li>
</ol>
<h2>双大括号</h2>
<ol>
<li>双大括号用于传递对象。</li>
</ol>
<p><code>person={{ name: &quot;Hedy Lamarr&quot;, inventions: 5 }}</code></p>
<ol start="2">
<li>设置元素内联样式需要用到双大括号，内联 style 属性 使用驼峰命名法编写。</li>
</ol>
<p><code>&lt;ul style={{ backgroundColor: 'black', color: 'pink' }}&gt;</code></p>
<h2>Props</h2>
<p>在声明 props 时， 不要忘记 <code>(</code> 和 <code>)</code> 之间的一对花括号 <code>{</code> 和 <code>}</code>  ：</p>
<pre><code class="language-JavaScript">function Avatar({ person, size }) {
  // ...
}
</code></pre>
<p>这种语法被称为 <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Unpacking_fields_from_objects_passed_as_a_function_parameter">解构</a>，等价于于从函数参数中读取属性：</p>
<pre><code class="language-JavaScript">function Avatar(props) {
  let person = props.person;
  let size = props.size;
  // ...
}
</code></pre>
<p>给 prop 指定默认值：</p>
<pre><code class="language-JavaScript">function Avatar({ person, size = 100 }) {
  // ...
}
</code></pre>
<p><strong>关于 Props 要注意的点：</strong></p>
<ol>
<li>Props 并不总是静态的，一个组件可能会随着时间的推移收到不同的 props。props 反映了组件在任何时间点的数据，并不仅仅是在开始时。</li>
<li>Prop 是只读的，如果需要能响应用户操作变化的 porp，请使用 State。</li>
</ol>
<h1>JSX 组件</h1>
<h2>为什么不应该嵌套组件定义？</h2>
<pre><code class="language-JavaScript">export default function Gallery() {
  // 永远不要在组件中定义组件
    function Profile() {
        // ...
    }
  // ...
}
</code></pre>
<p>上面这段代码非常慢，并且会导致 bug 产生。因此，应该在顶层定义每个组件：</p>
<pre><code class="language-JavaScript">export default function Gallery() {
  // ...
}
//  在顶层声明组件
function Profile() {
  // ...
}
</code></pre>
<h2>组件如何嵌套？</h2>
<p>嵌套浏览器内置标签是很常见的：</p>
<pre><code class="language-HTML">&lt;div&gt;
  &lt;img /&gt;
&lt;/div&gt;
</code></pre>
<p>有时你会希望以相同的方式嵌套自己的组件：</p>
<pre><code class="language-HTML">&lt;Card&gt;
  &lt;Avatar /&gt;
&lt;/Card&gt;
</code></pre>
<p>当你将内容嵌套在 JSX 标签中时，父组件将在名为 <code>children</code> 的 <strong>prop</strong> 中接收到该内容。</p>
<pre><code class="language-JavaScript">function Card({ children }) {
    return (
        &lt;div className=&quot;card&quot;&gt;
          {children}
        &lt;/div&gt;
      );
    }

export default function Profile() {
  return (
    &lt;Card&gt;
      {/* Any Content */}
    &lt;/Card&gt;
  );
}
</code></pre>
<p>就像一个「插槽」：</p>
<p><img src="https://zcnejflewpus.feishu.cn/space/api/box/stream/download/asynccode/?code=NzQyYzZkZjJhOGNjN2FiMjU2MmZiZjdhODhiZmEzMDdfcnVPdXd2ekFhRjkxTHdlbUR2RGdOTHpqd1lpalpyMlhfVG9rZW46QnBDOWJ2OHUxb0RXNjR4QXoweGNBR1RCbmhjXzE3NTM2NzU3MDQ6MTc1MzY3OTMwNF9WNA" alt=""></p>
<h1>条件渲染</h1>
<h2>1. 多 return</h2>
<p>当条件为真时, 渲染 <code>&lt;A /&gt;</code>，否则 <code>&lt;B /&gt;</code>。</p>
<pre><code class="language-JavaScript">function Item({ name, isPacked }) {
  if (isPacked) {
    return &lt;li className=&quot;item&quot;&gt;{name} ✅&lt;/li&gt;;
  }
  return &lt;li className=&quot;item&quot;&gt;{name}&lt;/li&gt;;
}
</code></pre>
<h2>2. 嵌套 return + 三目运算符</h2>
<p>当条件为真时, 渲染 <code>A</code>，否则 <code>B</code>。</p>
<pre><code class="language-JavaScript">function Item({ name, isPacked }) {
  return (
    &lt;li className=&quot;item&quot;&gt;
      {isPacked ? (
        &lt;del&gt;
          {name + ' ✅'}
        &lt;/del&gt;
      ) : (
        name
      )}
    &lt;/li&gt;
  );
}
</code></pre>
<ul>
<li>用 <code>()</code> 包裹起来的是 JSX 元素，用 <code>{}</code> 包裹起来的是 JavaScript 表达式。</li>
<li><code>()</code> 与 <code>{}</code> 可以相互嵌套。</li>
</ul>
<h2>3. 与运算符 &amp;&amp;</h2>
<p>当条件为真时渲染，否则不渲染任何东西。</p>
<pre><code class="language-JavaScript">return (
  &lt;li className=&quot;item&quot;&gt;
    {name} {isPacked &amp;&amp; '✅'}
  &lt;/li&gt;
);
</code></pre>
<p>仅当 <code>isPacked</code> 为 <code>true</code> 时，渲染勾选符号。</p>
<blockquote>
<p>当 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Logical_AND">JavaScript &amp;&amp; 表达式</a> 的左侧为 <code>true</code> 时，它则返回其右侧的值。当左侧的结果是 <code>false</code>，则整个表达式会变成 <code>false</code>。在 JSX 里，React 会将 <code>false</code> 视为一个“空值”，就像 <code>null</code> 或者 <code>undefined</code>，这样 React 就不会在这里进行任何渲染。</p>
<p>切勿将数字放在 <code>&amp;&amp;</code> 左侧，如果左侧是 <code>0</code>，整个表达式将变成左侧的值（<code>0</code>），React 此时则会渲染 <code>0</code> 而不是不进行渲染。</p>
</blockquote>
<p>React 不会渲染 <code>false</code>、<code>null</code>、<code>undefined</code> 、<code>&quot;&quot;</code>、<code>[]</code>，会渲染 <code>0</code> ，渲染 <code>{}</code> 会控制台警告。</p>
<h2>4. 作为变量的方式</h2>
<p>当条件为真时, 渲染 <code>A</code>，否则 <code>B</code>。</p>
<pre><code class="language-JavaScript">function Item({ name, isPacked }) {
  let itemContent = name;
  if (isPacked) {
    itemContent = (
      &lt;del&gt;
        {name + &quot; ✅&quot;}
      &lt;/del&gt;
    );
  }
  return (
    &lt;li&gt;
      {itemContent}
    &lt;/li&gt;
  );
}
</code></pre>
<ul>
<li>普通 JS 表达式可以被赋值给变量变量</li>
<li>用 <code>()</code> 包裹的 JSX 元素也可以被赋值给一个变量</li>
</ul>
<blockquote>
<p><code>&lt;del&gt;...&lt;/del&gt;</code> 是一个 JSX 元素，编译后会转换为 <code>React.createElement()</code> 调用。</p>
</blockquote>
<h1>列表渲染</h1>
<h2>Fragment</h2>
<p>Fragment 语法的简写形式是 <code>&lt;&gt; &lt;/&gt;</code>，但 <code>&lt;&gt; &lt;/&gt;</code> 无法接受 key 值。</p>
<p>使用 Fragment 语法让每个列表项都输出多个 DOM 节点，<code>Fragment</code> 标签本身并不会出现在 DOM 上。</p>
<pre><code class="language-JavaScript">import { Fragment } from 'react';

// ...

const listItems = people.map(person =&gt;
  &lt;Fragment key={person.id}&gt;
    &lt;h1&gt;{person.name}&lt;/h1&gt;
    &lt;p&gt;{person.bio}&lt;/p&gt;
  &lt;/Fragment&gt;
);
</code></pre>
<h2>如何设定 Key 值</h2>
<p>列表项必须有 key 值，key 值必须是唯一且不变的。</p>
<p>不同来源的数据往往对应不同的 key 值获取方式：</p>
<ul>
<li>来自数据库的数据： 如果你的数据是从数据库中获取的，那你可以直接使用数据表中的主键，因为它们天然具有唯一性。</li>
<li>本地产生数据： 如果你数据的产生和保存都在本地（例如笔记软件里的笔记），那么你可以使用一个自增计数器，<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Crypto/randomUUID" data-lark-is-custom="true">crypto.randomUUID()</a> 或者一个类似 <a href="https://www.npmjs.com/package/uuid" data-lark-is-custom="true">uuid</a> 的库来生成 key（<strong>仅用于数据初始化而非动态生成</strong>）。</li>
</ul>
<blockquote>
<p>组件每次重新渲染都会重新检查所有的 key 值，所以不要在运行过程中动态地产生 key，像是 <code>key={Math.random()}</code> 这种方式。这会导致每次重新渲染后的 key 值都不一样。</p>
</blockquote>
<h1>副作用</h1>
<h2>什么是纯函数？</h2>
<p>理想情况下，React 组件应当是纯函数。</p>
<p>纯函数：</p>
<ul>
<li>只负责自己的任务。它不会更改在该函数调用前就已存在的对象或变量。</li>
<li>输入相同，则输出相同。给定相同的输入，纯函数应总是返回相同的结果。</li>
</ul>
<p>更改组件内部的对象或变量是允许的。</p>
<h2>什么是副作用？</h2>
<ul>
<li>在计算机科学中，副作用是指函数在 <strong>执行过程</strong> 中对 <strong>外部环境</strong>（函数作用域之外）的可观察影响。也就是说，在函数内修改外部变量的行为是副作用。</li>
<li>在 React 语境下，副作用是 <strong>渲染之外</strong> 的操作，是不可预测的。副作用让组件的行为不再仅由输入决定。</li>
</ul>
<h2>副作用有哪些？</h2>
<p>通常包括：</p>
<ul>
<li>发起网络请求（<code>fetch()</code>）</li>
<li>定时器（<code>setTimeout()</code>、<code>setInterval()</code>）</li>
<li>直接修改 DOM（<code>document.title=&quot;&quot;</code>、<code>document.getElementById.style=&quot;&quot;</code>）</li>
</ul>
<h2>如果渲染过程触发副作用，会造成什么错误？</h2>
<p>React 的渲染过程是 React 根据当前 <code>props</code> 和 <code>state</code> 计算并生成虚拟 DOM 的操作。</p>
<p>React 的渲染机制基于 <strong>纯函数式更新</strong>，其核心假设是：</p>
<blockquote>
<p>给定相同的 <code>props</code> 和 <code>state</code>，组件必须返回相同的 JSX。、</p>
</blockquote>
<p>如果不满足这个假设，可能会导致一系列问题，其中最严重就是无限渲染循环的问题：</p>
<p>渲染 -&gt; 触发副作用 -&gt; 改变了渲染的初始条件 -&gt; 重新渲染 -&gt; 再次触发副作用……</p>
<h2>React 如何管理副作用？</h2>
<ol>
<li>让副作用属于<strong>事件处理程序</strong>：比如 <code>onClick</code>、<code>onChange</code>，这样即使事件处理程序是在组件内部定义的，它们也不会在渲染期间运行！</li>
<li>通过 Hook 将副作用与渲染逻辑隔离：比如调用 <code>useEffect</code> 将副作用 <strong>延迟到渲染完成后执行</strong>，避免阻塞渲染。</li>
</ol>
<h1>模块树、依赖树</h1>
<ul>
<li>渲染树表示单次渲染中 React 组件之间的嵌套关系。</li>
<li>构建 React 应用程序时，通常会有一个构建步骤。此步骤中，<a href="https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Extensions/Client-side_tools/Overview#%E7%8E%B0%E4%BB%A3%E5%B7%A5%E5%85%B7%E7%B3%BB%E7%BB%9F">bundler（捆绑器）</a>将捆绑所有必要的 JavaScript 以供客户端使用。bundler 使用依赖树来确定应包含哪些模块。</li>
</ul>
<h1>事件处理函数</h1>
<pre><code class="language-JavaScript">// 这个 alert 在组件渲染时触发，而不是点击时触发！
&lt;button onClick={alert('你点击了我！')}&gt;
</code></pre>
<p>如果想要定义内联事件处理函数，将其包装在匿名函数中，创建一个稍后调用的函数，而不会在每次渲染时执行其内部代码。如下所示：</p>
<pre><code class="language-JavaScript">&lt;button onClick={() =&gt; alert('你点击了我！')}&gt;
</code></pre>
<p>在 React 中所有事件都会传播（向上冒泡），除了 <code>onScroll</code>，它仅适用于你附加到的 JSX 标签。</p>
<h2>stopPropagation() 和 preventDefault()</h2>
<p>如果你想阻止一个事件到达父组件，调用 <code>e.stopPropagation()</code> ：</p>
<pre><code class="language-JavaScript">function Button({ handleClick }) {
  return (
    &lt;button onClick={e =&gt; {
      e.stopPropagation();
      handleClick ();
    }}&gt;
    &lt;/button&gt;
  );
}
</code></pre>
<p>通过在事件名称末尾添加 <code>Capture</code>，即便阻止了传播，也可以捕获子元素上的事件：</p>
<pre><code class="language-JavaScript">&lt;div onClickCapture={() =&gt; { /* 这会首先执行 */ }}&gt;
  &lt;button onClick={e =&gt; e.stopPropagation()} /&gt;
  &lt;button onClick={e =&gt; e.stopPropagation()} /&gt;
&lt;/div&gt;
</code></pre>
<p><code>onClickCapture</code> 处理函数是在事件 <strong>向下传递</strong> 的期间执行的。</p>
<p>使用 <code>e.preventDefault()</code> 来阻止默认行为。</p>
<h1>State</h1>
<h2>Hook 是什么？</h2>
<p>Hook 是 React 中以 use 开头的特殊的函数，在组件 <strong>渲染</strong> 中使用，而 State 是 React Hooks 之一。</p>
<blockquote>
<p>Hooks 只能在组件或自定义 Hook 的最顶层调用。 你不能在条件语句、循环语句或其他嵌套函数内调用 Hook。Hook 是函数，但可将它们视为你希望组件<strong>做点什么</strong>的声明。在组件顶部 “use” React 特性，类似于在文件顶部“导入”模块。</p>
</blockquote>
<h2>State有什么用？</h2>
<p>普通变量的局限：</p>
<ol>
<li>更改局部变量不会触发重新渲染。</li>
<li>发生重新渲染时，局部变量更改不会被保存。</li>
</ol>
<p>State</p>
<ol>
<li>State 变量 <strong>保存</strong> 渲染间的数据。</li>
<li>State setter 函数更新变量并 <strong>触发 React 再次渲染</strong> 组件。</li>
</ol>
<pre><code class="language-JavaScript">const [index, setIndex] = useState(0);
</code></pre>
<h2>React 如何知道返回哪个 state ？</h2>
<p>是按照组件渲染中 Hooks <strong>调用的顺序</strong> 进行区分的。这也是为什么不能在条件语句、循环语句或其他嵌套函数内调用 Hook。</p>
<p>在 React 内部，为每个组件保存了一个数组，其中每一项都是一个 state 对。每次渲染前将索引设为0，每次调用 useState 时，React 都会返回一个 state 对，并增加索引值以指向下一个 state 对。</p>
<pre><code class="language-JavaScript">[
    [xState, usexState],
    [yState, useyState],
    [zState, usezState]
]
</code></pre>
<h1>React 渲染</h1>
<h2>触发渲染的时机</h2>
<ol>
<li>组件的初次渲染。</li>
</ol>
<pre><code class="language-JavaScript">import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(&lt;App /&gt;);
</code></pre>
<ol start="2">
<li>组件（或者其祖先之一）的 <strong>状态发生了改变</strong>。主要是通过 setState 触发，……</li>
</ol>
<h2>进行渲染</h2>
<p>在触发渲染后，React 会调用组件来确定要在屏幕上显示的内容。</p>
<ul>
<li>在进行初次渲染时, React 会调用根组件。</li>
<li>对于后续的渲染, React 会调用内部状态 <strong>更新</strong> 触发了渲染的函数组件。</li>
</ul>
<h2>修改 DOM</h2>
<p>在渲染（调用）你的组件之后，React 将会修改 DOM。</p>
<ul>
<li><strong>对于初次渲染</strong>，React 会使用 <code>appendChild()</code>将其创建的所有 DOM 节点放在屏幕上。</li>
<li><strong>对于重渲染</strong>，在渲染时计算更改操作，React 只更改 **存在差异 **的 DOM 节点。</li>
</ul>
<h1>State 和渲染之间的关系</h1>
<p>渲染出来的组件「快照」就像是静态的（它的 props、事件处理函数和内部变量都是根据当前渲染时的 state 被计算出来的），直到下次渲染，React 会重新调用组件的函数，生成新的「快照」。</p>
<p>一次渲染中一个 state 变量的值只能有一个，不论怎么设置都是不变的。</p>
<h2>重新渲染的时机</h2>
<p>React 并不会在 set State 之后立即渲染，而是批处理。</p>
<h2>更新函数</h2>
<p>更新函数会被放入队列，下一次渲染期间被独立地执行。</p>
<p>通过更新函数，解决在一个渲染期间多次触发 set State 和预期的结果不同的问题（因为 state 是「静态」的）.</p>
<pre><code class="language-JavaScript">setNumber(n =&gt; n + 1); // state: 0 -&gt; 1
setNumber(n =&gt; n + 1); // state: 1 -&gt; 2
setNumber(n =&gt; n + 1); // state: 2 -&gt; 3
</code></pre>
<p>通常可以通过相应 state 变量的第一个字母来命名更新函数的参数：</p>
<pre><code class="language-JavaScript">setEnabled(e =&gt; !e);
setLastName(ln =&gt; ln.reverse());
setFriendCount(fc =&gt; fc * 2);
</code></pre>
<h1>Mutation</h1>
<p>Mutation 就是直接改变 state 对象/数组。</p>
<p>React 强烈建议避免这种操作。应该将 React 中所有的 state 都视为不可直接修改的，修改要使用 set。</p>
<h2>更新对象</h2>
<p>不能直接改变 state 对象，而是应该 set 一个新对象，如果修改对象的深层属性，可能要进行多重展开（连 Proxy 都要借助 Immer 库来实现，React 修改对象的操作也太麻烦了）。</p>
<blockquote>
<p>React 官方： React  并不依赖于 mutation ，所以你不需要对对象进行任何特殊操作。它不需要像很多「响应式」的解决方案一样去劫持对象的属性、总是用代理把对象包裹起来，或者在初始化时做其他工作。这也是 React 允许你把任何对象存放在 state 中——不管对象有多大——而不会造成有任何额外的性能或正确性问题的原因。</p>
</blockquote>
<h2>更新数组</h2>
<ul>
<li>不能使用 <code>arr[0] = 'bird'</code> 给数组元素赋值。</li>
<li>不能使用会 <strong>更改原始数组</strong> 的方法，例如 <code>push()</code> 和 <code>pop()</code>。</li>
</ul>
<table>
<thead>
<tr>
<th><br/></th>
<th>避免使用 (会改变原始数组)</th>
<th>推荐使用 (会返回一个新数组）</th>
</tr>
</thead>
<tbody>
<tr>
<td>添加元素</td>
<td>push，unshift</td>
<td>concat，[…arr] 展开语法</td>
</tr>
<tr>
<td>删除元素</td>
<td>pop，shift，splice</td>
<td>filter，slice</td>
</tr>
<tr>
<td>替换元素</td>
<td>splice，arr[i] = … 赋值</td>
<td>map</td>
</tr>
<tr>
<td>排序</td>
<td>reverse，sort</td>
<td>先将数组复制一份</td>
</tr>
</tbody>
</table>
<blockquote>
<p>不幸的是，虽然 <a data-lark-is-custom="true" href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/slice">slice 和 splice</a> 的名字相似，但作用却迥然不同：</p>
<ul>
<li><code>slice</code> 让你可以拷贝数组或是数组的一部分，返回值是留下的部分。</li>
<li><code>splice</code> 会直接修改原始数组（删除，插入），返回删掉元素的数组。</li>
</ul>
</blockquote>
<p><strong>拷贝要注意</strong>：然而，数组的拷贝是浅拷贝。因此，如果你修改了拷贝数组内部的某个对象，其实你正在直接修改当前的 state。</p>
]]></content:encoded>
      <media:content url="https://yuhhhy.cn/images/20250728-reactjs-0.avif" medium="image" type="image/avif"/>
    </item>
  </channel>
</rss>