<?xml version="1.0" encoding="UTF-8"?><rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>掘金 JavaScript</title><link>https://juejin.cn/tag/JavaScript</link><atom:link href="http://rss.144-124-237-35.sslip.io/juejin/tag/JavaScript" rel="self" type="application/rss+xml"></atom:link><description>掘金 JavaScript - Powered by AtomRSS</description><generator>AtomRSS</generator><webMaster>contact@atomgroup.dev (AtomRSS)</webMaster><language>en</language><image><url>https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/leancloud-assets/5d70fd6af940df373834.png~tplv-t2oaga2asx-image.image</url><title>掘金 JavaScript</title><link>https://juejin.cn/tag/JavaScript</link></image><lastBuildDate>Sat, 08 Aug 2026 06:56:02 GMT</lastBuildDate><ttl>5</ttl><item><title>无状态 Stateless：大模型为什么记不住你是谁</title><description>&lt;h2 data-id=&quot;heading-0&quot;&gt;无状态 Stateless：大模型为什么记不住你是谁&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;上一节课，我从 LLM 与 Agent 的区别出发，理解了 Tool Calling 如何让模型从&quot;回答问题&quot;走向&quot;干活&quot;。紧接着又用 AI Loop 把&quot;人盯着 AI 改&quot;编排成了可观测、可停止的闭环。&lt;/p&gt;
&lt;p&gt;但在写那些 Demo 的时候，我一直在做一件重复的事：&lt;strong&gt;每次调用大模型，都要手动把之前的对话历史全部带上。&lt;/strong&gt; 我当时只是照着写，并没有想清楚为什么必须这样做。这一节课，我要解决的问题是：&lt;strong&gt;大模型为什么不自己记住我？为什么要我每次都把聊天记录重新发一遍？&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;答案藏在两个词里：&lt;strong&gt;HTTP&lt;/strong&gt; 和 &lt;strong&gt;Stateless（无状态）&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-1&quot;&gt;调用 LLM 接口，本质是什么？&lt;/h3&gt;
&lt;p&gt;先回到最底层。我平时写的 &lt;code&gt;client.chat.completions.create({...})&lt;/code&gt;，看起来像是在调用一个&quot;智能&quot;的函数。但剥开 SDK 的包装，它本质就是一次 &lt;strong&gt;HTTP 调用&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot; lang=&quot;text&quot;&gt;我的程序  --HTTP POST--&amp;gt;  LLM 服务器（算力生成结果）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LLM 通过算力生成结果，而要能承受&lt;strong&gt;高并发、高可用&lt;/strong&gt;，后端就必须支持无状态（Stateless）。这不是大模型独有的要求，而是所有要扛住大规模流量的 HTTP 服务共同的设计选择。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-2&quot;&gt;无状态是什么？为了减小负担，支持高并发&lt;/h3&gt;
&lt;p&gt;一个很直接的定义：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;无状态是为了减小负担，支持高并发。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;具体到 HTTP 协议层面，它是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot; lang=&quot;text&quot;&gt;HTTP 无状态协议（无状态 GET/POST... restful 从服务器获取资源）
  + header（cookie? 识别身份 / Authorization）（增量）

所有人都公平
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&quot;公平&quot;很关键。HTTP 本身是无状态的，意思是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;每次请求都是独立的&lt;/strong&gt;，不依赖于之前的请求；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务器不需要存储客户端的状态&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务器可以水平扩展&lt;/strong&gt;，因为每个请求都是独立的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那&quot;有状态&quot;呢？如果服务器要记住&quot;你是谁&quot;，就得在服务端维护会话状态，这会导致 LLM 服务器压力太大。想象一下，几百万用户同时在和模型聊天，如果每条连接都要在服务器内存里保存对话状态，一台服务器根本扛不住，而且一旦这台服务器挂了，所有人的对话上下文就全丢了。&lt;/p&gt;
&lt;p&gt;所以 LLM 选择无状态，本质上是&lt;strong&gt;基于 HTTP 的无状态特性&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个请求独立；&lt;/li&gt;
&lt;li&gt;服务器不需要记住任何客户端的会话；&lt;/li&gt;
&lt;li&gt;任何一台服务器处理这个请求都没差别。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-id=&quot;heading-3&quot;&gt;LLM 运行的底层规则&lt;/h3&gt;
&lt;p&gt;把上面的理解整理一下，LLM 运行时有几条底层规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无状态 Stateless&lt;/strong&gt;：服务端不保存你的对话状态；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;尝试让 LLM&quot;懂&quot;我们&lt;/strong&gt;：更加清晰地表达我们的需求；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;每次手动带上全部对话&lt;/strong&gt;：因为服务器不记，所以只能由客户端把上下文一起发过去；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务器端并发&lt;/strong&gt;：在任何一台服务器上运行都没差别。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这四条规则串起来，就解释了我写 Demo 时那个&quot;每次都要 push chatHistory&quot;的动作——不是 SDK 的设计偏好，而是无状态架构的必然结果。服务器不记你，你只能自己记，然后每次请求都带上。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-4&quot;&gt;Stateless 的优势：不是&quot;请求都一样&quot;，而是&quot;不依赖某个实例&quot;&lt;/h3&gt;
&lt;p&gt;笔记里有一段很严谨的表述，我觉得值得原样保留：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Stateless 的含义是：&lt;em&gt;服务端不依赖&lt;strong&gt;某个服务器进程&lt;/strong&gt;中&lt;strong&gt;保存的会话状态&lt;/strong&gt;来处理请求；处理请求所需的信息都由&lt;strong&gt;请求本身或外部共享存储提供&lt;/strong&gt;。&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-id=&quot;heading-5&quot;&gt;为什么能简化高并发&lt;/h4&gt;
&lt;p&gt;简单点说，由于 Stateless，每一次请求对于 LLM 来说都是一样的。那些携带过来的增量（历史对话、system prompt），本质也不过就是上下文信息罢了，不影响请求的本质。也就是说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;请求的处理&lt;strong&gt;不依赖某个服务实例&lt;/strong&gt;内部持有的会话状态。&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那么只要增加处理请求的服务器，就能解决高并发的问题——这就是&lt;strong&gt;水平扩展&lt;/strong&gt;。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-6&quot;&gt;水平扩展具体有哪些：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;请求可以被&lt;strong&gt;负载均衡&lt;/strong&gt;到任意实例；&lt;/li&gt;
&lt;li&gt;不需要把同一个用户&lt;strong&gt;固定路由&lt;/strong&gt;到某台服务器；&lt;/li&gt;
&lt;li&gt;实例故障时，用户请求可以&lt;strong&gt;切换到其他实例&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扩容和缩容&lt;/strong&gt;相对简单；&lt;/li&gt;
&lt;li&gt;不需要在每个实例之间&lt;strong&gt;同步本地会话状态&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-id=&quot;heading-7&quot;&gt;但要严谨地说&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;其实是 &lt;em&gt;Stateless 降低了水平扩展和故障转移的复杂度，但能否支撑高并发，仍取决于模型推理资源、调度策略和其他系统瓶颈。&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;总结起来：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Stateless 的核心优势不是&quot;所有请求都一样&quot;，而是&quot;请求处理不依赖某个服务实例本地保存的状态&quot;，因此更容易进行水平扩展、负载均衡和故障转移。对于 LLM，历史对话可以作为请求上下文传入，但上下文长度会影响计算成本，所以 Stateless 只是简化扩展，并不意味着增加服务器就能无限解决高并发问题。&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这里提到了一个新的问题：&lt;strong&gt;上下文长度会影响计算成本。&lt;/strong&gt; 每次带上全部对话，不只是网络传输的负担，更是模型推理时算力的负担。对话越长，每次请求的 token 开销就越大。这也直接引出了后面 chatHistory 的问题。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-8&quot;&gt;从 Stateless 到工程升级：让 LLM 越来越&quot;懂&quot;我们&lt;/h3&gt;
&lt;p&gt;既然 LLM 是无状态的，每次都要靠我们带上上下文，那怎么让模型更好地理解我们？现在有一个大概的升级路径：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot; lang=&quot;text&quot;&gt;Prompt Engineering  →  Context Engineering  →  Loop Engineering  →  Harness
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-9&quot;&gt;Prompt Engineering：聊天对话&lt;/h4&gt;
&lt;p&gt;第一层是 Prompt Engineering。做法是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;历史对话；&lt;/li&gt;
&lt;li&gt;知识库 &lt;code&gt;claude.md&lt;/code&gt;、&lt;code&gt;agent.md&lt;/code&gt; 作为上下文。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过本质还是依赖 LLM 的概率性生成获得结果，有一个很生动的比喻：&lt;strong&gt;抽卡&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Prompt 质量或设计只能提升抽到金卡的概率，不是特别可控的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这让我想起之前写 Prompt 的经历——同样一个 Prompt，有时模型回答得很好，有时又跑偏了。本质原因就是这还是一个&lt;strong&gt;概率游戏&lt;/strong&gt;，Prompt Engineering 解决不了 AI 可信度问题。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-10&quot;&gt;Context Engineering：上下文工程&lt;/h4&gt;
&lt;p&gt;第二层是 Context Engineering。当 LLM 不懂的、没有的知识，就通过更优质的信息喂给它：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RAG&lt;/strong&gt;：检索增强生成，把外部知识检索回来塞进上下文；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP&lt;/strong&gt;：Model Context Protocol，标准化地连接外部数据源和工具；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;skill&lt;/strong&gt;：把特定能力封装成可复用的技能。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-id=&quot;heading-11&quot;&gt;Loop Engineering：循环工程&lt;/h4&gt;
&lt;p&gt;第三层是 Loop Engineering：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ReAct&lt;/strong&gt;：Reasoning → Action → Observation 的循环框架；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检测标准&lt;/strong&gt;：每一轮都要有可验收的标准。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-id=&quot;heading-12&quot;&gt;Harness：AI 工程手段&lt;/h4&gt;
&lt;p&gt;第四层是 Harness，也就是上一节课讲的&quot;护栏/约束框架&quot;。它负责约束目标、执行、评估和资源边界，让 Loop 从实验变成生产系统。&lt;/p&gt;
&lt;p&gt;这四层不是替代关系，而是叠加关系。Prompt 是基础，Context 补充知识，Loop 处理多步骤任务，Harness 保证安全和可控。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-13&quot;&gt;chatHistory 有什么问题？&lt;/h3&gt;
&lt;p&gt;理解了 Stateless 之后，回头看 Demo 里那个 &lt;code&gt;chatHistory&lt;/code&gt; 全局变量，就能看到几个真实的问题：&lt;/p&gt;
&lt;h4 data-id=&quot;heading-14&quot;&gt;1. 没有维护全部的 history&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;大模型的回复也是关键。&lt;/strong&gt; 如果只存了用户的消息，不存模型的回复，那下一轮请求时上下文就是残缺的，模型就无法理解&quot;你刚才说的那个&quot;指的是什么。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-15&quot;&gt;2. 成本：messages 越来越大，token 开销越大&lt;/h4&gt;
&lt;p&gt;因为每次请求都要带上全部历史对话，&lt;code&gt;messages&lt;/code&gt; 数组会越来越长。对话轮数越多，每次请求的 token 开销就越大。这正是前面那段严谨表述里说的&quot;上下文长度会影响计算成本&quot;。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-16&quot;&gt;3. LRU 与 capacity&lt;/h4&gt;
&lt;p&gt;这是减小 messages 或者说上下文也就是开销的两种方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LRU（Least Recently Used）&lt;/strong&gt;：一次对话里一直聊，是因为任务还没完成。但 tokens 开销会变大，最近在聊的留下，久远了的可以适当地删除。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;capacity（容量）&lt;/strong&gt;：上下文窗口是有上限的，不能无限增长。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这其实就是工程上要做的事情：在有限上下文窗口里，用 LRU 策略保留最近、最相关的对话，淘汰久远的部分，在成本和上下文完整性之间找平衡。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-17&quot;&gt;Demo 实战：手动维护 chatHistory&lt;/h3&gt;
&lt;p&gt; Demo 项目结构很简单：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot; lang=&quot;text&quot;&gt;demo/
  ├── index.mjs
  ├── package.json
  └── pnpm-lock.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;package.json&lt;/code&gt; 依赖 &lt;code&gt;dotenv&lt;/code&gt; 和 &lt;code&gt;openai&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-json&quot; lang=&quot;json&quot;&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;demo&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&quot;version&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;1.0.0&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&quot;main&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;index.js&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&quot;scripts&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;echo \&quot;Error: no test specified\&quot; &amp;amp;&amp;amp; exit 1&quot;&lt;/span&gt;
  &lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&quot;keywords&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&quot;author&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&quot;license&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;ISC&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;commonjs&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;&quot;dependencies&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&quot;dotenv&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;^17.4.2&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;&quot;openai&quot;&lt;/span&gt;&lt;span class=&quot;hljs-punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;^7.4.0&quot;&lt;/span&gt;
  &lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;hljs-punctuation&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;核心代码在 &lt;code&gt;index.mjs&lt;/code&gt;。这里用 DeepSeek 作为模型服务，通过 OpenAI 兼容接口调用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;OpenAI&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;openai&#39;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { config } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;dotenv&#39;&lt;/span&gt;;
&lt;span class=&quot;hljs-title function_&quot;&gt;config&lt;/span&gt;();

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; client = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;OpenAI&lt;/span&gt;({
  &lt;span class=&quot;hljs-attr&quot;&gt;apiKey&lt;/span&gt;: process.&lt;span class=&quot;hljs-property&quot;&gt;env&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;DEEPSEEK_API_KEY&lt;/span&gt;,
  &lt;span class=&quot;hljs-attr&quot;&gt;baseURL&lt;/span&gt;: process.&lt;span class=&quot;hljs-property&quot;&gt;env&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;DEEPSEEK_BASE_URL&lt;/span&gt;,
})
&lt;span class=&quot;hljs-comment&quot;&gt;// 异步任务&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// promise 实例，最后返回 promise 对象，里面有 resolve, reject 属性&lt;/span&gt;

&lt;span class=&quot;hljs-comment&quot;&gt;// 对话历史 （全局变量）&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; chatHistory = [
  { &lt;span class=&quot;hljs-attr&quot;&gt;role&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;system&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;content&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;你是一个严谨的助手&#39;&lt;/span&gt; }
];
&lt;span class=&quot;hljs-keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;testStateless&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;第一次请求，告诉模型一个信息&#39;&lt;/span&gt;);
  &lt;span class=&quot;hljs-comment&quot;&gt;// 为了让llm 懂我们，每次对话都携带上 history （上下文） &lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// 将信息添加到 history 中&lt;/span&gt;
  chatHistory.&lt;span class=&quot;hljs-title function_&quot;&gt;push&lt;/span&gt;({
    &lt;span class=&quot;hljs-attr&quot;&gt;role&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;user&#39;&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;content&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;请记住我叫字节戴&#39;&lt;/span&gt;
  })
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; response = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; client.&lt;span class=&quot;hljs-property&quot;&gt;chat&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;completions&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;create&lt;/span&gt;({
    &lt;span class=&quot;hljs-attr&quot;&gt;model&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;deepseek-v4-flash&#39;&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;messages&lt;/span&gt;: chatHistory
  });
  &lt;span class=&quot;hljs-comment&quot;&gt;// 模型回复&lt;/span&gt;
  chatHistory.&lt;span class=&quot;hljs-title function_&quot;&gt;push&lt;/span&gt;({
    &lt;span class=&quot;hljs-attr&quot;&gt;role&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;assistant&#39;&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;content&lt;/span&gt;: response.&lt;span class=&quot;hljs-property&quot;&gt;choices&lt;/span&gt;[&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;].&lt;span class=&quot;hljs-property&quot;&gt;message&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;content&lt;/span&gt;
  })
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;模型回复：&#39;&lt;/span&gt;, 
    response.&lt;span class=&quot;hljs-property&quot;&gt;choices&lt;/span&gt;[&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;].&lt;span class=&quot;hljs-property&quot;&gt;message&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;content&lt;/span&gt;);
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;第二次请求，直接问我是谁&#39;&lt;/span&gt;);
  &lt;span class=&quot;hljs-comment&quot;&gt;// 增添信息到 history 中 &lt;/span&gt;
  chatHistory.&lt;span class=&quot;hljs-title function_&quot;&gt;push&lt;/span&gt;({
    &lt;span class=&quot;hljs-attr&quot;&gt;role&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;user&#39;&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;content&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;请问我的名字是什么？&#39;&lt;/span&gt;
  })
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; response2 = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; client.&lt;span class=&quot;hljs-property&quot;&gt;chat&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;completions&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;create&lt;/span&gt;({
    &lt;span class=&quot;hljs-attr&quot;&gt;model&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;deepseek-v4-flash&#39;&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;messages&lt;/span&gt;: chatHistory
  });
  &lt;span class=&quot;hljs-comment&quot;&gt;// 对话历史&lt;/span&gt;
  chatHistory.&lt;span class=&quot;hljs-title function_&quot;&gt;push&lt;/span&gt;({
    &lt;span class=&quot;hljs-attr&quot;&gt;role&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;assistant&#39;&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;content&lt;/span&gt;: response2.&lt;span class=&quot;hljs-property&quot;&gt;choices&lt;/span&gt;[&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;].&lt;span class=&quot;hljs-property&quot;&gt;message&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;content&lt;/span&gt;
  })
  &lt;span class=&quot;hljs-comment&quot;&gt;// 模型回复&lt;/span&gt;
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;模型回复：&#39;&lt;/span&gt;, 
    response2.&lt;span class=&quot;hljs-property&quot;&gt;choices&lt;/span&gt;[&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;].&lt;span class=&quot;hljs-property&quot;&gt;message&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;content&lt;/span&gt;);
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(chatHistory);
}

&lt;span class=&quot;hljs-comment&quot;&gt;// console.log(testStateless())&lt;/span&gt;
&lt;span class=&quot;hljs-title function_&quot;&gt;testStateless&lt;/span&gt;()
  .&lt;span class=&quot;hljs-title function_&quot;&gt;catch&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-params&quot;&gt;err&lt;/span&gt; =&amp;gt;&lt;/span&gt; { &lt;span class=&quot;hljs-comment&quot;&gt;// test函数中的 reject 方法 &lt;/span&gt;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(err);
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-18&quot;&gt;这段代码在做什么&lt;/h4&gt;
&lt;p&gt;整个 Demo 演示了 Stateless 的核心：&lt;strong&gt;模型本身不记任何东西，是&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;chatHistory&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;这个全局变量在替它记。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;执行流程是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化 &lt;code&gt;chatHistory&lt;/code&gt;，放一条 &lt;code&gt;system&lt;/code&gt; 消息设定助手人设；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第一次请求&lt;/strong&gt;：push 一条 &lt;code&gt;user&lt;/code&gt; 消息&quot;请记住我叫字节戴&quot;，把整个 &lt;code&gt;chatHistory&lt;/code&gt; 发给模型；&lt;/li&gt;
&lt;li&gt;模型回复后，把 &lt;code&gt;assistant&lt;/code&gt; 的回复也 push 进 &lt;code&gt;chatHistory&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二次请求&lt;/strong&gt;：push 一条 &lt;code&gt;user&lt;/code&gt; 消息&quot;请问我的名字是什么？&quot;，再次把&lt;strong&gt;完整的&lt;/strong&gt; &lt;code&gt;chatHistory&lt;/code&gt; 发给模型；&lt;/li&gt;
&lt;li&gt;模型因为上下文里有&quot;我叫字节戴&quot;这句话，所以能回答出&quot;字节戴&quot;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意第二次请求时，&lt;code&gt;chatHistory&lt;/code&gt; 已经包含了四条消息：&lt;code&gt;system&lt;/code&gt; + 第一次的 &lt;code&gt;user&lt;/code&gt; + 第一次的 &lt;code&gt;assistant&lt;/code&gt; + 第二次的 &lt;code&gt;user&lt;/code&gt;。模型能记住我的名字，不是因为它的服务器存了我的状态，而是因为我&lt;strong&gt;把上一轮对话原封不动地又发了一遍&lt;/strong&gt;。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-19&quot;&gt;代码里的几个学习信号&lt;/h4&gt;
&lt;p&gt;注释里藏着几个值得注意的点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;// 异步任务&lt;/code&gt; / &lt;code&gt;// promise 实例，最后返回 promise 对象，里面有 resolve, reject 属性&lt;/code&gt;：&lt;code&gt;client.chat.completions.create&lt;/code&gt; 返回的是 Promise，所以用 &lt;code&gt;async/await&lt;/code&gt;，最外层用 &lt;code&gt;.catch()&lt;/code&gt; 接住 reject。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;// 为了让llm 懂我们，每次对话都携带上 history （上下文）&lt;/code&gt;：这句话就是 Stateless 的实操翻译——&quot;让 LLM 懂我们&quot;靠的不是服务端记忆，而是每次手动带上上下文。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;// console.log(testStateless())&lt;/code&gt; 被注释掉了，改成 &lt;code&gt;testStateless().catch(...)&lt;/code&gt;：因为 &lt;code&gt;testStateless&lt;/code&gt; 返回的是 Promise，直接 &lt;code&gt;console.log&lt;/code&gt; 只会打印 &lt;code&gt;Promise { &amp;lt;pending&amp;gt; }&lt;/code&gt;，必须用 &lt;code&gt;await&lt;/code&gt; 或 &lt;code&gt;.then/.catch&lt;/code&gt; 才能拿到真实结果。&lt;/li&gt;
&lt;li&gt;两次请求之间，&lt;code&gt;assistant&lt;/code&gt; 的回复都被 push 进了 &lt;code&gt;chatHistory&lt;/code&gt;：这对应了前面说的&quot;大模型的回复也是关键&quot;，如果不存模型回复，上下文就残缺了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-id=&quot;heading-20&quot;&gt;我现在怎么理解这节课&lt;/h3&gt;
&lt;p&gt;在此之前，我对&quot;为什么要带 chatHistory&quot;的理解是模糊的——只知道照着写，不知道为什么。现在我理清了一条完整的因果链：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot; lang=&quot;text&quot;&gt;LLM 调用本质是 HTTP 调用
  → HTTP 是无状态协议（每个请求独立，服务器不存客户端状态）
    → LLM 服务端不保存你的对话状态
      → 每次请求必须手动带上全部对话上下文
        → messages 越来越长，token 开销越来越大
          → 需要 LRU、capacity 管理来控制成本
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Stateless 的核心优势不是&quot;所有请求都一样&quot;，而是&quot;请求处理不依赖某个服务实例本地保存的状态&quot;。正因如此，LLM 服务可以水平扩展、负载均衡、故障转移——你发出去的请求落在哪台服务器上，结果都一样。&lt;/p&gt;
&lt;p&gt;但 Stateless 也带来了代价：上下文管理的责任从服务端转移到了客户端。我们要自己维护 &lt;code&gt;chatHistory&lt;/code&gt;，要在成本和上下文完整性之间权衡，要用 LRU 淘汰久远的对话。这也解释了为什么从 Prompt Engineering 到 Context Engineering、Loop Engineering 再到 Harness，每一层升级本质上都是在回答同一个问题：&lt;strong&gt;在无状态的约束下，怎样让模型更可靠地理解我们、完成任务。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;回到 Vibe Coding 时代，这个认知让我重新看待平时用 Cursor、Claude Code 这些工具时的体验。它们能&quot;记住&quot;我的项目上下文，不是模型在服务端帮我记着，而是工具在客户端帮我组装上下文、管理 token、做检索和裁剪。Stateless 是地基，上面所有的&quot;智能感&quot;都是工程一层一层搭上去的。&lt;/p&gt;
&lt;p&gt;这里我暂时没有踩坑记录，但有一个后续要验证的问题：如果对话很长，LRU 淘汰策略具体该怎么实现？是简单按条数截断，还是按 token 数计算？截断的时候从哪里切才不会破坏上下文连贯性？这些应该会在后面遇到。&lt;/p&gt;</description><link>https://juejin.cn/post/7671185982582865972</link><guid isPermaLink="false">https://juejin.cn/post/7671185982582865972</guid><pubDate>Sat, 08 Aug 2026 02:55:34 GMT</pubDate><author>Asize</author><category>人工智能</category><category>JavaScript</category><category>架构</category></item><item><title>学了 useContext 还是不理解？从 prop drilling 到 useMouse 的一次完整复盘</title><description>&lt;h2 data-id=&quot;heading-0&quot;&gt;今日重点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;组件通信的四种关系与各自适用边界&lt;/li&gt;
&lt;li&gt;useContext 三步模板：createContext → Provider → useContext&lt;/li&gt;
&lt;li&gt;封装自定义 Hook 的意义：复用、校验、隔离&lt;/li&gt;
&lt;li&gt;Context 与单向数据流的关系&lt;/li&gt;
&lt;li&gt;谁触发组件渲染，memo 能拦住谁拦不住谁&lt;/li&gt;
&lt;li&gt;useMouse 的抽象过程与 mousemove 高频渲染的选择&lt;/li&gt;
&lt;li&gt;useEffect 借还模式与 Hooks 调用规则&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-id=&quot;heading-1&quot;&gt;知识关系&lt;/h2&gt;
&lt;p&gt;组件通信是背景问题 → useContext 是跨层级通信方案 → Provider 的 value 变化驱动数据订阅者渲染 → 封装自定义 Hook 把 Context 消费逻辑模块化 → useMouse 展示如何把事件监听 + 状态管理抽象为可复用 Hook → 高频事件引出 useState 与 useRef 的渲染差异 → useEffect 清理函数保证不泄漏。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-2&quot;&gt;组件通信的四种关系&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-3&quot;&gt;知识点&lt;/h3&gt;
&lt;p&gt;React 页面由组件构成，组件之间需要传递数据，这就是组件通信。根据组件在树中的位置关系，有四种通信方式。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-4&quot;&gt;关键代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs&quot;&gt;父子：props（父→子） + 回调函数（子→父）
兄弟：状态提升到公共父组件
爷孙：useContext 隧道直达
陌生：状态管理库（Redux、Zustand）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-5&quot;&gt;运行过程&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;父子&lt;/strong&gt;是最基础的模型：父把数据写在子组件的标签属性上，子通过函数参数接收。子不能直接改父的数据，只能通过父传下来的回调函数&quot;通知&quot;父。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;兄弟&lt;/strong&gt;没有直接通道，必须把共享数据提升到最近的公共父组件，由父统一管理、分发给两个子。本质是两个父子通信拼在一起。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;爷孙&lt;/strong&gt;就是 prop drilling 的痛点：爷爷有数据，孙子需要用，中间的父亲被迫接收并转发自己不关心的 props。层级越深越难维护。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;陌生&lt;/strong&gt;是两个组件在树中完全没有共同祖先，通常出现在大型应用中，需要专门的状态管理库。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-6&quot;&gt;为什么这样设计&lt;/h3&gt;
&lt;p&gt;React 坚持单向数据流：数据从父流向子，不能反向。好处是数据变化可追踪——只有持有 state 的组件才能改它，其他组件只能通过回调&quot;申请&quot;。如果数据可以双向随意修改，bug 排查时会找不到元凶。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-7&quot;&gt;使用边界&lt;/h3&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;关系&lt;/th&gt;&lt;th&gt;方案&lt;/th&gt;&lt;th&gt;适用场景&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;父子&lt;/td&gt;&lt;td&gt;props&lt;/td&gt;&lt;td&gt;层级浅，一传一&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;兄弟&lt;/td&gt;&lt;td&gt;状态提升&lt;/td&gt;&lt;td&gt;兄弟少，公共父亲近&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;爷孙&lt;/td&gt;&lt;td&gt;useContext&lt;/td&gt;&lt;td&gt;中间层不需要感知数据&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;陌生&lt;/td&gt;&lt;td&gt;状态库&lt;/td&gt;&lt;td&gt;全局共享，关系复杂&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 data-id=&quot;heading-8&quot;&gt;自测&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;兄弟组件通信为什么必须经过父组件？&lt;/li&gt;
&lt;li&gt;状态提升的本质是什么？&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-id=&quot;heading-9&quot;&gt;参考答案&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;React 数据只能从上往下流，两个兄弟之间没有横向通道，只能把数据提到公共父组件再分发。&lt;/li&gt;
&lt;li&gt;本质是两个父子通信拼在一起：兄弟 A 通过回调通知父，父通过 props 传给兄弟 B。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-10&quot;&gt;useContext 三步模板&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-11&quot;&gt;知识点&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useContext&lt;/code&gt; 是 React 提供的跨层级数据传递方案。它在组件树中开一条&quot;数据隧道&quot;，后代组件直接从祖先拿数据，不用一层层传 props。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-12&quot;&gt;真实问题&lt;/h3&gt;
&lt;p&gt;下面这种情况，Father 不需要 theme，但必须接收并转发：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-string&quot;&gt;&#39;dark&#39;&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Father&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;theme&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Father&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;{ theme }&lt;/span&gt;) {          &lt;span class=&quot;hljs-comment&quot;&gt;// 不用但必须写&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Child&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;theme&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;      &lt;span class=&quot;hljs-comment&quot;&gt;// 必须往下传&lt;/span&gt;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Child&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;{ theme }&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;{theme}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;           &lt;span class=&quot;hljs-comment&quot;&gt;// 真正使用&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;层级一深，中间组件全被污染。useContext 解决的就是这个问题。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-13&quot;&gt;关键代码&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;第一步：创建 Context&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { createContext } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt; = &lt;span class=&quot;hljs-title function_&quot;&gt;createContext&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;createContext&lt;/code&gt; 的参数是默认值，不是共享数据。默认值只在组件没有被 Provider 包裹时兜底使用。真正的共享数据由 Provider 的 &lt;code&gt;value&lt;/code&gt; 决定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步：Provider 广播&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [theme, setTheme] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;)

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Page&lt;/span&gt; /&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{()&lt;/span&gt; =&amp;gt;&lt;/span&gt; setTheme(theme === &#39;light&#39; ? &#39;dark&#39; : &#39;light&#39;)}&amp;gt;
        切换主题
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Provider 放在组件树的任意位置，它的作用域就是它包裹的所有后代组件。Provider 不是全局的——放在哪，范围就到哪。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三步：useContext 消费&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useContext } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;../ThemeContext&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Child&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;)
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;{theme}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-14&quot;&gt;运行过程&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-arduino&quot; lang=&quot;arduino&quot;&gt;点击&lt;span class=&quot;hljs-string&quot;&gt;&quot;切换主题&quot;&lt;/span&gt;按钮
  → &lt;span class=&quot;hljs-built_in&quot;&gt;setTheme&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;dark&#39;&lt;/span&gt;)  &lt;span class=&quot;hljs-comment&quot;&gt;// App 的 state 变了&lt;/span&gt;
    → App 重新执行，theme 变量变成 &lt;span class=&quot;hljs-string&quot;&gt;&#39;dark&#39;&lt;/span&gt;
      → &amp;lt;Provider value=&lt;span class=&quot;hljs-string&quot;&gt;&quot;dark&quot;&lt;/span&gt;&amp;gt;  &lt;span class=&quot;hljs-comment&quot;&gt;// 广播新值&lt;/span&gt;
        → 所有调了 &lt;span class=&quot;hljs-built_in&quot;&gt;useContext&lt;/span&gt;(ThemeContext) 的组件标记为脏
          → Page 重新渲染，拿到 &lt;span class=&quot;hljs-string&quot;&gt;&#39;dark&#39;&lt;/span&gt;
          → Child 重新渲染，拿到 &lt;span class=&quot;hljs-string&quot;&gt;&#39;dark&#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-15&quot;&gt;为什么叫&quot;消费&quot;&lt;/h3&gt;
&lt;p&gt;React 用&quot;生产者-消费者&quot;模式命名：Provider 是生产者（提供数据），useContext 是消费者（订阅数据）。&quot;消费&quot;不是&quot;获取一次就完了&quot;，而是持续订阅——Provider 的 value 一变，消费者自动拿到新值并重新渲染。&lt;/p&gt;
&lt;p&gt;类比：自来水厂是 Provider，水管是 Context，你家水龙头是 Consumer。水厂压力一变，你家的水立刻跟着变。你在&quot;消费&quot;自来水，不是去水厂&quot;获取&quot;水。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-16&quot;&gt;容易混淆&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;createContext(&#39;默认值&#39;)&lt;/code&gt; 里的参数&lt;strong&gt;不是&lt;/strong&gt;共享数据。共享数据是 Provider 的 &lt;code&gt;value&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt; = &lt;span class=&quot;hljs-title function_&quot;&gt;createContext&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;)   &lt;span class=&quot;hljs-comment&quot;&gt;// 默认值&lt;/span&gt;

&amp;lt;&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;Provider&lt;/span&gt; value=&lt;span class=&quot;hljs-string&quot;&gt;&quot;dark&quot;&lt;/span&gt;&amp;gt;          &lt;span class=&quot;hljs-comment&quot;&gt;// 这才是共享数据&lt;/span&gt;
  &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Child&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;                                   &lt;span class=&quot;hljs-comment&quot;&gt;// useContext → &quot;dark&quot;&lt;/span&gt;
&amp;lt;/&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;Provider&lt;/span&gt;&amp;gt;

&lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Child&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;                                     &lt;span class=&quot;hljs-comment&quot;&gt;// 没 Provider → &quot;light&quot;（默认值兜底）&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-17&quot;&gt;使用边界&lt;/h3&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;适合放 Context&lt;/th&gt;&lt;th&gt;不适合放 Context&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;全局主题&lt;/td&gt;&lt;td&gt;频繁变化的值（输入框）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;当前用户信息&lt;/td&gt;&lt;td&gt;只在一两个组件用的数据&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;语言设置&lt;/td&gt;&lt;td&gt;能用 props 简单解决的场景&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;滥用 Context 会让组件数据来源变隐晦，且 value 变了所有消费者都重渲染，不适合高频更新的数据。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-18&quot;&gt;自测&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;createContext 的参数是什么？和 Provider 的 value 有什么关系？&lt;/li&gt;
&lt;li&gt;Provider 是全局的吗？为什么？&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-id=&quot;heading-19&quot;&gt;参考答案&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;createContext 的参数是默认值，只在后代组件找不到 Provider 时兜底。Provider 的 value 才是真正的共享数据，会覆盖默认值。&lt;/li&gt;
&lt;li&gt;不是全局的。Provider 放在哪个位置，它的作用域就是它包裹的那部分组件树。外面的组件拿不到数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-20&quot;&gt;封装 useTheme 的意义&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-21&quot;&gt;知识点&lt;/h3&gt;
&lt;p&gt;不封装直接用 &lt;code&gt;useContext(ThemeContext)&lt;/code&gt; 也能跑。封装成 &lt;code&gt;useTheme()&lt;/code&gt; 有三个好处。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-22&quot;&gt;关键代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;../ThemeContext&#39;&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useContext } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-23&quot;&gt;为什么这样设计&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;一、少写 import。&lt;/strong&gt; 每个消费组件不用同时引 &lt;code&gt;useContext&lt;/code&gt; 和 &lt;code&gt;ThemeContext&lt;/code&gt;，只用引 &lt;code&gt;useTheme&lt;/code&gt; 一个。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二、可以加错误校验。&lt;/strong&gt; 如果有人忘了写 Provider，没封装时静默返回默认值，bug 很难排查。封装后：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; context = &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;)
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (context === &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Error&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;useTheme 必须在 Provider 内部使用！&#39;&lt;/span&gt;)
  }
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; context
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;忘了包 Provider → 立刻报错，问题一目了然。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;三、改实现不惊动使用者。&lt;/strong&gt; 假设后面不依赖 Context 了，换其他方案，只要改 &lt;code&gt;useTheme&lt;/code&gt; 一个函数，所有消费组件不需要动一行代码。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-24&quot;&gt;使用边界&lt;/h3&gt;
&lt;p&gt;小项目可能感受不到差别，但组件多了、跨团队了，封装的价值就体现出来了。hooks 目录属于项目架构的一部分。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-25&quot;&gt;自测&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;封装 useTheme 的最大价值是什么？&lt;/li&gt;
&lt;li&gt;不封装直接用 useContext(ThemeContext) 有什么隐患？&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-id=&quot;heading-26&quot;&gt;参考答案&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;能在取值前做校验，忘了写 Provider 时立刻报错而不是静默失败。&lt;/li&gt;
&lt;li&gt;没有校验，如果组件没有被 Provider 包裹，会静默拿到默认值，bug 很难发现。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-27&quot;&gt;Context 与单向数据流&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-28&quot;&gt;知识点&lt;/h3&gt;
&lt;p&gt;Context 没有改变 React 单向数据流的规则。数据还是从上往下流，只是换了一条路。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-29&quot;&gt;运行过程&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs&quot;&gt;props 方式：  App → Page → Child    走楼梯，层层经过
Context 方式： App ═══════ Child    坐电梯，直达
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;方向永远是祖先到后代，单向往下。子组件依然不能反过来改 Provider 的数据，除非 Provider 同时把 setState 通过 value 传下来。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-30&quot;&gt;容易混淆&lt;/h3&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;props&lt;/th&gt;&lt;th&gt;Context&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;数据流向&lt;/td&gt;&lt;td&gt;上→下&lt;/td&gt;&lt;td&gt;上→下&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;中间组件&lt;/td&gt;&lt;td&gt;必须接收并转发&lt;/td&gt;&lt;td&gt;完全不知情&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;改变数据&lt;/td&gt;&lt;td&gt;父的 setState&lt;/td&gt;&lt;td&gt;父的 setState（通过 value 传）&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;单向数据流是 React 的铁律，Context 没打破它，只是跳过了中间层的代码传递。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-31&quot;&gt;自测&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Context 改变了 React 的单向数据流吗？&lt;/li&gt;
&lt;li&gt;子组件能通过 Context 反过来改父组件的数据吗？&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-id=&quot;heading-32&quot;&gt;参考答案&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;没有。数据永远是祖先到后代，流向方向没变。&lt;/li&gt;
&lt;li&gt;不能。除非父组件把 setState 也放进 value 传下去——即便如此，&quot;改&quot;的动作发生在子组件，但状态所有权仍然在父组件。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-33&quot;&gt;谁触发谁渲染&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-34&quot;&gt;知识点&lt;/h3&gt;
&lt;p&gt;Provider 的 value 变了，用 useContext 订阅了该 Context 的组件会重新渲染。但即使没订阅，子组件也会因为父组件渲染而跟着渲染。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-35&quot;&gt;关键代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// App.jsx&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [theme, setTheme] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;)
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Page&lt;/span&gt; /&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{()&lt;/span&gt; =&amp;gt;&lt;/span&gt; setTheme(theme === &#39;light&#39; ? &#39;dark&#39; : &#39;light&#39;)}&amp;gt;切换&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  )
}

&lt;span class=&quot;hljs-comment&quot;&gt;// Page.jsx — 用了 useTheme&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Page&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;()
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Child&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
}

&lt;span class=&quot;hljs-comment&quot;&gt;// Child.jsx — 也用了 useTheme&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Child&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;()
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;{theme}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-36&quot;&gt;运行过程&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-markdown&quot; lang=&quot;markdown&quot;&gt;点击切换按钮
  → setTheme 改了 App 的 state
&lt;span class=&quot;hljs-code&quot;&gt;    → App 重新渲染
      → Page 重新渲染（两个原因：父渲染 + useContext 订阅）
        → Child 重新渲染（两个原因：父 Page 渲染 + useContext 订阅）
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Page 和 Child 都有两个渲染原因：父组件带着跑 + useContext 订阅。用 &lt;code&gt;React.memo&lt;/code&gt; 可以拦住父渲染链：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Page&lt;/span&gt; = &lt;span class=&quot;hljs-title class_&quot;&gt;React&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;memo&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;()
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Child&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;memo 检查 Page 的 props 没变 → 拦住父渲染链。但 &lt;code&gt;useTheme()&lt;/code&gt; 订阅了 Context → Provider value 变了 → &lt;strong&gt;memo 拦不住&lt;/strong&gt;，Page 照样渲染。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-37&quot;&gt;为什么这样设计&lt;/h3&gt;
&lt;p&gt;Context 的订阅机制独立于组件树的父子渲染链。memo 只能拦截 props 变化驱动的渲染，不能拦截 Context 订阅驱动的渲染。这是设计上的保证：一个组件声明了依赖某个 Context，不管中间被什么包裹，数据变了它一定能更新。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-38&quot;&gt;易错点&lt;/h3&gt;
&lt;p&gt;子组件渲染的原因不能只看一点。&lt;code&gt;React.memo&lt;/code&gt; 能拦住父渲染链，但拦不住 Context 订阅。反过来，如果组件没订阅 Context 但加了 memo，Provider value 变了也不会渲染。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-39&quot;&gt;自测&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Page 用了 useTheme，App 重渲染时 Page 一定重渲染吗？React.memo 能拦住吗？&lt;/li&gt;
&lt;li&gt;如果 Page 没用 useTheme 但加了 React.memo，Provider value 变了，Page 会渲染吗？&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-id=&quot;heading-40&quot;&gt;参考答案&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;是的。memo 拦不住 Context 订阅触发的渲染，Page 照样渲染。&lt;/li&gt;
&lt;li&gt;不会。Page 没订阅 Context，memo 发现 props 没变就不会渲染。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-41&quot;&gt;自定义 Hook：useMouse&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-42&quot;&gt;知识点&lt;/h3&gt;
&lt;p&gt;自定义 Hook 是把组件里的响应式逻辑（useState、useEffect 等）抽出来，变成一个以 &lt;code&gt;use&lt;/code&gt; 开头的可复用函数。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-43&quot;&gt;真实问题&lt;/h3&gt;
&lt;p&gt;在 App 里写了鼠标跟踪功能后，如果别的页面也要用，不能复制粘贴。把它抽象为自定义 Hook。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-44&quot;&gt;关键代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useState, useEffect } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useMouse&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [x, setX] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;)
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [y, setY] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;)

  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;handleMouseMove&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt; {
      &lt;span class=&quot;hljs-title function_&quot;&gt;setX&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;clientX&lt;/span&gt;)
      &lt;span class=&quot;hljs-title function_&quot;&gt;setY&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;clientY&lt;/span&gt;)
    }

    &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;addEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove)
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
      &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;removeEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove)
    }
  }, [])

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; { x, y }
}

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;default&lt;/span&gt; useMouse
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用方只需一行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot; lang=&quot;bash&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-title&quot;&gt;App&lt;/span&gt;&lt;/span&gt;() {
  const { x, y } = useMouse()
  &lt;span class=&quot;hljs-built_in&quot;&gt;return&lt;/span&gt; &amp;lt;div&amp;gt;{x &amp;amp;&amp;amp; y ? `坐标：&lt;span class=&quot;hljs-variable&quot;&gt;${x}&lt;/span&gt;, &lt;span class=&quot;hljs-variable&quot;&gt;${y}&lt;/span&gt;` : &lt;span class=&quot;hljs-string&quot;&gt;&#39;请移动鼠标&#39;&lt;/span&gt;}&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-45&quot;&gt;为什么以&amp;nbsp;&lt;code&gt;use&lt;/code&gt;&amp;nbsp;开头&lt;/h3&gt;
&lt;p&gt;React 靠函数名识别&quot;这是 Hook&quot;。不以 &lt;code&gt;use&lt;/code&gt; 开头，React 认为它是普通函数，里面的 &lt;code&gt;useState&lt;/code&gt; 直接报错。这是约定，也是运行时规则。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-46&quot;&gt;容易混淆&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Hooks 不能嵌套在普通函数里，必须直接写在 Hook 函数体内。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;错误写法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-scss&quot; lang=&quot;scss&quot;&gt;const useMouse = () =&amp;gt; {
  function &lt;span class=&quot;hljs-built_in&quot;&gt;setup&lt;/span&gt;() {            &lt;span class=&quot;hljs-comment&quot;&gt;// 普通函数&lt;/span&gt;
    const &lt;span class=&quot;hljs-selector-attr&quot;&gt;[x, setX]&lt;/span&gt; = &lt;span class=&quot;hljs-built_in&quot;&gt;useState&lt;/span&gt;()  &lt;span class=&quot;hljs-comment&quot;&gt;// 报错！&lt;/span&gt;
  }
  &lt;span class=&quot;hljs-built_in&quot;&gt;setup&lt;/span&gt;()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因：React 靠 Hooks 的调用顺序来匹配每个 state。第一次渲染时 useState、useState、useEffect 按顺序执行，第二次渲染必须完全一致。如果把 Hook 放在条件或嵌套函数里，顺序可能变化，React 分不清谁是谁。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-47&quot;&gt;使用边界&lt;/h3&gt;
&lt;p&gt;自定义 Hook 和普通函数的区别：&lt;/p&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;普通函数&lt;/th&gt;&lt;th&gt;自定义 Hook&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;纯计算，输入 → 输出&lt;/td&gt;&lt;td&gt;里面可以用 useState、useEffect&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;没有响应式&lt;/td&gt;&lt;td&gt;数据变了消费组件跟着渲染&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;命名无限制&lt;/td&gt;&lt;td&gt;必须以 use 开头&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 data-id=&quot;heading-48&quot;&gt;自测&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;为什么自定义 Hook 必须以 use 开头？&lt;/li&gt;
&lt;li&gt;为什么 Hooks 不能放在条件语句或嵌套函数里？&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-id=&quot;heading-49&quot;&gt;参考答案&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;React 靠函数名识别 Hook，只有&amp;nbsp;&lt;code&gt;use&lt;/code&gt;&amp;nbsp;开头才会被检查 hooks 调用规则。&lt;/li&gt;
&lt;li&gt;React 靠 Hooks 的调用顺序匹配 state。条件或嵌套函数可能改变顺序，React 就无法建立正确的对应关系。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-50&quot;&gt;mousemove 高频渲染&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-51&quot;&gt;知识点&lt;/h3&gt;
&lt;p&gt;mousemove 事件一秒触发几十上百次，每次 &lt;code&gt;setX&lt;/code&gt; / &lt;code&gt;setY&lt;/code&gt; 都触发渲染。如果数据需要显示在 UI 上，渲染是必须的；如果只需要暂存数据，用 &lt;code&gt;useRef&lt;/code&gt; 避免渲染。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-52&quot;&gt;关键代码&lt;/h3&gt;
&lt;p&gt;React 在同一个事件回调里的多个 setState 会合并成一次渲染：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-scss&quot; lang=&quot;scss&quot;&gt;const handleMouseMove = (e) =&amp;gt; {
  &lt;span class=&quot;hljs-built_in&quot;&gt;setX&lt;/span&gt;(e.clientX)   &lt;span class=&quot;hljs-comment&quot;&gt;// ─── 合并为一次渲染&lt;/span&gt;
  &lt;span class=&quot;hljs-built_in&quot;&gt;setY&lt;/span&gt;(e.clientY)   &lt;span class=&quot;hljs-comment&quot;&gt;// ───&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果不需要显示坐标，用 useRef：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useMouse&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; pos = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;x&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;y&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt; })

  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;handleMouseMove&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt; {
      pos.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = { &lt;span class=&quot;hljs-attr&quot;&gt;x&lt;/span&gt;: e.&lt;span class=&quot;hljs-property&quot;&gt;clientX&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;y&lt;/span&gt;: e.&lt;span class=&quot;hljs-property&quot;&gt;clientY&lt;/span&gt; }
      &lt;span class=&quot;hljs-comment&quot;&gt;// 不调 setState，零渲染&lt;/span&gt;
    }
    &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;addEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove)
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;removeEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove)
  }, [])

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; pos
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-53&quot;&gt;为什么这样设计&lt;/h3&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;useState&lt;/th&gt;&lt;th&gt;useRef&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;改了会渲染吗&lt;/td&gt;&lt;td&gt;会&lt;/td&gt;&lt;td&gt;不会&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;适合什么&lt;/td&gt;&lt;td&gt;数据要显示在 UI 上&lt;/td&gt;&lt;td&gt;数据只需暂存、计算&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;例子&lt;/td&gt;&lt;td&gt;列表、表单、开关&lt;/td&gt;&lt;td&gt;DOM 引用、计时器 ID、坐标&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;选择标准：需要显示就用 useState，只需背后计算就用 useRef。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-54&quot;&gt;使用边界&lt;/h3&gt;
&lt;p&gt;高频事件（mousemove、scroll、resize）处理时要特别注意性能。一秒钟 60 次渲染对简单组件不会造成可见卡顿，但如果组件树复杂且没有优化，可能成为瓶颈。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-55&quot;&gt;自测&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;同一事件回调里两次 setState 触发几次渲染？&lt;/li&gt;
&lt;li&gt;什么情况下用 useRef 代替 useState 存鼠标坐标？&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-id=&quot;heading-56&quot;&gt;参考答案&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;一次。React 自动合并同一个事件回调里的多个 setState。&lt;/li&gt;
&lt;li&gt;当坐标不需要显示在 UI 上，只需暂存用于计算时（如传给 WebSocket）。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-57&quot;&gt;useEffect 清理函数&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-58&quot;&gt;知识点&lt;/h3&gt;
&lt;p&gt;useEffect 的回调函数可以返回一个清理函数，在组件卸载前执行。这是 React 对原生 API（事件监听、定时器、Worker 线程）的&quot;借还&quot;机制。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-59&quot;&gt;关键代码&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;addEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove)  &lt;span class=&quot;hljs-comment&quot;&gt;// 借&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;removeEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove)  &lt;span class=&quot;hljs-comment&quot;&gt;// 还&lt;/span&gt;
  }
}, [])
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-60&quot;&gt;运行过程&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs&quot;&gt;组件挂载 → 执行 effect 回调 → addEventListener 绑定
组件运行中 → 正常工作
组件卸载 → 执行清理函数 → removeEventListener 解绑
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-61&quot;&gt;为什么这样设计&lt;/h3&gt;
&lt;p&gt;组件卸载后，它绑定的事件监听器还在内存里。React 管理自己的虚拟 DOM，但不管 DOM 的 addEventListener、setInterval、new Worker() 等原生 API。这些资源必须手动回收，否则每个组件挂载/卸载一次就多一份泄漏，页面越来越慢。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需要清理的资源：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-csharp&quot; lang=&quot;csharp&quot;&gt;addEventListener    → removeEventListener
setInterval         → &lt;span class=&quot;hljs-function&quot;&gt;clearInterval
&lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;Worker&lt;/span&gt;()        → worker.&lt;span class=&quot;hljs-title&quot;&gt;terminate&lt;/span&gt;()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-62&quot;&gt;易错点&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;const&lt;/code&gt; 定义的函数放在 &lt;code&gt;useEffect&lt;/code&gt; 后面，虽然这里能运行（effect 回调在渲染后执行，那时函数已初始化），但这是坏习惯。建议把函数定义放在 &lt;code&gt;useEffect&lt;/code&gt; 前面。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-63&quot;&gt;自测&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;useEffect 的清理函数在什么时候执行？&lt;/li&gt;
&lt;li&gt;不清理事件监听会有什么后果？&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-id=&quot;heading-64&quot;&gt;参考答案&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;组件卸载前执行，用来清理 effect 中创建的资源。&lt;/li&gt;
&lt;li&gt;监听器一直占着内存，组件反复挂载卸载 → 内存泄漏 → 性能下降。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-65&quot;&gt;代码串起来&lt;/h2&gt;
&lt;p&gt;以 App2.jsx 为例，完整链路：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-scss&quot; lang=&quot;scss&quot;&gt;用户点击按钮
  → onClick 触发 &lt;span class=&quot;hljs-built_in&quot;&gt;setTheme&lt;/span&gt;(&#39;dark&#39;)
    → App 重新执行，theme 变为 &#39;dark&#39;
    → &amp;lt;Provider value=&quot;dark&quot;&amp;gt; 广播新值
    → Page 里的 &lt;span class=&quot;hljs-built_in&quot;&gt;useTheme&lt;/span&gt;() 感知到变化，Page 重渲染
    → Child 里的 &lt;span class=&quot;hljs-built_in&quot;&gt;useTheme&lt;/span&gt;() 感知到变化，Child 重渲染
    → 页面显示新主题
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;useMouse 的链路：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-markdown&quot; lang=&quot;markdown&quot;&gt;用户移动鼠标
  → 浏览器触发 mousemove 事件
&lt;span class=&quot;hljs-code&quot;&gt;    → handleMouseMove 执行 → setX + setY
      → React 合并为一次渲染
        → App 重渲染 → 页面显示新坐标
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-id=&quot;heading-66&quot;&gt;最后回顾&lt;/h2&gt;
&lt;p&gt;Context 解决跨层级组件通信，Provider 是局部容器，useContext 是订阅消费。封装自定义 Hook 让逻辑可复用、可校验。高频事件要区分 useState 和 useRef：要显示就渲染，不显示就 ref。useEffect 清理函数是&quot;借了要还&quot;。Hooks 必须直接写在 Hook 函数顶层，不能嵌套。&lt;/p&gt;
&lt;h2 data-id=&quot;heading-67&quot;&gt;自查清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;nbsp;我能说出组件通信的四种关系吗？&lt;/li&gt;
&lt;li&gt;&amp;nbsp;我能写出 useContext 的三步模板吗？&lt;/li&gt;
&lt;li&gt;&amp;nbsp;我知道 createContext 的默认值和 Provider value 的区别吗？&lt;/li&gt;
&lt;li&gt;&amp;nbsp;我知道 Context 没有打破单向数据流吗？&lt;/li&gt;
&lt;li&gt;&amp;nbsp;我能解释 React.memo 对 Context 订阅组件是否有效吗？&lt;/li&gt;
&lt;li&gt;&amp;nbsp;我能写出一个自定义 Hook 吗？&lt;/li&gt;
&lt;li&gt;&amp;nbsp;我知道什么时候用 useState 什么时候用 useRef 吗？&lt;/li&gt;
&lt;li&gt;&amp;nbsp;我知道 useEffect 清理函数什么时候执行、清理什么吗？&lt;/li&gt;
&lt;li&gt;&amp;nbsp;我知道 Hooks 为什么不能嵌套在普通函数里吗？&lt;/li&gt;
&lt;li&gt;&amp;nbsp;我能回答每个章节的自测题吗&lt;/li&gt;
&lt;/ul&gt;</description><link>https://juejin.cn/post/7670712759411884042</link><guid isPermaLink="false">https://juejin.cn/post/7670712759411884042</guid><pubDate>Sat, 08 Aug 2026 02:38:26 GMT</pubDate><author>嘟嘟0717</author><category>前端</category><category>JavaScript</category><category>React.js</category></item><item><title>模型吐到一半的 JSON 为什么不崩、不卡、不抖？流式 Function Call 的三层修复</title><description>&lt;h2 data-id=&quot;heading-0&quot;&gt;模型吐到一半的 JSON 为什么不崩、不卡、不抖？流式 Function Call 的三层修复&lt;/h2&gt;
&lt;p&gt;模型流式输出 Function Call 时，到某一帧可能是 &lt;code&gt;{&quot;tool&quot;:&quot;search&quot;,&quot;args&quot;:{&quot;from&quot;:&quot;北京&quot;,&quot;to&quot;:&quot;上&lt;/code&gt;——这是一段物理上不完整的 JSON，&lt;code&gt;JSON.parse&lt;/code&gt; 一调就抛。但 UI 里&quot;正在生成 to 字段&quot;已经平滑地冒出来了，既没崩也没卡。这份&quot;半截也能渲染、还不抖&quot;的背后，是三层各管一段的修复。&lt;/p&gt;
&lt;p&gt;这三层是数据流上三个独立的环节，彼此正交：字节层负责把切碎的网络包拼回完整文本，语法层负责把半截 JSON 修到能 parse，UI 层负责让字段平滑出现不跳变。下面分开讲，每层各治一个症状。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-1&quot;&gt;先分清：半截 JSON 有两层&quot;不完整&quot;&lt;/h3&gt;
&lt;p&gt;很多人一上来就把&quot;半截 JSON 渲染&quot;当成一个问题，其实它有两层不完整，必须分开治，谁也替不了谁：&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;层&lt;/th&gt;&lt;th&gt;不完整的样子&lt;/th&gt;&lt;th&gt;不治会怎样&lt;/th&gt;&lt;th&gt;治它的&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;字节层（网络）&lt;/td&gt;&lt;td&gt;一个 4 字节 emoji 被切成 2+2；一行 &lt;code&gt;data:&lt;/code&gt; 被切成两半&lt;/td&gt;&lt;td&gt;decode 抛错或乱码&lt;/td&gt;&lt;td&gt;SSEParser&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;语法层（JSON）&lt;/td&gt;&lt;td&gt;&lt;code&gt;{&quot;a&quot;:1,&lt;/code&gt; 少个值；&lt;code&gt;&quot;北&lt;/code&gt; 少半个字符串；&lt;code&gt;tru&lt;/code&gt; 少半截关键字&lt;/td&gt;&lt;td&gt;&lt;code&gt;JSON.parse&lt;/code&gt; 必抛，UI 卡住&lt;/td&gt;&lt;td&gt;json-repair 状态机&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;UI 层（Function Call）&lt;/td&gt;&lt;td&gt;字段一会儿有一会儿没，每次 parse 出新对象&lt;/td&gt;&lt;td&gt;列表跳变、整块重渲&lt;/td&gt;&lt;td&gt;agent-parse + 稳定 key&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;字节层只管&quot;把字节流正确拼成一行 JSON 文本&quot;，不保证语法完整；语法层只管&quot;把半截文本修到能 parse&quot;，不管网络。两层独立。UI 层则在这两者之上，把 parse 出的结构稳定地交给 React。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-2&quot;&gt;字节层：手写 SSEParser，把网络包拼回完整文本&lt;/h3&gt;
&lt;p&gt;模型推流走 SSE（Server-Sent Events）。浏览器的 &lt;code&gt;fetch&lt;/code&gt; 拿到的 &lt;code&gt;ReadableStream&lt;/code&gt; 是按&lt;strong&gt;网络包&lt;/strong&gt;切的，不按行、不按字符切，所以有三个坑必须处理。&lt;/p&gt;
&lt;p&gt;第一个坑，行被切成两半。一个 chunk 可能只到 &lt;code&gt;data: {&quot;a&quot;:1&lt;/code&gt;，下一个 chunk 才补上 &lt;code&gt;, &quot;b&quot;:2}&lt;/code&gt;。解法是用一个 &lt;code&gt;lineBuf&lt;/code&gt; 缓存没拼完的行，下个 chunk 拼上来再按换行切。关键是：一个 SSE 事件什么时候算结束，由&quot;遇到空行&quot;决定，不由&quot;chunk 结束&quot;决定——空行本身也可能跨 chunk。&lt;/p&gt;
&lt;p&gt;第二个坑，UTF-8 多字节字符跨 chunk。一个 4 字节的 emoji 被切成 2+2，如果直接按字节 decode，前 2 字节是非法序列，&lt;code&gt;TextDecoder&lt;/code&gt; 默认抛错。解法是开 &lt;code&gt;TextDecoder&lt;/code&gt; 的流式模式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ts&quot; lang=&quot;ts&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;private&lt;/span&gt; decoder = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;TextDecoder&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;utf-8&#39;&lt;/span&gt;, { &lt;span class=&quot;hljs-attr&quot;&gt;fatal&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; })
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; text = &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;decoder&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;decode&lt;/span&gt;(chunk, { &lt;span class=&quot;hljs-attr&quot;&gt;stream&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;{ stream: true }&lt;/code&gt; 让 decoder 缓冲还没成形的字节——前 2 字节不立即 decode，等下个 chunk 补齐 4 字节再一起出；&lt;code&gt;{ fatal: false }&lt;/code&gt; 保证遇到脏字节不抛错，降级成替换符。中文、emoji 流式不乱码，靠的就是这个。&lt;/p&gt;
&lt;p&gt;第三个坑，换行符不统一。规范要求行结束统一成 LF，但实际可能来 &lt;code&gt;\n&lt;/code&gt;、&lt;code&gt;\r\n&lt;/code&gt;、单独的 &lt;code&gt;\r&lt;/code&gt;。这里手写遍历而不用正则 split：正则在长行上回溯成本高，手写遍历是 O(n) 无回溯，顺便处理 &lt;code&gt;\r\n&lt;/code&gt; 占两字节的情况。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ts&quot; lang=&quot;ts&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;lineBuf&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;length&lt;/span&gt;; i++) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; ch = &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;lineBuf&lt;/span&gt;[i]
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (ch === &lt;span class=&quot;hljs-string&quot;&gt;&#39;\n&#39;&lt;/span&gt; || ch === &lt;span class=&quot;hljs-string&quot;&gt;&#39;\r&#39;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; consumed = ch === &lt;span class=&quot;hljs-string&quot;&gt;&#39;\r&#39;&lt;/span&gt; &amp;amp;&amp;amp; &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;lineBuf&lt;/span&gt;[i + &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;] === &lt;span class=&quot;hljs-string&quot;&gt;&#39;\n&#39;&lt;/span&gt; ? &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt; : &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; line = &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;lineBuf&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;slice&lt;/span&gt;(start, i)
    start = i + consumed
    &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (consumed === &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;) i++        &lt;span class=&quot;hljs-comment&quot;&gt;// 跳过 \r\n 的 \n&lt;/span&gt;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;processLine&lt;/span&gt;(line, out)
  }
}
&lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;lineBuf&lt;/span&gt; = &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;lineBuf&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;slice&lt;/span&gt;(start)   &lt;span class=&quot;hljs-comment&quot;&gt;// 没消费完的尾段留给下个 chunk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;字节层把 SSE 拼成了正确的 JSON 文本行，但这行可能是 &lt;code&gt;{&quot;tool&quot;:&quot;search&quot;,&quot;args&quot;:{&quot;from&quot;:&quot;北京&quot;,&quot;to&quot;:&quot;上&lt;/code&gt;——语法层接手。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-3&quot;&gt;语法层：json-repair 状态机，把半截 JSON 修到能 parse&lt;/h3&gt;
&lt;p&gt;字节层给的文本行，语法上大概率不完整，&lt;code&gt;JSON.parse&lt;/code&gt; 必抛。语法层要做的，是把任意&quot;不完整的 JSON 前缀&quot;修成保证能 parse 不抛的串。这是一个约 450 行的前向扫描状态机，有三个设计目标必须同时满足：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单次前向扫描，O(n)，不回头；&lt;/li&gt;
&lt;li&gt;幂等（idempotent，同一输入不管执行多少次结果都一致）——合法的完整 JSON 输进来，原样返回；&lt;/li&gt;
&lt;li&gt;boundary-insensitive——修复结果只依赖内容，不依赖&quot;在哪里被切断&quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后两条是整个状态机的灵魂，下面会展开。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-4&quot;&gt;三件法宝：容器栈、Expect 状态机、lastTokenEnd 边界&lt;/h4&gt;
&lt;p&gt;状态机一次扫描维护三样东西。&lt;/p&gt;
&lt;p&gt;第一样是&lt;strong&gt;容器栈&lt;/strong&gt;，记录还没闭合的 &lt;code&gt;{&lt;/code&gt; 和 &lt;code&gt;[&lt;/code&gt;。扫到结尾时，按栈的逆序补上对应的 &lt;code&gt;}&lt;/code&gt; 和 &lt;code&gt;]&lt;/code&gt;。这是检测嵌套深度的唯一正道，正则做不到。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ts&quot; lang=&quot;ts&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Frame&lt;/span&gt; = &lt;span class=&quot;hljs-string&quot;&gt;&#39;{&#39;&lt;/span&gt; | &lt;span class=&quot;hljs-string&quot;&gt;&#39;[&#39;&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;stack&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;Frame&lt;/span&gt;[] = []
&lt;span class=&quot;hljs-comment&quot;&gt;// 遇到 { / [ → push；遇到 } / ] → pop&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 扫描结束，按 stack 逆序补闭合&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二样是 &lt;strong&gt;Expect 状态机&lt;/strong&gt;，跟踪&quot;光标处接下来该期望什么&quot;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ts&quot; lang=&quot;ts&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Expect&lt;/span&gt; = &lt;span class=&quot;hljs-string&quot;&gt;&#39;value&#39;&lt;/span&gt; | &lt;span class=&quot;hljs-string&quot;&gt;&#39;key&#39;&lt;/span&gt; | &lt;span class=&quot;hljs-string&quot;&gt;&#39;colon&#39;&lt;/span&gt; | &lt;span class=&quot;hljs-string&quot;&gt;&#39;comma&#39;&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// {     → expect = &#39;key&#39;&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// &quot;key&quot; → expect = &#39;colon&#39;&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// :     → expect = &#39;value&#39;&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// value → expect = &#39;comma&#39;&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// ,     → 对象里 expect=&#39;key&#39;；数组里 expect=&#39;value&#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个状态机决定了结尾残缺时该补什么占位。&lt;/p&gt;
&lt;p&gt;第三样，也是最巧妙的 &lt;strong&gt;lastTokenEnd 边界&lt;/strong&gt;——&quot;最后一个完整词法 token 之后的位置&quot;。一个完整的 token，要么是闭合的字符串、要么是写完的数字或 true/false/null、要么是结构字符（&lt;code&gt;{ [ } ] : ,&lt;/code&gt;）本身。扫描过程中不断更新这个位置，扫描结束时，&lt;code&gt;slice(lastTokenEnd)&lt;/code&gt; 就是&quot;还没写完的尾巴&quot;。所有修复都只针对这个尾巴，不动前面已经完整的部分——这就是 boundary-insensitive 能成立的根基。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-5&quot;&gt;六条修复规则&lt;/h4&gt;
&lt;p&gt;扫描完，按状态对尾巴执行六条规则：&lt;/p&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;规则&lt;/th&gt;&lt;th&gt;触发条件&lt;/th&gt;&lt;th&gt;修复&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;R0&lt;/td&gt;&lt;td&gt;输入 trim 后为空&lt;/td&gt;&lt;td&gt;返回 &lt;code&gt;{}&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;R1&lt;/td&gt;&lt;td&gt;停在字符串内没闭合&lt;/td&gt;&lt;td&gt;补 &lt;code&gt;&quot;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;R2&lt;/td&gt;&lt;td&gt;停在对象的 key 字符串内没闭合&lt;/td&gt;&lt;td&gt;补 &lt;code&gt;&quot;&lt;/code&gt;,再补 &lt;code&gt;:null&lt;/code&gt; 当值占位&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;R3&lt;/td&gt;&lt;td&gt;尾巴是 &lt;code&gt;tru&lt;/code&gt;/&lt;code&gt;fal&lt;/code&gt;/&lt;code&gt;nul&lt;/code&gt; 这种半截关键字&lt;/td&gt;&lt;td&gt;丢弃，在值位置补 &lt;code&gt;null&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;R4&lt;/td&gt;&lt;td&gt;尾巴是悬空逗号&lt;/td&gt;&lt;td&gt;删掉&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;R5&lt;/td&gt;&lt;td&gt;该有值却没有（&lt;code&gt;expect===&#39;value&#39;&lt;/code&gt; 且上一个字符是 &lt;code&gt;:&lt;/code&gt;）&lt;/td&gt;&lt;td&gt;补 &lt;code&gt;null&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;R6&lt;/td&gt;&lt;td&gt;容器栈没闭合&lt;/td&gt;&lt;td&gt;按栈逆序补 &lt;code&gt;}&lt;/code&gt;/&lt;code&gt;]&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h4 data-id=&quot;heading-6&quot;&gt;几个能推演的边界&lt;/h4&gt;
&lt;p&gt;半截关键字。输入 &lt;code&gt;{&quot;value&quot;: tru&lt;/code&gt;，模型正写到 &lt;code&gt;true&lt;/code&gt; 的中途。扫描时遇到 &lt;code&gt;t&lt;/code&gt;，调用消费字面量的函数，发现 &lt;code&gt;tru&lt;/code&gt; 还没到完整的 &lt;code&gt;true&lt;/code&gt;，&lt;code&gt;lastTokenEnd&lt;/code&gt; 不前进，于是尾巴就是 &lt;code&gt;tru&lt;/code&gt;。R3 触发，丢弃尾巴，在值位置补 &lt;code&gt;null&lt;/code&gt;，结果 &lt;code&gt;{&quot;value&quot;: null}&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;半截数字。输入 &lt;code&gt;{&quot;count&quot;: 12.&lt;/code&gt;，模型正写 &lt;code&gt;12.5&lt;/code&gt; 但只到小数点。消费字面量时识别出 &lt;code&gt;12&lt;/code&gt;，遇到 &lt;code&gt;.&lt;/code&gt; 进入小数部分但小数位为 0，返回 &lt;code&gt;12&lt;/code&gt;。尾巴是 &lt;code&gt;12.&lt;/code&gt;，数字修复函数判断 &lt;code&gt;12.&lt;/code&gt; 不完整，修成 &lt;code&gt;12&lt;/code&gt;，结果 &lt;code&gt;{&quot;count&quot;: 12}&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为什么这里宁可丢也不补全成 &lt;code&gt;true&lt;/code&gt; 或 &lt;code&gt;12.5&lt;/code&gt;？因为模型在哪个字符被切断是任意的——可能在 &lt;code&gt;t&lt;/code&gt;、&lt;code&gt;tr&lt;/code&gt;、&lt;code&gt;tru&lt;/code&gt;、&lt;code&gt;true&lt;/code&gt;。如果按&quot;最长关键字前缀&quot;去补全，结果就依赖切断位置，违反 boundary-insensitive。丢掉半截、补个确定的 &lt;code&gt;null&lt;/code&gt;，结果只依赖内容本身。&lt;/p&gt;
&lt;p&gt;截断后的乱码。输入 &lt;code&gt;{&quot;text&quot;: &quot;abc&quot;def&lt;/code&gt;，模型写到一半输出错了。扫描在闭合第一个 &lt;code&gt;&quot;&lt;/code&gt; 后进入非字符串状态，遇到 &lt;code&gt;def&lt;/code&gt; 不识别成任何字面量开头，当 junk 处理。最后 R4、R6 收尾，结果是 &lt;code&gt;{&quot;text&quot;: &quot;abc&quot;}&lt;/code&gt;——后半截 &lt;code&gt;def&lt;/code&gt; 被丢掉。这是设计权衡：截断位置之后的内容不可信，宁可丢也不假补。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-7&quot;&gt;为什么幂等和 boundary-insensitive 这么重要&lt;/h4&gt;
&lt;p&gt;UI 层会频繁拿同一段内容重新修复（避免漏字符、重复），修复函数必须稳定，不能同一段内容这次修成这样、下次修成那样——这是幂等的意义。模型推 token 的边界是任意的，可能一次来 1 个字符、下次来 5 个，修复结果不能因为&quot;切在哪个字符&quot;而不同——这是 boundary-insensitive 的意义。两条性质合起来，保证了 UI 在流式过程中不抖动。&lt;/p&gt;
&lt;p&gt;量级上，一次 repair 就是一次 O(n) 扫描，n 是这段 JSON 的长度。Function Call 通常不到 10KB，单次 repair 耗时在 1 毫秒以内，对每帧 16 毫秒的预算几乎无感。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-8&quot;&gt;UI 层：agent-parse，让字段平滑冒出&lt;/h3&gt;
&lt;p&gt;光&quot;能 parse&quot;还不够。Function Call 有自己的结构（工具名 + 参数），而且不同厂商格式还不一样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-css&quot; lang=&quot;css&quot;&gt;OpenAI:    { &quot;name&quot;: &lt;span class=&quot;hljs-string&quot;&gt;&quot;...&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;arguments&quot;&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&quot;...&quot;&lt;/span&gt; }   // arguments 是字符串
Anthropic: { &quot;tool&quot;: &lt;span class=&quot;hljs-string&quot;&gt;&quot;...&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;input&quot;&lt;/span&gt;: {...} }
通用:      { &quot;name&quot;: &lt;span class=&quot;hljs-string&quot;&gt;&quot;...&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;args&quot;&lt;/span&gt;: {...} }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;UI 层做的事是：先用 repair 把半截修到能 parse，再 &lt;code&gt;JSON.parse&lt;/code&gt;，然后判断状态——修复后和原文本一致就是 &lt;code&gt;complete&lt;/code&gt;（合法 JSON）；不一致就是 &lt;code&gt;parsing&lt;/code&gt;（还在生成）；parse 失败或不是对象就是 &lt;code&gt;invalid&lt;/code&gt;。最后提取工具名（优先 &lt;code&gt;name&lt;/code&gt; 再 &lt;code&gt;tool&lt;/code&gt;）和参数。&lt;/p&gt;
&lt;p&gt;流式场景有个细节：OpenAI 风格里 &lt;code&gt;arguments&lt;/code&gt; 是个字符串，半截时直接 &lt;code&gt;JSON.parse(&quot;北京&quot;)&lt;/code&gt; 会抛。所以只有状态是 &lt;code&gt;complete&lt;/code&gt;、确认是闭合的字符串时才 parse 参数，否则把参数留空，避免半截字符串抛错。&lt;/p&gt;
&lt;p&gt;为什么&quot;to 字段正在写&quot;看起来是平滑冒出来的？关键在参数提取的稳定性。每次 props 更新，重新跑一次参数提取是 O(n)，但已完成的项目对前缀是稳定的——只往尾巴追加，不重写已完成的项。所以 React 列表用字段名做 key 时，已完成项不会因为尾巴新增而重渲染，只有尾部那个还没写完的项在更新：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-css&quot; lang=&quot;css&quot;&gt;t=&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;  &#39;{&quot;tool&quot;:&lt;span class=&quot;hljs-string&quot;&gt;&quot;search&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;&#39;                  → tool: search,  args: {}
t=1  &#39;&lt;/span&gt;{&quot;tool&quot;:&lt;span class=&quot;hljs-string&quot;&gt;&quot;search&quot;&lt;/span&gt;,&lt;span class=&quot;hljs-string&quot;&gt;&quot;args&quot;&lt;/span&gt;:{&quot;&lt;span class=&quot;hljs-selector-tag&quot;&gt;from&lt;/span&gt;&quot;&#39;   → tool: search,  args: { &lt;span class=&quot;hljs-selector-tag&quot;&gt;from&lt;/span&gt;: … }
t=&lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;  &#39;{&quot;tool&quot;:&lt;span class=&quot;hljs-string&quot;&gt;&quot;search&quot;&lt;/span&gt;,&lt;span class=&quot;hljs-string&quot;&gt;&quot;args&quot;&lt;/span&gt;:{&quot;&lt;span class=&quot;hljs-selector-tag&quot;&gt;from&lt;/span&gt;&quot;:&lt;span class=&quot;hljs-string&quot;&gt;&quot;北京&quot;&lt;/span&gt;&lt;span class=&quot;hljs-string&quot;&gt;&#39; → tool: search, args: { from: 北京 }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;from: 北京&lt;/code&gt; 一旦完成，后面再来 token 它也不变，视觉上就是&quot;to 字段在慢慢冒出来&quot;，已完成的 from 稳稳不动。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-9&quot;&gt;三层独立，各治一症&lt;/h3&gt;
&lt;p&gt;回头看，字节层拼文本、语法层修语法、UI 层稳结构，三层各管一段、彼此独立。这种分层是整套方案能稳的关键：任何一层只对自己的问题负责，字节层的坑不会漏到语法层，语法层的半截不会让 UI 层跳变。&lt;/p&gt;
&lt;p&gt;最后还有一道跨三层的保险：属性测试（property-based testing，不写死具体用例，而是声明一条对所有输入都成立的规则，再用随机生成的输入去验证）。这里声明两条规则——任意半截输入修复后都能 parse 不抛；任意切分位置的修复结果只依赖内容。用一个带固定种子的随机数生成器（测出问题能原样复现）跑几百组随机切片来验证：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ts&quot; lang=&quot;ts&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 规则一：任何半截输入修复后都能 parse&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; partial &lt;span class=&quot;hljs-keyword&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;hljs-variable constant_&quot;&gt;CORPUS&lt;/span&gt;) {
  &lt;span class=&quot;hljs-title function_&quot;&gt;expect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;JSON&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;parse&lt;/span&gt;(&lt;span class=&quot;hljs-title function_&quot;&gt;repair&lt;/span&gt;(partial))).&lt;span class=&quot;hljs-property&quot;&gt;not&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;toThrow&lt;/span&gt;()
}

&lt;span class=&quot;hljs-comment&quot;&gt;// 规则二：随机切片的结果和一次性塞入一致&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; seed = &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;; seed &amp;lt;= &lt;span class=&quot;hljs-number&quot;&gt;30&lt;/span&gt;; seed++) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; rng = &lt;span class=&quot;hljs-title function_&quot;&gt;mulberry32&lt;/span&gt;(seed)
  &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; src &lt;span class=&quot;hljs-keyword&quot;&gt;of&lt;/span&gt; &lt;span class=&quot;hljs-variable constant_&quot;&gt;CORPUS&lt;/span&gt;) {
    &lt;span class=&quot;hljs-title function_&quot;&gt;expect&lt;/span&gt;(&lt;span class=&quot;hljs-title function_&quot;&gt;streamed&lt;/span&gt;(src, rng)).&lt;span class=&quot;hljs-title function_&quot;&gt;toEqual&lt;/span&gt;(&lt;span class=&quot;hljs-title function_&quot;&gt;allAtOnce&lt;/span&gt;(src))
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要任意一组切片违反规则，测试就挂。这等于让机器替你验证：不管模型怎么切 token、切在哪个字节，修复结果都稳定可 parse。三层修复加上这道属性测试，半截 JSON 才真正做到边收边渲染、不崩不卡不抖。&lt;/p&gt;</description><link>https://juejin.cn/post/7671128855914430491</link><guid isPermaLink="false">https://juejin.cn/post/7671128855914430491</guid><pubDate>Fri, 07 Aug 2026 22:49:33 GMT</pubDate><author>huabuyu</author><category>前端</category><category>JavaScript</category></item><item><title>治好 AI 长回答的卡、闪、膨胀：渲染开销从 50 万次砍到 1000</title><description>&lt;h2 data-id=&quot;heading-0&quot;&gt;治好 AI 长回答的卡、闪、膨胀：渲染开销从 50 万次砍到 1000&lt;/h2&gt;
&lt;p&gt;AI 对话产品的长回答通常有三个毛病：卡（滚动一卡一卡）、闪（文字反复抖动）、膨胀（文章越长越卡）。最直白的渲染方式——每来一个 token 就把整篇 markdown 重新解析、重新渲染——三种毛病全占：一篇 1000 个 token 的回答累计要渲染 1+2+3+…+1000 ≈ 50 万次块；token 停在 &lt;code&gt;**加粗&lt;/code&gt; 中间时整块会抖；一万字的文章有约 70 个块全挂 DOM。&lt;/p&gt;
&lt;p&gt;这篇把三个症状各拆到根因，各给解法。一个关键的发现是：&quot;卡&quot;其实不是一个问题，而是三个互相独立的问题叠加在一起。全文要讲的五个优化彼此正交，工程上可以任意组合、任意顺序启用，下面按症状归类，只是为了讲清楚每个优化治的是什么。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-1&quot;&gt;先看清三个症状各从哪来&lt;/h3&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;症状&lt;/th&gt;&lt;th&gt;根因&lt;/th&gt;&lt;th&gt;解法&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;卡&lt;/td&gt;&lt;td&gt;每个 token 重新解析整篇，O(n²)&lt;/td&gt;&lt;td&gt;增量切块：只解析尾块&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;卡&lt;/td&gt;&lt;td&gt;内容没变的块 render 也重跑&lt;/td&gt;&lt;td&gt;记忆化：哈希 + memo&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;卡&lt;/td&gt;&lt;td&gt;主线程被解析挤占&lt;/td&gt;&lt;td&gt;Worker：解析搬出主线程&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;闪&lt;/td&gt;&lt;td&gt;半截语法让整块反复拆重建&lt;/td&gt;&lt;td&gt;投机闭合：渲染前临时补全&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;膨胀&lt;/td&gt;&lt;td&gt;长文 DOM 节点太多&lt;/td&gt;&lt;td&gt;视口虚拟化：只留视口&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&quot;卡&quot;占了三行，因为它有三个独立根因——这也是它最难治、最容易被误当成单一问题的原因。下面挨个治。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-2&quot;&gt;症状一：卡&lt;/h3&gt;
&lt;p&gt;卡是读者感受最深的症状，但拆开看，它是三件独立的事叠在一起。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-3&quot;&gt;根因一：重算——每个 token 重新解析整篇&lt;/h4&gt;
&lt;p&gt;最直白的实现里，每个 token 到来都把整篇 markdown 重新解析一遍。算笔账：第 1 个 token 渲染 1 个块，第 2 个渲染前 2 个，第 100 个渲染前 100 个，一篇 100 个块的回答累计 1+2+…+100 = 5050 次解析；1000 个块就是 50 万次。每个 token 的开销和文章长度成正比，整篇累加是 O(n²)。&lt;/p&gt;
&lt;p&gt;解法是增量切块：已经渲染完成的块内容不会再变，没必要每个 token 都解析它们。把已完成的块冻住，只跟踪正在写的最后一段（下文叫尾块），每个 token 只解析尾块。&lt;/p&gt;
&lt;p&gt;为什么&quot;已完成块不会再变&quot;成立？因为 markdown 的块级语法里，一个块一旦被空行、代码围栏或列表标记结束，渲染结果只由自己的内容决定，后面再来新块改变不了它。这是块级语法的特性，也是增量切块能成立的根基——所以增量是&quot;块级&quot;的，不是字符级的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-arduino&quot; lang=&quot;arduino&quot;&gt;┌─────────────────────────────────────────────────────────┐
│ 已完成的块：冻住，永不再变                                │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐                  │
│ │ 块 &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;     │ │ 块 &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;     │ │ 块 &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;     │ ...              │
│ └──────────┘ └──────────┘ └──────────┘                  │
├─────────────────────────────────────────────────────────┤
│ 正在写的尾块：还能增长，每次只扫描这一段                  │
│ ┌──────────────────────────────────────────────┐        │
│ │ &lt;span class=&quot;hljs-string&quot;&gt;&quot;我正在写第 4 段：## 这是标&quot;&lt;/span&gt;                │ 当前    │
│ └──────────────────────────────────────────────┘        │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;切块的关键是判断什么时候一段文字算写完了。markdown 的块边界有三种：空行结束普通段落；代码围栏（fence，三个反引号 &lt;code&gt;```&lt;/code&gt; 标记的代码块边界）开始或结束代码块；列表、标题、引用各有标记。容易踩的坑是代码块内部的空行——代码块里本来就有空行，见到空行就切，一段代码会被腰斩成两块，渲染直接断裂。所以要先判断是不是在围栏里：在围栏里只等闭合，不在才按空行切：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ts&quot; lang=&quot;ts&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; inFence = &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; line &lt;span class=&quot;hljs-keyword&quot;&gt;of&lt;/span&gt; lines) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (!inFence) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (line === &lt;span class=&quot;hljs-string&quot;&gt;&#39;&#39;&lt;/span&gt;) &lt;span class=&quot;hljs-title function_&quot;&gt;finishBlock&lt;/span&gt;()                 &lt;span class=&quot;hljs-comment&quot;&gt;// 围栏外：空行结束当前块&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (line.&lt;span class=&quot;hljs-title function_&quot;&gt;startsWith&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;```&#39;&lt;/span&gt;)) inFence = &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;
  } &lt;span class=&quot;hljs-keyword&quot;&gt;else&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (line.&lt;span class=&quot;hljs-title function_&quot;&gt;startsWith&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;```&#39;&lt;/span&gt;)) inFence = &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;    &lt;span class=&quot;hljs-comment&quot;&gt;// 围栏内：只等闭合&lt;/span&gt;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前后对比：增量做法每个 token 只解析尾块，单次开销只和尾块长度有关，绝大多数 token 只是往尾块追加一个字符，尾块一完成就清空。n 个 token 累加从 O(n²) 降到 O(n)，1000 个 token 的解析次数从 50 万降到 1000——标题里那个数字。&lt;/p&gt;
&lt;p&gt;这一刀砍掉了 O(n²)，但卡还没治完。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-4&quot;&gt;根因二：重渲——内容没变的块 render 也重跑&lt;/h4&gt;
&lt;p&gt;解析降下来之后，重新 profile 会发现一个新浪费：每个 token 到来时，React 仍会把所有块的 render 函数全部重跑一遍，再逐个比对。已完成块的内容明明没变，render 跑完的结果和上次一模一样，这次比对纯属浪费。这在 O(n²) 解析时被掩盖，解析快了就冒头。&lt;/p&gt;
&lt;p&gt;记忆化（memoization，把函数上次的结果缓存起来，同样的输入直接复用）就是治这个。顺带一提，它之所以能生效，靠的是上一节增量切块建立的稳定块对象——已完成块在 token 之间不被重新创建，引用不变，记忆化才有意义。所以这两个优化虽然正交，但配合起来才完整。&lt;/p&gt;
&lt;p&gt;给每个块算一个内容哈希，套上 React.memo，让它在哈希没变时直接跳过 render。为什么用哈希不用整段文本比较？一段 1000 字的块逐字符要比 1000 次，比哈希只比一次。这里用 cyrb53，一种非加密的快速哈希：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ts&quot; lang=&quot;ts&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;cyrb53&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;str: &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt;, seed = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;&lt;/span&gt;): &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; h1 = &lt;span class=&quot;hljs-number&quot;&gt;0xdeadbeef&lt;/span&gt; ^ seed
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; h2 = &lt;span class=&quot;hljs-number&quot;&gt;0x41c6ce57&lt;/span&gt; ^ seed
  &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; ch &lt;span class=&quot;hljs-keyword&quot;&gt;of&lt;/span&gt; str) {
    h1 = &lt;span class=&quot;hljs-title class_&quot;&gt;Math&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;imul&lt;/span&gt;(h1 ^ ch, &lt;span class=&quot;hljs-number&quot;&gt;2654435761&lt;/span&gt;)
    h2 = &lt;span class=&quot;hljs-title class_&quot;&gt;Math&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;imul&lt;/span&gt;(h2 ^ ch, &lt;span class=&quot;hljs-number&quot;&gt;1597334677&lt;/span&gt;)
  }
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (&lt;span class=&quot;hljs-number&quot;&gt;4294967296&lt;/span&gt; * (&lt;span class=&quot;hljs-number&quot;&gt;2097151&lt;/span&gt; &amp;amp; h2) + (h1 &amp;gt;&amp;gt;&amp;gt; &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)).&lt;span class=&quot;hljs-title function_&quot;&gt;toString&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;36&lt;/span&gt;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Block&lt;/span&gt; = &lt;span class=&quot;hljs-title function_&quot;&gt;memo&lt;/span&gt;(
  &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Block&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;{ seg, streaming }: Props&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; text = seg.&lt;span class=&quot;hljs-property&quot;&gt;status&lt;/span&gt; === &lt;span class=&quot;hljs-string&quot;&gt;&#39;active&#39;&lt;/span&gt; ? &lt;span class=&quot;hljs-title function_&quot;&gt;speculativeClose&lt;/span&gt;(seg.&lt;span class=&quot;hljs-property&quot;&gt;text&lt;/span&gt;) : seg.&lt;span class=&quot;hljs-property&quot;&gt;text&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;{renderMarkdown(text)}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  },
  &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;a, b&lt;/span&gt;) =&amp;gt;&lt;/span&gt;
    a.&lt;span class=&quot;hljs-property&quot;&gt;seg&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;id&lt;/span&gt; === b.&lt;span class=&quot;hljs-property&quot;&gt;seg&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;id&lt;/span&gt; &amp;amp;&amp;amp;
    a.&lt;span class=&quot;hljs-property&quot;&gt;seg&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;hash&lt;/span&gt; === b.&lt;span class=&quot;hljs-property&quot;&gt;seg&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;hash&lt;/span&gt; &amp;amp;&amp;amp;
    a.&lt;span class=&quot;hljs-property&quot;&gt;seg&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;status&lt;/span&gt; === b.&lt;span class=&quot;hljs-property&quot;&gt;seg&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;status&lt;/span&gt; &amp;amp;&amp;amp;
    a.&lt;span class=&quot;hljs-property&quot;&gt;streaming&lt;/span&gt; === b.&lt;span class=&quot;hljs-property&quot;&gt;streaming&lt;/span&gt;,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比较函数里要比 id、哈希、状态这三样：id 是块的身份（换了块一定要重渲），哈希是内容指纹（内容变了要重渲），状态区分它是已完成还是正在写（影响要不要补全语法）。三者合起来才完整刻画了&quot;这个块需不需要重新渲染&quot;。&lt;/p&gt;
&lt;p&gt;前后对比：页面上 100 个完成的块再来一个新 token，记忆化前 React 要跑 101 次 render，记忆化后只跑 1 次（那个正在写的块），render 调用从 O(n) 降到 O(1)。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-5&quot;&gt;根因三：主线程被解析挤占&lt;/h4&gt;
&lt;p&gt;前两个根因都治了，CPU 总开销已经很低。但模型吐得快的时候（实测每秒 30 多个 token），偶尔还是会顿一下。原因是主线程同时要解析 markdown、维护状态、渲染 React、响应交互，而浏览器主线程上一个任务超过 50ms 就会让人感觉到掉帧（这种长任务叫 long task，可以用 PerformanceObserver 采集）。解析和渲染抢同一个主线程，解析一忙，渲染和交互就得排队等。&lt;/p&gt;
&lt;p&gt;解法是把切块和算哈希这两件重活搬到 Web Worker。为什么搬这两件、不把渲染也搬过去？因为 DOM 和 React 只能在主线程跑，Worker 没有 DOM，搬不过去；能搬走的只有不碰 DOM 的纯计算。主线程只保留渲染：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ts&quot; lang=&quot;ts&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 主线程&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; seg = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;RemoteSegmenter&lt;/span&gt;(worker)
seg.&lt;span class=&quot;hljs-title function_&quot;&gt;push&lt;/span&gt;(delta)
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; segments = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; seg.&lt;span class=&quot;hljs-title function_&quot;&gt;getSegments&lt;/span&gt;()

&lt;span class=&quot;hljs-comment&quot;&gt;// Worker 线程&lt;/span&gt;
self.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; seg = &lt;span class=&quot;hljs-title function_&quot;&gt;getOrCreate&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;id&lt;/span&gt;)
  seg.&lt;span class=&quot;hljs-title function_&quot;&gt;push&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;delta&lt;/span&gt;)
  self.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;segments&lt;/span&gt;: seg.&lt;span class=&quot;hljs-title function_&quot;&gt;getSegments&lt;/span&gt;() })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前后对比：主线程不再被解析占用，long task 从&quot;解析加渲染&quot;压缩到&quot;只渲染&quot;，滚动和输入响应不再被切块挤占。&lt;/p&gt;
&lt;p&gt;卡的三个根因都治完了。接下来是闪。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-6&quot;&gt;症状二：闪&lt;/h3&gt;
&lt;p&gt;闪来自半截语法。token 停在 &lt;code&gt;**加粗&lt;/code&gt; 中间时，解析器并不知道它将来会闭合，要么把 &lt;code&gt;**&lt;/code&gt; 当字面字符，要么干脆不渲染；等真正的闭合符号到了，这段从普通文本变成 &lt;code&gt;&amp;lt;strong&amp;gt;&lt;/code&gt;，DOM 结构变了，React 把整块拆掉重建，肉眼就是一抖。半截状态反复出现，抖动也就反复出现。&lt;/p&gt;
&lt;p&gt;要消灭它，就得让闭合前后结构一样——办法是渲染前先按最可能的方式把没闭合的符号补上，让半截语法在闭合前就呈现出和闭合后相同的结构。这手法借鉴 CPU 的投机执行（speculative execution，不等条件确定就先按最可能的结果往下做，事后再核对），叫投机闭合（speculative closing）。&lt;/p&gt;
&lt;p&gt;举个能推演的例子。模型吐到 &lt;code&gt;**加&lt;/code&gt; 就停了，这是半截加粗。不补的话，&lt;code&gt;**加&lt;/code&gt; 被当成字面文本；等真符号到了变成 &lt;code&gt;**加粗**&lt;/code&gt;，又渲染成加粗的&quot;加粗&quot;，结构从文本节点变成 &lt;code&gt;&amp;lt;strong&amp;gt;&lt;/code&gt;，React 拆重建，闪一下。投机闭合的做法是渲染前先补成 &lt;code&gt;**加**&lt;/code&gt;，渲染出加粗的&quot;加&quot;，也就是 &lt;code&gt;&amp;lt;strong&amp;gt;加&amp;lt;/strong&amp;gt;&lt;/code&gt;；等真符号到 &lt;code&gt;**加粗**&lt;/code&gt;，渲染出 &lt;code&gt;&amp;lt;strong&amp;gt;加粗&amp;lt;/strong&amp;gt;&lt;/code&gt;。两次结构都是 &lt;code&gt;&amp;lt;strong&amp;gt;&lt;/code&gt;，只是文本从&quot;加&quot;变成&quot;加粗&quot;，React 只更新文本节点，不拆重建，不闪。补的那一个 &lt;code&gt;**&lt;/code&gt; 在过程中完全看不出来。&lt;/p&gt;
&lt;p&gt;实现是一个二十几行的函数，用奇偶计数找出没闭合的符号，而且它是幂等的（idempotent，同一输入不管执行多少次结果都一致）——已经合法闭合的输入原样返回，不会越补越多：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ts&quot; lang=&quot;ts&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;speculativeClose&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;text: &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt;&lt;/span&gt;): &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;hljs-title function_&quot;&gt;fenceCount&lt;/span&gt;(text) % &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt; === &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;) text += &lt;span class=&quot;hljs-string&quot;&gt;&#39;\n```&#39;&lt;/span&gt;   &lt;span class=&quot;hljs-comment&quot;&gt;// 代码块没闭合&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;hljs-title function_&quot;&gt;backtickCount&lt;/span&gt;(text) % &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt; === &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;) text += &lt;span class=&quot;hljs-string&quot;&gt;&#39;`&#39;&lt;/span&gt;    &lt;span class=&quot;hljs-comment&quot;&gt;// 行内代码没闭合&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;hljs-title function_&quot;&gt;boldCount&lt;/span&gt;(text) % &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt; === &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;) text += &lt;span class=&quot;hljs-string&quot;&gt;&#39;**&#39;&lt;/span&gt;       &lt;span class=&quot;hljs-comment&quot;&gt;// 加粗没闭合&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (text.&lt;span class=&quot;hljs-title function_&quot;&gt;endsWith&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;](&#39;&lt;/span&gt;)) text += &lt;span class=&quot;hljs-string&quot;&gt;&#39;)&#39;&lt;/span&gt;              &lt;span class=&quot;hljs-comment&quot;&gt;// 链接没闭合&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;hljs-title function_&quot;&gt;bracketDiff&lt;/span&gt;(text) &amp;gt; &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;) text += &lt;span class=&quot;hljs-string&quot;&gt;&#39;]&#39;&lt;/span&gt;            &lt;span class=&quot;hljs-comment&quot;&gt;// 方括号没闭合&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; text
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有一条必须守住的原则：投机只发生在渲染这一步，保存的内容不能动。组件渲染尾块时投机闭合一下再交给渲染器，但存起来的始终是模型原始输出的文本——用户复制、分享拿到的必须是模型真实说的内容，而不是被偷偷补过符号的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Block&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;{ seg }: { seg: Segment }&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; text = seg.&lt;span class=&quot;hljs-property&quot;&gt;status&lt;/span&gt; === &lt;span class=&quot;hljs-string&quot;&gt;&#39;active&#39;&lt;/span&gt; ? &lt;span class=&quot;hljs-title function_&quot;&gt;speculativeClose&lt;/span&gt;(seg.&lt;span class=&quot;hljs-property&quot;&gt;text&lt;/span&gt;) : seg.&lt;span class=&quot;hljs-property&quot;&gt;text&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;{renderMarkdown(text)}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前后对比：原来每个停在未闭合位置的 token 都会触发一次整块拆重建；投机闭合后，闭合前后 DOM 结构一致，拆重建次数降到 0，只剩下廉价的文本节点更新。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-7&quot;&gt;症状三：膨胀&lt;/h3&gt;
&lt;p&gt;膨胀来自 DOM 数量。一万字的文章大约 70 个块，全部挂在 DOM 上，滚动时浏览器要处理几万个节点，越滚越卡。视口虚拟化（viewport virtualization，只渲染屏幕可见范围加少量缓冲的元素，其余用占位 div 撑开高度）让常驻 DOM 只剩视口附近那十几个，跟文章总长脱钩。&lt;/p&gt;
&lt;p&gt;第一个细节是滚动监听为什么用 IntersectionObserver 而不是 scroll 事件。scroll 每秒能触发几十上百次，回调里为了知道元素位置往往要读 &lt;code&gt;getBoundingClientRect&lt;/code&gt;，这一读会强制浏览器同步重算布局，性能很差。IntersectionObserver 是浏览器原生的异步观察 API，只在元素进出视口时回调。再用一个 WeakMap 把同一个滚动容器里所有块的监听合并成一个，用 Map 分发回调：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ts&quot; lang=&quot;ts&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-variable constant_&quot;&gt;REGISTRY&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;WeakMap&lt;/span&gt;&amp;lt;&lt;span class=&quot;hljs-title class_&quot;&gt;Element&lt;/span&gt;, { &lt;span class=&quot;hljs-attr&quot;&gt;io&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;IntersectionObserver&lt;/span&gt;; &lt;span class=&quot;hljs-attr&quot;&gt;cbs&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;Map&lt;/span&gt;&amp;lt;&lt;span class=&quot;hljs-title class_&quot;&gt;Element&lt;/span&gt;, &lt;span class=&quot;hljs-title class_&quot;&gt;Cb&lt;/span&gt;&amp;gt; }&amp;gt;()

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;observe&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;el: Element, root: Element, cb: Cb&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; entry = &lt;span class=&quot;hljs-variable constant_&quot;&gt;REGISTRY&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;get&lt;/span&gt;(root)
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (!entry) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; io = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;IntersectionObserver&lt;/span&gt;(dispatch, { &lt;span class=&quot;hljs-attr&quot;&gt;rootMargin&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;600px 0px&#39;&lt;/span&gt; })
    entry = { io, &lt;span class=&quot;hljs-attr&quot;&gt;cbs&lt;/span&gt;: &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Map&lt;/span&gt;() }
    &lt;span class=&quot;hljs-variable constant_&quot;&gt;REGISTRY&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;set&lt;/span&gt;(root, entry)
  }
  entry.&lt;span class=&quot;hljs-property&quot;&gt;cbs&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;set&lt;/span&gt;(el, cb)
  entry.&lt;span class=&quot;hljs-property&quot;&gt;io&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;observe&lt;/span&gt;(el)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二个细节是占位高度必须用实测值，不能估。假设一个块实际高 200px，离开视口时估成 150px 占位，它后面的所有块位置都会往上偏 50px；等用户滚回来，块按真实 200px 挂回去，位置又跳下去，整页抖一下。这种抖动用 CLS（累积布局偏移，Cumulative Layout Shift）来量化。正确做法是块离开视口前记下真实高度，占位就用这个值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;observe&lt;/span&gt;(el, root, &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;v&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (!v) heightRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = ref.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;offsetHeight&lt;/span&gt;   &lt;span class=&quot;hljs-comment&quot;&gt;// 离开前记下真实高度&lt;/span&gt;
    &lt;span class=&quot;hljs-title function_&quot;&gt;setVisible&lt;/span&gt;(v)
  })
}, [])

&lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (!visible) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;style&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{{&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;height:&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;heightRef.current&lt;/span&gt; || &lt;span class=&quot;hljs-attr&quot;&gt;32&lt;/span&gt; }} /&amp;gt;&lt;/span&gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一条硬规矩：正在写的尾块绝对不能被虚拟化，否则用户看不到字还在蹦，整个流式效果就断了。只有已完成的块才虚拟化：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; seg.&lt;span class=&quot;hljs-property&quot;&gt;status&lt;/span&gt; === &lt;span class=&quot;hljs-string&quot;&gt;&#39;final&#39;&lt;/span&gt; ? (
  &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;VirtualBlock&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;key&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{seg.id}&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;enabled&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{virtualize}&lt;/span&gt;&amp;gt;&lt;/span&gt;{block}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;VirtualBlock&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
) : (
  block
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前后对比：常驻 DOM 块从 70 降到约 15（视口加缓冲）。滚动时浏览器要处理的节点数量从此和文章长度无关，一万字和十万字滚起来一样顺。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-8&quot;&gt;五个优化是正交的，各治一症&lt;/h3&gt;
&lt;p&gt;回头看，这五个优化没有先后依赖，是五个正交的技术选择：增量切块决定解析多少，记忆化决定渲染多少，投机闭合决定尾块稳不稳，虚拟化决定 DOM 多少，Worker 决定计算在哪个线程。它们各管一层，任意组合都成立——只做其中一两个也能拿到对应那部分收益，全做才把卡、闪、膨胀一次治干净。&lt;/p&gt;
&lt;p&gt;项目里实现了四种可切换的渲染模式做对比：naive（暴力全量重渲）、memoized（只加记忆化）、incremental（完整增量）、worker-incremental（增量搬到 Worker）。切换后实时采集 React 提交次数、单帧最长耗时、帧率、掉帧数、常驻 DOM 块数、主线程 long task：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs&quot;&gt;mode                提交次数   单帧耗时   帧率   掉帧   常驻DOM块
naive               多         高         低     多     ~70
memoized            少一些     中         中     中     ~70
incremental         少         低         高     少     ~70
worker-incremental  最少       很低       稳60   几乎0  ~15（加虚拟化后）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表里的&quot;提交次数、单帧耗时、帧率、掉帧、DOM 块数&quot;都是 PerfStore 真实采集的指标，趋势确定——越往右每一项都更好；但具体数值会随文章长度和机器不同变化，所以只标趋势不写死数字。真正要上线，建议在目标机型和典型文章长度上跑一遍采集面板，拿实测值再定性能预算。&lt;/p&gt;
&lt;p&gt;模型每秒吐 30 多个 token，前端每 30 毫秒就要处理一次新输入，而一帧只有 16 毫秒。只有把每个环节的开销都压到最小——解析从 O(n²) 到 O(n)、render 从 O(n) 到 O(1)、DOM 从全量到视口、主线程从混合到隔离——才能在每个 token 到达后跟得上、不掉帧。&lt;/p&gt;</description><link>https://juejin.cn/post/7671165490337153034</link><guid isPermaLink="false">https://juejin.cn/post/7671165490337153034</guid><pubDate>Fri, 07 Aug 2026 22:22:31 GMT</pubDate><author>huabuyu</author><category>前端</category><category>JavaScript</category></item><item><title>从一段 JS 回调变为异步的代码来了解 JavaScript 异步编程</title><description>&lt;p&gt;在开发对接 AI 大模型（如 OpenAI、DeepSeek）的聊天应用时，流式响应（Server-Sent Events, SSE）是标配。你可能见过这样一段代码：后端数据源源不断地“推”过来，而前端需要像打字机一样优雅地“拉”取显示。&lt;/p&gt;
&lt;p&gt;如何把底层的“被动推模式（回调函数）”无缝转换为现代 JS 最喜欢的“主动拉模式（迭代器）”？&lt;/p&gt;
&lt;p&gt;很多经验不足的程序员写出的转换逻辑要么导致内存泄露，要么直接让 CPU 飙到 100%。今天，我们用几十行纯原生 JS 代码，亲手搭建一个高性能、零漏洞、具备全链路熔断能力的“工业级流式适配器”。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-0&quot;&gt;一、 核心代码示例&lt;/h2&gt;
&lt;p&gt;我们设计了一个极其精巧的流式包装器 &lt;code&gt;EventStreamAdapter&lt;/code&gt;。它的核心职责是：接受一个持续触发回调的异步工作流，将其完美封装为一个可以用 &lt;code&gt;for await...of&lt;/code&gt; 轻松遍历的异步生成器。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-typescript&quot; lang=&quot;typescript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;EventStreamAdapter&lt;/span&gt;&amp;lt;T, R&amp;gt; {
  &lt;span class=&quot;hljs-comment&quot;&gt;/**
   * 将基于回调的推模式，转换为基于异步生成器的拉模式
   * &lt;span class=&quot;hljs-doctag&quot;&gt;@param&lt;/span&gt; task 底层异步任务（例如请求大模型）
   * &lt;span class=&quot;hljs-doctag&quot;&gt;@param&lt;/span&gt; state 初始状态
   */&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;async&lt;/span&gt; *&lt;span class=&quot;hljs-title function_&quot;&gt;stream&lt;/span&gt;(
    &lt;span class=&quot;hljs-attr&quot;&gt;task&lt;/span&gt;: &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;options: { onEvent: (e: T) =&amp;gt; &lt;span class=&quot;hljs-built_in&quot;&gt;void&lt;/span&gt;; signal: AbortSignal }&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;&amp;lt;R&amp;gt;,
  ): &lt;span class=&quot;hljs-title class_&quot;&gt;AsyncGenerator&lt;/span&gt;&amp;lt;T, R, &lt;span class=&quot;hljs-literal&quot;&gt;undefined&lt;/span&gt;&amp;gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;queue&lt;/span&gt;: T[] = [];
    &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; isFinished = &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;taskError&lt;/span&gt;: &lt;span class=&quot;hljs-built_in&quot;&gt;unknown&lt;/span&gt; = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;taskResult&lt;/span&gt;: R | &lt;span class=&quot;hljs-literal&quot;&gt;undefined&lt;/span&gt;;

    &lt;span class=&quot;hljs-comment&quot;&gt;// 🔑 核心防御 1：创建全链路终止控制器&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; controller = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;AbortController&lt;/span&gt;();
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { signal } = controller;

    &lt;span class=&quot;hljs-comment&quot;&gt;// 🔓 叫醒服务的遥控器&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;resolveWait&lt;/span&gt;: &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;void&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {};
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;waitForEvent&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt;
      &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span class=&quot;hljs-built_in&quot;&gt;void&lt;/span&gt;&amp;gt;(&lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;r&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
        resolveWait = r; &lt;span class=&quot;hljs-comment&quot;&gt;// 偷梁换柱：把 Promise 的 resolve 暴露给全局变量&lt;/span&gt;
      });

    &lt;span class=&quot;hljs-comment&quot;&gt;// 底层数据到达时的回调函数&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;onEvent&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;e: T&lt;/span&gt;) =&amp;gt; {
      queue.&lt;span class=&quot;hljs-title function_&quot;&gt;push&lt;/span&gt;(e);
      &lt;span class=&quot;hljs-title function_&quot;&gt;resolveWait&lt;/span&gt;(); &lt;span class=&quot;hljs-comment&quot;&gt;// 拍醒正在冬眠的生成器&lt;/span&gt;
    };

    &lt;span class=&quot;hljs-comment&quot;&gt;// 执行底层任务，并妥善维护状态生命周期&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; taskPromise = &lt;span class=&quot;hljs-title function_&quot;&gt;task&lt;/span&gt;({ onEvent, signal })
      .&lt;span class=&quot;hljs-title function_&quot;&gt;then&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;res&lt;/span&gt;) =&amp;gt;&lt;/span&gt; { taskResult = res; })
      .&lt;span class=&quot;hljs-title function_&quot;&gt;catch&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; { taskError = e; })
      .&lt;span class=&quot;hljs-title function_&quot;&gt;finally&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
        isFinished = &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;;
        &lt;span class=&quot;hljs-title function_&quot;&gt;resolveWait&lt;/span&gt;(); &lt;span class=&quot;hljs-comment&quot;&gt;// 任务结束或崩溃时，同样唤醒生成器收工&lt;/span&gt;
      });

    &lt;span class=&quot;hljs-keyword&quot;&gt;try&lt;/span&gt; {
      &lt;span class=&quot;hljs-keyword&quot;&gt;while&lt;/span&gt; (&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;) {
        &lt;span class=&quot;hljs-comment&quot;&gt;// 🏎️ 优化：内层纯同步循环，一口气榨干当前队列里的所有货&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;while&lt;/span&gt; (queue.&lt;span class=&quot;hljs-property&quot;&gt;length&lt;/span&gt; &amp;gt; &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;) {
          &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; event = queue.&lt;span class=&quot;hljs-title function_&quot;&gt;shift&lt;/span&gt;()!;
          &lt;span class=&quot;hljs-keyword&quot;&gt;yield&lt;/span&gt; event; &lt;span class=&quot;hljs-comment&quot;&gt;// 阻断阀：向外递交数据，等待外部拉取&lt;/span&gt;
        }

        &lt;span class=&quot;hljs-comment&quot;&gt;// 队列搬空了，检查后台任务是否已经彻底收工&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (isFinished) {
          &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (taskError !== &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;throw&lt;/span&gt; taskError; &lt;span class=&quot;hljs-comment&quot;&gt;// 异常精准爆破&lt;/span&gt;
          &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; taskPromise; 
          &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; taskResult!; &lt;span class=&quot;hljs-comment&quot;&gt;// 完美退出&lt;/span&gt;
        }

        &lt;span class=&quot;hljs-comment&quot;&gt;// 🛑 核心防御 2：没有货且任务未结束，执行“无锁阻塞”，让出 CPU 核心控制权&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;waitForEvent&lt;/span&gt;();
      }
    } &lt;span class=&quot;hljs-keyword&quot;&gt;finally&lt;/span&gt; {
      &lt;span class=&quot;hljs-comment&quot;&gt;// 🛡️ 核心防御 3：外部无论因为 break、return 还是报错中断，必定触发此处的安全网&lt;/span&gt;
      controller.&lt;span class=&quot;hljs-title function_&quot;&gt;abort&lt;/span&gt;(); &lt;span class=&quot;hljs-comment&quot;&gt;// 顺着网线过去，把失控的后台任务直接强行掐断！&lt;/span&gt;
      taskPromise.&lt;span class=&quot;hljs-title function_&quot;&gt;catch&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {}); &lt;span class=&quot;hljs-comment&quot;&gt;// 吞掉离开上下文后的垃圾报错，防止进程崩溃&lt;/span&gt;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-1&quot;&gt;二、 代码逐层剖析：它精巧在哪？&lt;/h2&gt;
&lt;p&gt;这段短短几十行的代码，蕴含了极其深厚的资深开发思维。我们挑出其中最惊艳的 4 个设计点进行微观剖析：&lt;/p&gt;
&lt;h2 data-id=&quot;heading-2&quot;&gt;1. 为什么 &lt;code&gt;while(true)&lt;/code&gt; 没有占满 CPU？&lt;/h2&gt;
&lt;p&gt;初学者听到 &lt;code&gt;while(true)&lt;/code&gt; 都会冒冷汗，认为会让电脑卡死。但请注意循环体底部的 &lt;code&gt;await waitForEvent()&lt;/code&gt; 。&lt;br&gt;
当队列为空（&lt;code&gt;queue.length === 0&lt;/code&gt;）时，代码执行到这一行。&lt;code&gt;waitForEvent&lt;/code&gt; 返回一个处于等待状态（&lt;code&gt;pending&lt;/code&gt;）的 Promise。&lt;br&gt;
此时，V8 引擎会瞬间执行“现场截取”，把这个循环的进度、变量打包扔进堆内存的“冷库”里，原地冻结。在没有新事件发生的期间，该循环对 CPU 的开销是纯粹的 0%。&lt;/p&gt;
&lt;h2 data-id=&quot;heading-3&quot;&gt;2. 空函数 &lt;code&gt;resolveWait&lt;/code&gt; 究竟是怎么变成“叫醒钥匙”的？&lt;/h2&gt;
&lt;p&gt;在代码最开始，&lt;code&gt;resolveWait&lt;/code&gt; 只是一个空函数。但每当代码执行到 &lt;code&gt;waitForEvent&lt;/code&gt; 时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-typescript&quot; lang=&quot;typescript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;waitForEvent&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;&amp;lt;&lt;span class=&quot;hljs-built_in&quot;&gt;void&lt;/span&gt;&amp;gt;(&lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;r&lt;/span&gt;) =&amp;gt;&lt;/span&gt; { resolveWait = r; });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它利用了闭包特性，把这个新 Promise 的 &lt;code&gt;resolve&lt;/code&gt; 解锁按钮（参数 &lt;code&gt;r&lt;/code&gt;） 赋给了全局变量 &lt;code&gt;resolveWait&lt;/code&gt;。当后台数据到来触发 &lt;code&gt;onEvent&lt;/code&gt; 时，别人调用 &lt;code&gt;resolveWait()&lt;/code&gt;，调用的其实正是当前卡住代码的那个 Promise 的解锁开关。这种做法避开了繁重的第三方事件库，仅用几行原生 JS 就实现了轻量级的状态同步。&lt;/p&gt;
&lt;h2 data-id=&quot;heading-4&quot;&gt;3. 双层 &lt;code&gt;while&lt;/code&gt; 循环的“微任务极致优化”&lt;/h2&gt;
&lt;p&gt;代码没有傻傻地每处理一个事件就去走一遍复杂的异步 &lt;code&gt;await&lt;/code&gt; 流程。&lt;br&gt;
外层 &lt;code&gt;while&lt;/code&gt; 负责在没货时睡觉，而一旦被唤醒，内层纯同步的 &lt;code&gt;while (queue.length &amp;gt; 0)&lt;/code&gt; 就会瞬间爆发，像割韭菜一样一口气同步抽干当前缓冲区里的所有数据。这种“静如处子，动如脱兔”的设计，具备极高的高并发吞吐量。&lt;/p&gt;
&lt;h2 data-id=&quot;heading-5&quot;&gt;4. &lt;code&gt;finally&lt;/code&gt; 块里的“全链路熔断防火墙”&lt;/h2&gt;
&lt;p&gt;如果外部消费者使用 &lt;code&gt;for await...of&lt;/code&gt; 遍历这个流，中途突然写了个 &lt;code&gt;break;&lt;/code&gt; 跑路了，会发生什么？&lt;br&gt;
如果没有 &lt;code&gt;finally&lt;/code&gt; 块，后台的 &lt;code&gt;task&lt;/code&gt; 网络请求还在源源不断地下数据、吃带宽，成为失控的“僵尸协程”。&lt;br&gt;
在这段代码里，外部一旦 &lt;code&gt;break&lt;/code&gt;，JS 引擎会强制触发内部的 &lt;code&gt;finally&lt;/code&gt;。我们在这个安全网里顺理成章地执行 &lt;code&gt;controller.abort()&lt;/code&gt;，顺着网线直接把后台正在跑的网络请求一并掐死，干净利落。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-6&quot;&gt;三、 体系化技术底座：必须掌握的知识版图&lt;/h2&gt;
&lt;p&gt;要真正看懂并写出这种工业级代码，你需要将以下知识点融会贯通，串联成一个立体的系统。&lt;/p&gt;
&lt;h2 data-id=&quot;heading-7&quot;&gt;1. 计算机硬件与操作系统层（时间管理思维）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;非阻塞 I/O（Non-blocking I/O）：现代操作系统在等待网络响应时，网卡和 CPU 是分离的。当网络数据没到时，CPU 应当被彻底释放去干别的事（比如渲染 UI、处理用户点击）。&lt;/li&gt;
&lt;li&gt;通知（Notification）战胜轮询（Polling）：死循环卡死电脑是因为代码在毫无意义地疯狂主动询问；而高并发架构必须基于“通知机制”，代码进入休眠，由硬件中断和事件循环在数据到达时精准震醒。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-id=&quot;heading-8&quot;&gt;2. JavaScript 语言特性与运行时（V8 引擎层）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;异步生成器（Async Generator）：带有 &lt;code&gt;async *&lt;/code&gt; 的函数天生是拉模式（Pull-based）的。它是被动的，外面不拉（不调用 &lt;code&gt;.next()&lt;/code&gt;），里面就物理封印，一步也不往前走。&lt;/li&gt;
&lt;li&gt;执行栈（Call Stack）与堆内存（Heap）的现场交接：深刻理解 &lt;code&gt;await&lt;/code&gt; 并不是让线程在原地死等，而是将当前的执行上下文（Context）打包成快照存入堆内存，清空当前 CPU 执行栈，让出单线程的控制权。&lt;/li&gt;
&lt;li&gt;闭包与变量逃逸：通过作用域链，将 Promise 内部的私有控制权（&lt;code&gt;resolve&lt;/code&gt; 触发器）“逃逸”暴露到外部，从而实现跨上下文的精准异步控制。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-id=&quot;heading-9&quot;&gt;3. 高级架构与工程落地层（生产防御思维）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;设计模式（适配器模式变体）：在实际业务中，由于大模型、WebSockets 等高频底层数据全都是“推模式（Push）”，这种利用中间缓冲区（&lt;code&gt;queue&lt;/code&gt;）和阻塞器将推转换为拉的架构，是现代前端数据流的核心适配技术。&lt;/li&gt;
&lt;li&gt;未捕获异常防火墙（Unhandled Rejection）：在流式开发中，当迭代器上下文死掉后，后台任务若发生报错，会演变成游离在普通的 &lt;code&gt;try...catch&lt;/code&gt; 之外的全局灾难性报错。必须利用 &lt;code&gt;promise.catch(() =&amp;gt; {})&lt;/code&gt; 挂载全局垃圾桶，筑牢容错防火墙。&lt;/li&gt;
&lt;li&gt;背压与生命周期控制（AbortSignal）：全链路引入 &lt;code&gt;AbortController&lt;/code&gt;，确保生产者（底层任务）与消费者（外层组件）在全生命周期内的生存状态强绑定。外部消亡，内部自杀，实现内存和系统资源零泄露。&lt;/li&gt;
&lt;/ul&gt;</description><link>https://juejin.cn/post/7671122376885534760</link><guid isPermaLink="false">https://juejin.cn/post/7671122376885534760</guid><pubDate>Fri, 07 Aug 2026 16:49:33 GMT</pubDate><author>岳出于山</author><category>前端</category><category>JavaScript</category></item><item><title>JS 运行原理与 Event Loop：从 Web Worker 实战说起</title><description>&lt;h2 data-id=&quot;heading-0&quot;&gt;JS 运行原理与 Event Loop：从 Web Worker 实战说起&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;面试官：「说说 JavaScript 的运行原理？什么是 Event Loop？Web Worker 是怎么回事？」&lt;/p&gt;
&lt;p&gt;我：「好的，让我从一个实际的代码例子说起...」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-id=&quot;heading-1&quot;&gt;目录&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671120692243070982#%E5%89%8D%E8%A8%80&quot; title=&quot;#%E5%89%8D%E8%A8%80&quot;&gt;前言&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671120692243070982#%E4%B8%80javascript-%E5%BC%95%E6%93%8E%E4%BB%A3%E7%A0%81%E6%98%AF%E6%80%8E%E4%B9%88%E8%B7%91%E8%B5%B7%E6%9D%A5%E7%9A%84&quot; title=&quot;#%E4%B8%80javascript-%E5%BC%95%E6%93%8E%E4%BB%A3%E7%A0%81%E6%98%AF%E6%80%8E%E4%B9%88%E8%B7%91%E8%B5%B7%E6%9D%A5%E7%9A%84&quot;&gt;一、JavaScript 引擎：代码是怎么跑起来的？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671120692243070982#%E4%BA%8C%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E4%B8%8E%E8%B0%83%E7%94%A8%E6%A0%88&quot; title=&quot;#%E4%BA%8C%E6%89%A7%E8%A1%8C%E4%B8%8A%E4%B8%8B%E6%96%87%E4%B8%8E%E8%B0%83%E7%94%A8%E6%A0%88&quot;&gt;二、执行上下文与调用栈&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671120692243070982#%E4%B8%89event-loop%E5%8D%95%E7%BA%BF%E7%A8%8B%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%BC%82%E6%AD%A5&quot; title=&quot;#%E4%B8%89event-loop%E5%8D%95%E7%BA%BF%E7%A8%8B%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%BC%82%E6%AD%A5&quot;&gt;三、Event Loop：单线程如何实现异步？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671120692243070982#%E5%9B%9Basyncawait-%E5%9C%A8-event-loop-%E4%B8%AD%E7%9A%84%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F&quot; title=&quot;#%E5%9B%9Basyncawait-%E5%9C%A8-event-loop-%E4%B8%AD%E7%9A%84%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F&quot;&gt;四、async/await 在 Event Loop 中的执行顺序&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671120692243070982#%E4%BA%94web-worker%E7%AA%81%E7%A0%B4%E5%8D%95%E7%BA%BF%E7%A8%8B%E7%9A%84%E9%99%90%E5%88%B6&quot; title=&quot;#%E4%BA%94web-worker%E7%AA%81%E7%A0%B4%E5%8D%95%E7%BA%BF%E7%A8%8B%E7%9A%84%E9%99%90%E5%88%B6&quot;&gt;五、Web Worker：突破单线程的限制&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671120692243070982#%E5%85%AD%E6%B5%8F%E8%A7%88%E5%99%A8-vs-nodejs-%E7%9A%84-event-loop-%E5%B7%AE%E5%BC%82&quot; title=&quot;#%E5%85%AD%E6%B5%8F%E8%A7%88%E5%99%A8-vs-nodejs-%E7%9A%84-event-loop-%E5%B7%AE%E5%BC%82&quot;&gt;六、浏览器 vs Node.js 的 Event Loop 差异&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671120692243070982#%E4%B8%83%E9%9D%A2%E8%AF%95%E9%AB%98%E9%A2%91%E9%97%AE%E9%A2%98%E6%B7%B1%E5%BA%A6%E5%9B%9E%E7%AD%94&quot; title=&quot;#%E4%B8%83%E9%9D%A2%E8%AF%95%E9%AB%98%E9%A2%91%E9%97%AE%E9%A2%98%E6%B7%B1%E5%BA%A6%E5%9B%9E%E7%AD%94&quot;&gt;七、面试高频问题深度回答&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671120692243070982#%E5%85%AB%E5%AE%9E%E9%99%85%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF%E4%B8%8E%E6%80%A7%E8%83%BD%E5%AF%B9%E6%AF%94&quot; title=&quot;#%E5%85%AB%E5%AE%9E%E9%99%85%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF%E4%B8%8E%E6%80%A7%E8%83%BD%E5%AF%B9%E6%AF%94&quot;&gt;八、实际应用场景与性能对比&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671120692243070982#%E6%80%BB%E7%BB%93&quot; title=&quot;#%E6%80%BB%E7%BB%93&quot;&gt;总结&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-2&quot;&gt;前言&lt;/h3&gt;
&lt;p&gt;最近在做一个 React 项目时，遇到了一个典型的性能问题：在主线程执行 50 亿次循环计算时，页面直接卡死了，按钮点击无响应，动画全部冻结。&lt;/p&gt;
&lt;p&gt;这让我不得不深入研究 JavaScript 的运行原理，以及如何用 Web Worker 解决这类问题。&lt;/p&gt;
&lt;p&gt;今天就以这个实际案例为切入点，聊聊 JS 的运行机制、Event Loop，以及 Web Worker 的底层原理。&lt;strong&gt;全文以面试视角展开，每个知识点都配有面试官追问和回答示范。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-3&quot;&gt;一、JavaScript 引擎：代码是怎么跑起来的？&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-4&quot;&gt;1.1 V8 引擎的内部结构&lt;/h4&gt;
&lt;p&gt;JavaScript 代码之所以能运行，核心依赖于 &lt;strong&gt;JavaScript 引擎&lt;/strong&gt;（以 Chrome 的 V8 为例）。&lt;/p&gt;
&lt;p&gt;很多人只知道「V8 把 JS 编译成机器码」，但面试官想听的是&lt;strong&gt;具体流程&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-scss&quot; lang=&quot;scss&quot;&gt;┌──────────────────────────────────────────────────────────────┐
│                         V8 引擎                               │
│                                                              │
│   源代码                                                      │
│     │                                                        │
│     ▼                                                        │
│   ┌──────────┐     ┌──────────────┐     ┌──────────────┐    │
│   │  Parser   │────▶│   Ignition    │────▶│  TurboFan    │    │
│   │  解析器   │     │   解释器      │     │  优化编译器   │    │
│   └──────────┘     └──────────────┘     └──────────────┘    │
│        │                 │                     │             │
│        ▼                 ▼                     ▼             │
│   ┌──────────┐     ┌──────────────┐     ┌──────────────┐    │
│   │   AST     │     │   字节码      │     │  优化机器码   │    │
│   │  抽象语法树│     │  (快速启动)   │     │  (高性能执行) │    │
│   └──────────┘     └──────────────┘     └──────────────┘    │
│                            │                     │           │
│                            │    ┌────────────┐   │           │
│                            └───▶│ Deoptimize │◀──┘           │
│                                 │  去优化     │               │
│                                 └────────────┘               │
│                                                              │
│   ┌──────────────────────────────────────────────────────┐   │
│   │              Memory Heap (内存堆)                      │   │
│   │         存储对象、数组等引用类型数据                     │   │
│   └──────────────────────────────────────────────────────┘   │
│   ┌──────────────────────────────────────────────────────┐   │
│   │              Call Stack (调用栈)                       │   │
│   │         执行上下文的栈结构，后进先出                     │   │
│   └──────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;执行流程（面试回答版）&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Parser（解析器）&lt;/strong&gt;：将源代码解析为 AST（抽象语法树）。这一步会进行词法分析和语法分析，检查语法错误。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ignition（解释器）&lt;/strong&gt;：将 AST 转换为&lt;strong&gt;字节码&lt;/strong&gt;并立即执行。字节码比机器码更紧凑，启动速度快，适合快速执行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;TurboFan（优化编译器）&lt;/strong&gt;：V8 会监控代码执行，找出&lt;strong&gt;热点代码&lt;/strong&gt;（被频繁调用的函数），将其编译为高度优化的&lt;strong&gt;机器码&lt;/strong&gt;，性能接近 C++。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Deoptimization（去优化）&lt;/strong&gt;：如果优化后的代码假设不成立（比如变量类型变了），TurboFan 会把优化后的机器码&lt;strong&gt;回退&lt;/strong&gt;为字节码重新执行。这是很多面试官会追问的点。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;面试追问&lt;/strong&gt;：「为什么要用 Ignition + TurboFan 两层架构，直接编译成机器码不行吗？」&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;回答&lt;/strong&gt;：直接编译成机器码虽然执行快，但&lt;strong&gt;编译时间长、内存占用大&lt;/strong&gt;。V8 的策略是：先用 Ignition 快速启动执行字节码，同时收集类型信息，再用 TurboFan 对热点代码进行优化编译。这样兼顾了&lt;strong&gt;启动速度&lt;/strong&gt;和&lt;strong&gt;峰值性能&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-id=&quot;heading-5&quot;&gt;1.2 执行上下文（Execution Context）&lt;/h4&gt;
&lt;p&gt;在讲调用栈之前，先理解&lt;strong&gt;执行上下文&lt;/strong&gt;——它是代码执行的环境：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 全局执行上下文 —— 代码运行时第一个创建&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; globalVar = &lt;span class=&quot;hljs-string&quot;&gt;&#39;我是全局变量&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;outer&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-comment&quot;&gt;// outer 函数执行上下文 —— 包含 outer 的变量环境&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; outerVar = &lt;span class=&quot;hljs-string&quot;&gt;&#39;我是 outer 的变量&#39;&lt;/span&gt;;

  &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;inner&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-comment&quot;&gt;// inner 函数执行上下文 —— 包含 inner 的变量环境&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// 同时通过作用域链可以访问 outerVar 和 globalVar&lt;/span&gt;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(outerVar);  &lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 可以访问&lt;/span&gt;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(globalVar); &lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 可以访问&lt;/span&gt;
  }

  &lt;span class=&quot;hljs-title function_&quot;&gt;inner&lt;/span&gt;();
}

&lt;span class=&quot;hljs-title function_&quot;&gt;outer&lt;/span&gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个执行上下文包含三个核心组件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-kotlin&quot; lang=&quot;kotlin&quot;&gt;┌─────────────────────────────────────────┐
│         执行上下文 (Execution Context)    │
│                                         │
│  ┌───────────────────────────────────┐  │
│  │  变量环境 (Variable Environment)   │  │
│  │  - &lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; 声明的变量                  │  │
│  │  - 函数声明                        │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │  词法环境 (Lexical Environment)    │  │
│  │  - let / &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; 声明的变量          │  │
│  │  - 块级作用域                      │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │  &lt;span class=&quot;hljs-keyword&quot;&gt;this&lt;/span&gt; 绑定                        │  │
│  │  - 指向当前执行上下文的 &lt;span class=&quot;hljs-keyword&quot;&gt;this&lt;/span&gt; 值    │  │
│  └───────────────────────────────────┘  │
│  ┌───────────────────────────────────┐  │
│  │  作用域链 (Scope Chain)            │  │
│  │  - 当前变量环境 + 所有父级环境     │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-6&quot;&gt;二、执行上下文与调用栈&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-7&quot;&gt;2.1 调用栈（Call Stack）—— 代码执行的核心&lt;/h4&gt;
&lt;p&gt;调用栈是理解 JS 运行原理的关键。它是一个 &lt;strong&gt;后进先出（LIFO）&lt;/strong&gt; 的数据结构，用于记录函数的执行顺序。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;multiply&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;a, b&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; a * b;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;square&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;n&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;multiply&lt;/span&gt;(n, n);
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;printSquare&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;n&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; result = &lt;span class=&quot;hljs-title function_&quot;&gt;square&lt;/span&gt;(n);
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(result);
}

&lt;span class=&quot;hljs-title function_&quot;&gt;printSquare&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用栈的变化过程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-css&quot; lang=&quot;css&quot;&gt;步骤 &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;: 调用 &lt;span class=&quot;hljs-built_in&quot;&gt;printSquare&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;)
┌──────────────────────┐
│   &lt;span class=&quot;hljs-built_in&quot;&gt;printSquare&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;)     │  ← 新的执行上下文入栈
├──────────────────────┤
│   全局执行上下文      │
└──────────────────────┘

步骤 &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;: printSquare 内部调用 &lt;span class=&quot;hljs-built_in&quot;&gt;square&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;)
┌──────────────────────┐
│   &lt;span class=&quot;hljs-built_in&quot;&gt;square&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;)          │
├──────────────────────┤
│   &lt;span class=&quot;hljs-built_in&quot;&gt;printSquare&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;)     │
├──────────────────────┤
│   全局执行上下文      │
└──────────────────────┘

步骤 &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;: square 内部调用 &lt;span class=&quot;hljs-built_in&quot;&gt;multiply&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;)
┌──────────────────────┐
│   &lt;span class=&quot;hljs-built_in&quot;&gt;multiply&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;)     │
├──────────────────────┤
│   &lt;span class=&quot;hljs-built_in&quot;&gt;square&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;)          │
├──────────────────────┤
│   &lt;span class=&quot;hljs-built_in&quot;&gt;printSquare&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;)     │
├──────────────────────┤
│   全局执行上下文      │
└──────────────────────┘

步骤 &lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;: multiply 返回 &lt;span class=&quot;hljs-number&quot;&gt;16&lt;/span&gt;，弹出栈
┌──────────────────────┐
│   &lt;span class=&quot;hljs-built_in&quot;&gt;square&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;)          │
├──────────────────────┤
│   &lt;span class=&quot;hljs-built_in&quot;&gt;printSquare&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;)     │
├──────────────────────┤
│   全局执行上下文      │
└──────────────────────┘

步骤 &lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;-&lt;span class=&quot;hljs-number&quot;&gt;7&lt;/span&gt;: 依次弹出 square → printSquare → 全局上下文
           调用栈清空
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-8&quot;&gt;2.2 栈溢出（Stack Overflow）&lt;/h4&gt;
&lt;p&gt;调用栈有大小限制。递归没有终止条件时，会触发栈溢出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 经典的栈溢出示例&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;recurse&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-title function_&quot;&gt;recurse&lt;/span&gt;();  &lt;span class=&quot;hljs-comment&quot;&gt;// 无限递归，调用栈不断增长&lt;/span&gt;
}

&lt;span class=&quot;hljs-title function_&quot;&gt;recurse&lt;/span&gt;();  &lt;span class=&quot;hljs-comment&quot;&gt;// ❌ Uncaught RangeError: Maximum call stack size exceeded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;面试追问&lt;/strong&gt;：「调用栈一般能放多少层？」&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;回答&lt;/strong&gt;：取决于引擎和栈帧大小。V8 中大约 &lt;strong&gt;10000-15000 层&lt;/strong&gt;，可以通过以下代码测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; count = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;test&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) { count++; &lt;span class=&quot;hljs-title function_&quot;&gt;test&lt;/span&gt;(); }
&lt;span class=&quot;hljs-keyword&quot;&gt;try&lt;/span&gt; { &lt;span class=&quot;hljs-title function_&quot;&gt;test&lt;/span&gt;(); } &lt;span class=&quot;hljs-keyword&quot;&gt;catch&lt;/span&gt;(e) { &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(count); } &lt;span class=&quot;hljs-comment&quot;&gt;// ~12500&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;h4 data-id=&quot;heading-9&quot;&gt;2.3 为什么 JavaScript 是单线程的？&lt;/h4&gt;
&lt;p&gt;这是面试中的经典问题。答案很简单：&lt;strong&gt;避免 DOM 操作的复杂性&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果 JS 是多线程的，假设两个线程同时操作同一个 DOM 元素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程 A：删除这个 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;线程 B：修改这个 &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; 的内容&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就产生了&lt;strong&gt;竞态条件（Race Condition）&lt;/strong&gt;，浏览器不知道该听谁的。所以 JavaScript 从诞生之初就被设计为单线程语言。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但是&lt;/strong&gt;，单线程意味着一次只能做一件事。如果遇到耗时操作（比如我们的 50 亿次循环），整个页面就会被阻塞：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 这段代码会阻塞页面 5-10 秒&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;hljs-number&quot;&gt;5000000000&lt;/span&gt;; i++) {
  sum += num * i;
}
&lt;span class=&quot;hljs-comment&quot;&gt;// 在此期间，页面无法响应任何用户交互&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 浏览器甚至会弹出&quot;页面无响应&quot;警告&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是为什么我们需要 Event Loop 和 Web Worker。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-10&quot;&gt;三、Event Loop：单线程如何实现异步？&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-11&quot;&gt;3.1 为什么需要 Event Loop？&lt;/h4&gt;
&lt;p&gt;既然 JS 是单线程的，那它是怎么处理网络请求、定时器、用户点击这些异步操作的呢？&lt;/p&gt;
&lt;p&gt;答案就是 &lt;strong&gt;Event Loop（事件循环）&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;┌──────────────────────────────────────────────────────────────┐
│                    &lt;span class=&quot;hljs-title class_&quot;&gt;JavaScript&lt;/span&gt; 运行时                          │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │                  &lt;span class=&quot;hljs-title class_&quot;&gt;Call&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Stack&lt;/span&gt; (调用栈)                    │  │
│  │              同步代码在这里执行                          │  │
│  └────────────────────────────────────────────────────────┘  │
│                            │                                 │
│                            ▼                                 │
│  ┌────────────────────────────────────────────────────────┐  │
│  │              &lt;span class=&quot;hljs-title class_&quot;&gt;Web&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;APIs&lt;/span&gt; (浏览器提供的异步能力)             │  │
│  │   &lt;span class=&quot;hljs-built_in&quot;&gt;setTimeout&lt;/span&gt; / fetch / &lt;span class=&quot;hljs-variable constant_&quot;&gt;DOM&lt;/span&gt;事件 / requestAnimationFrame │  │
│  └────────────────────────────────────────────────────────┘  │
│                            │                                 │
│                            ▼                                 │
│  ┌────────────────────────────────────────────────────────┐  │
│  │              &lt;span class=&quot;hljs-title class_&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Queue&lt;/span&gt; (任务队列)                      │  │
│  │                                                        │  │
│  │  ┌──────────────────────────────────────────────────┐  │  │
│  │  │       宏任务队列 (&lt;span class=&quot;hljs-title class_&quot;&gt;Macro&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Queue&lt;/span&gt;)               │  │  │
│  │  │  &lt;span class=&quot;hljs-built_in&quot;&gt;setTimeout&lt;/span&gt; / &lt;span class=&quot;hljs-built_in&quot;&gt;setInterval&lt;/span&gt; / I/O / &lt;span class=&quot;hljs-variable constant_&quot;&gt;UI&lt;/span&gt;渲染          │  │  │
│  │  │  setImmediate (&lt;span class=&quot;hljs-title class_&quot;&gt;Node&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;js&lt;/span&gt;) / &lt;span class=&quot;hljs-title class_&quot;&gt;MessageChannel&lt;/span&gt;          │  │  │
│  │  └──────────────────────────────────────────────────┘  │  │
│  │  ┌──────────────────────────────────────────────────┐  │  │
│  │  │       微任务队列 (&lt;span class=&quot;hljs-title class_&quot;&gt;Micro&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Queue&lt;/span&gt;)               │  │  │
│  │  │  &lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;then&lt;/span&gt; / &lt;span class=&quot;hljs-keyword&quot;&gt;catch&lt;/span&gt; / &lt;span class=&quot;hljs-keyword&quot;&gt;finally&lt;/span&gt;                   │  │  │
│  │  │  &lt;span class=&quot;hljs-title class_&quot;&gt;MutationObserver&lt;/span&gt; / queueMicrotask                │  │  │
│  │  │  &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; 之后的代码                                 │  │  │
│  │  └──────────────────────────────────────────────────┘  │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  ┌─────┐  渲染时机（每帧最多执行一次）                   │  │
│  │  │ &lt;span class=&quot;hljs-variable constant_&quot;&gt;RAF&lt;/span&gt; │  requestAnimationFrame 回调                   │  │
│  │  └─────┘  在微任务之后、宏任务之前执行                   │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘
                            │
                            │  ◀──── 循环执行 ────▶
                            ▼
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-12&quot;&gt;3.2 Event Loop 的完整执行顺序&lt;/h4&gt;
&lt;p&gt;这是面试中的高频考点。很多人的回答不够完整，只说了&quot;同步 → 微任务 → 宏任务&quot;，但实际上还有一个关键环节——&lt;strong&gt;渲染&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;完整的 Event Loop 一轮循环&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-vbnet&quot; lang=&quot;vbnet&quot;&gt;┌─────────────────────────────────────────────────────────┐
│                   &lt;span class=&quot;hljs-keyword&quot;&gt;Event&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;Loop&lt;/span&gt; 一轮循环                     │
│                                                         │
│  ① 从宏任务队列取出【一个】宏任务执行                     │
│                    │                                    │
│                    ▼                                    │
│  ② 执行过程中产生的同步代码直接执行                       │
│                    │                                    │
│                    ▼                                    │
│  ③ 清空微任务队列中的【所有】微任务                       │
│     （包括执行微任务时新产生的微任务）                     │
│                    │                                    │
│                    ▼                                    │
│  ④ 执行 requestAnimationFrame 回调（如果有）             │
│                    │                                    │
│                    ▼                                    │
│  ⑤ 浏览器判断是否需要渲染                                │
│     如果需要 → 执行渲染（重排 + 重绘）                    │
│                    │                                    │
│                    ▼                                    │
│  ⑥ 回到步骤 ①，开始下一轮循环                            │
│                                                         │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来看一道经典面试题：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;1. 同步代码 - 开始&#39;&lt;/span&gt;);

&lt;span class=&quot;hljs-built_in&quot;&gt;setTimeout&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;2. 宏任务 - setTimeout&#39;&lt;/span&gt;);
}, &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;);

&lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;resolve&lt;/span&gt;().&lt;span class=&quot;hljs-title function_&quot;&gt;then&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;3. 微任务 - Promise.then 1&#39;&lt;/span&gt;);
}).&lt;span class=&quot;hljs-title function_&quot;&gt;then&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;4. 微任务 - Promise.then 2&#39;&lt;/span&gt;);
});

&lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;5. 同步代码 - 结束&#39;&lt;/span&gt;);

&lt;span class=&quot;hljs-comment&quot;&gt;// 输出顺序：&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 1. 同步代码 - 开始&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 5. 同步代码 - 结束&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 3. 微任务 - Promise.then 1&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 4. 微任务 - Promise.then 2&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 2. 宏任务 - setTimeout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;执行过程分析&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;第一轮 &lt;span class=&quot;hljs-title class_&quot;&gt;Event&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Loop&lt;/span&gt;：
  宏任务（script 标签整体执行）:
    ├── &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;1. 同步代码 - 开始&#39;&lt;/span&gt;)  → 输出 &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;
    ├── &lt;span class=&quot;hljs-built_in&quot;&gt;setTimeout&lt;/span&gt; → 回调注册到宏任务队列
    ├── &lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;resolve&lt;/span&gt;().&lt;span class=&quot;hljs-title function_&quot;&gt;then&lt;/span&gt;() → 回调注册到微任务队列
    └── &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;5. 同步代码 - 结束&#39;&lt;/span&gt;)  → 输出 &lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;

  微任务（清空所有微任务）:
    ├── 执行 &lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;  → 输出 &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;
    │   └── .&lt;span class=&quot;hljs-title function_&quot;&gt;then&lt;/span&gt;() 产生新微任务 → &lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;
    └── 执行 &lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;then&lt;/span&gt; &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;  → 输出 &lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;
        （微任务队列清空）

第二轮 &lt;span class=&quot;hljs-title class_&quot;&gt;Event&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Loop&lt;/span&gt;：
  宏任务:
    └── 执行 &lt;span class=&quot;hljs-built_in&quot;&gt;setTimeout&lt;/span&gt; 回调  → 输出 &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;

  微任务: （队列为空，跳过）
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-13&quot;&gt;3.3 宏任务 vs 微任务&lt;/h4&gt;























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类型&lt;/th&gt;&lt;th&gt;常见来源&lt;/th&gt;&lt;th&gt;执行时机&lt;/th&gt;&lt;th&gt;每次取几个&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;宏任务&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;setTimeout、setInterval、I/O、UI渲染、script标签&lt;/td&gt;&lt;td&gt;Event Loop 每轮开始&lt;/td&gt;&lt;td&gt;&lt;strong&gt;一个&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;微任务&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Promise.then/catch/finally、MutationObserver、queueMicrotask、await 之后&lt;/td&gt;&lt;td&gt;每个宏任务执行完后&lt;/td&gt;&lt;td&gt;&lt;strong&gt;全部&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键区别&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;微任务优先级高于宏任务&lt;/li&gt;
&lt;li&gt;每个宏任务执行完后，会&lt;strong&gt;清空所有微任务&lt;/strong&gt;（包括微任务中新增的微任务）&lt;/li&gt;
&lt;li&gt;宏任务每次只取&lt;strong&gt;一个&lt;/strong&gt;执行&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;面试追问&lt;/strong&gt;：「Promise.then 的回调是什么时候注册的？」&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;回答&lt;/strong&gt;：当 Promise 状态变为 resolved 时，&lt;code&gt;.then()&lt;/code&gt; 的回调会被放入&lt;strong&gt;微任务队列&lt;/strong&gt;。如果 Promise 在 &lt;code&gt;.then()&lt;/code&gt; 调用时已经 resolved，回调会立即入队。这就是为什么 &lt;code&gt;Promise.resolve().then(fn)&lt;/code&gt; 的回调会在当前宏任务结束后立即执行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-14&quot;&gt;四、async/await 在 Event Loop 中的执行顺序&lt;/h3&gt;
&lt;p&gt;这是 2026 年面试的&lt;strong&gt;必考题&lt;/strong&gt;。很多人背了&quot;同步 → 微任务 → 宏任务&quot;，但遇到 async/await 就懵了。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-15&quot;&gt;4.1 核心结论&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;await&lt;/code&gt; 后面的代码等价于放在 &lt;code&gt;.then()&lt;/code&gt; 回调中&lt;/strong&gt;，属于微任务。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-16&quot;&gt;4.2 经典面试题&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;async1&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;1. async1 start&#39;&lt;/span&gt;);
  &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;async2&lt;/span&gt;();
  &lt;span class=&quot;hljs-comment&quot;&gt;// 下面这行等价于 async2().then(() =&amp;gt; { ... })&lt;/span&gt;
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;2. async1 end&#39;&lt;/span&gt;);
}

&lt;span class=&quot;hljs-keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;async2&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;3. async2&#39;&lt;/span&gt;);
}

&lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;4. script start&#39;&lt;/span&gt;);

&lt;span class=&quot;hljs-built_in&quot;&gt;setTimeout&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;5. setTimeout&#39;&lt;/span&gt;);
}, &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;);

&lt;span class=&quot;hljs-title function_&quot;&gt;async1&lt;/span&gt;();

&lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;resolve&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;6. Promise executor&#39;&lt;/span&gt;);
  &lt;span class=&quot;hljs-title function_&quot;&gt;resolve&lt;/span&gt;();
}).&lt;span class=&quot;hljs-title function_&quot;&gt;then&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;7. Promise then&#39;&lt;/span&gt;);
});

&lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;8. script end&#39;&lt;/span&gt;);

&lt;span class=&quot;hljs-comment&quot;&gt;// 输出顺序：&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 4. script start&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 1. async1 start&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 3. async2&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 6. Promise executor&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 8. script end&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 2. async1 end&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 7. Promise then&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 5. setTimeout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-17&quot;&gt;4.3 逐步分析&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;第一轮：执行同步代码（script 宏任务）
  ├── &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;4. script start&#39;&lt;/span&gt;)           → 输出 &lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;
  ├── &lt;span class=&quot;hljs-built_in&quot;&gt;setTimeout&lt;/span&gt; → 注册到宏任务队列
  ├── 调用 &lt;span class=&quot;hljs-title function_&quot;&gt;async1&lt;/span&gt;()
  │   ├── &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;1. async1 start&#39;&lt;/span&gt;)       → 输出 &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;
  │   ├── &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;async2&lt;/span&gt;()
  │   │   ├── &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;3. async2&#39;&lt;/span&gt;)         → 输出 &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;
  │   │   └── &lt;span class=&quot;hljs-title function_&quot;&gt;async2&lt;/span&gt;() 返回 resolved &lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;
  │   └── &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; 后面的代码 → 注册到微任务队列 ⭐
  ├── &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;(executor)
  │   ├── &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;6. Promise executor&#39;&lt;/span&gt;)   → 输出 &lt;span class=&quot;hljs-number&quot;&gt;6&lt;/span&gt;
  │   └── &lt;span class=&quot;hljs-title function_&quot;&gt;resolve&lt;/span&gt;() → .&lt;span class=&quot;hljs-property&quot;&gt;then&lt;/span&gt; 回调注册到微任务队列
  └── &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;8. script end&#39;&lt;/span&gt;)             → 输出 &lt;span class=&quot;hljs-number&quot;&gt;8&lt;/span&gt;

清空微任务队列：
  ├── 执行 async1 的 &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; 后续                 → 输出 &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;
  └── 执行 &lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;then&lt;/span&gt;                        → 输出 &lt;span class=&quot;hljs-number&quot;&gt;7&lt;/span&gt;

第二轮：执行宏任务
  └── &lt;span class=&quot;hljs-built_in&quot;&gt;setTimeout&lt;/span&gt; 回调                          → 输出 &lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;面试追问&lt;/strong&gt;：「&lt;code&gt;await async2()&lt;/code&gt; 和 &lt;code&gt;await Promise.resolve()&lt;/code&gt; 有什么区别？」&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;回答&lt;/strong&gt;：如果 await 后面是一个 &lt;strong&gt;非 Promise 值&lt;/strong&gt;，引擎会自动用 &lt;code&gt;Promise.resolve()&lt;/code&gt; 包装它。但如果是已经 resolved 的 Promise，&lt;code&gt;await&lt;/code&gt; 仍然会让出执行权，后面的代码会被放入微任务队列。也就是说，&lt;strong&gt;&lt;code&gt;await&lt;/code&gt; 一定会让出执行权&lt;/strong&gt;，不管后面的值是什么状态。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-18&quot;&gt;五、Web Worker：突破单线程的限制&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-19&quot;&gt;5.1 从实际问题说起&lt;/h4&gt;
&lt;p&gt;回到我们的项目。在 &lt;code&gt;ref-worker-demo&lt;/code&gt; 中，有一个计算 50 亿次循环的需求：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// worker.js - 计算逻辑&lt;/span&gt;
self.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { num } = e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; sum = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;hljs-number&quot;&gt;5000000000&lt;/span&gt;; i++) {
        sum += num * i;  &lt;span class=&quot;hljs-comment&quot;&gt;// ⚠️ 注意：超过 Number.MAX_SAFE_INTEGER 会丢失精度&lt;/span&gt;
    }
    self.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;result&lt;/span&gt;: sum });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;精度问题说明&lt;/strong&gt;：当 &lt;code&gt;i&lt;/code&gt; 超过 &lt;code&gt;Number.MAX_SAFE_INTEGER&lt;/code&gt;（2^53 - 1）时，&lt;code&gt;num * i&lt;/code&gt; 的结果会丢失精度。在实际项目中，如果需要精确计算大数，应该使用 &lt;code&gt;BigInt&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 使用 BigInt 解决精度问题&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; sum = &lt;span class=&quot;hljs-number&quot;&gt;0n&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0n&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;hljs-number&quot;&gt;5000000000n&lt;/span&gt;; i++) {
    sum += &lt;span class=&quot;hljs-title class_&quot;&gt;BigInt&lt;/span&gt;(num) * i;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果这段代码直接在主线程执行，会发生什么？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 在主线程执行 - 这会导致页面卡死！&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;startHeavyCalc&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-title function_&quot;&gt;setLoading&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;);
  &lt;span class=&quot;hljs-comment&quot;&gt;// 这 50 亿次循环会阻塞主线程 5-10 秒&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// 在此期间：&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// - 页面无法响应点击&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// - 动画会停止&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// - Event Loop 被阻塞，所有宏任务和微任务都无法执行&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// - 浏览器甚至会弹出&quot;页面无响应&quot;警告&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;hljs-number&quot;&gt;5000000000&lt;/span&gt;; i++) {
    sum += num * i;
  }
  &lt;span class=&quot;hljs-title function_&quot;&gt;setResult&lt;/span&gt;(sum);
  &lt;span class=&quot;hljs-title function_&quot;&gt;setLoading&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;);
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-20&quot;&gt;5.2 Web Worker 的架构&lt;/h4&gt;
&lt;p&gt;Web Worker 允许我们在主线程之外创建一个独立的后台线程，专门处理耗时计算：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-scss&quot; lang=&quot;scss&quot;&gt;┌──────────────────────────────────────────────────────────────┐
│                     主线程 (Main Thread)                      │
│                                                              │
│   Call Stack         Event Loop         Web APIs             │
│   ┌──────────┐      ┌──────────┐      ┌──────────────┐      │
│   │ 渲染组件  │      │ 宏任务队列│      │ setTimeout   │      │
│   │ DOM 操作  │      │ 微任务队列│      │ fetch        │      │
│   │ React 更新│      └──────────┘      │ DOM 事件     │      │
│   └──────────┘                        └──────────────┘      │
│         │                                                      │
│         │  &lt;span class=&quot;hljs-built_in&quot;&gt;postMessage&lt;/span&gt;({ num: &lt;span class=&quot;hljs-number&quot;&gt;88&lt;/span&gt; })                           │
│         │  ─────────────────────────────────────────▶         │
│         │                   (结构化克隆序列化)                  │
│         │                                                      │
└─────────┼──────────────────────────────────────────────────────┘
          │
          ▼
┌──────────────────────────────────────────────────────────────┐
│                    Worker 线程 (独立线程)                      │
│                                                              │
│   Call Stack         Event Loop         独立的堆内存          │
│   ┌──────────┐      ┌──────────┐      ┌──────────────┐      │
│   │ &lt;span class=&quot;hljs-number&quot;&gt;50&lt;/span&gt;亿次循环│      │ 宏任务队列│      │ 计算过程中的  │      │
│   │ 在这里执行│      │ 微任务队列│      │ 临时变量     │      │
│   │ 不阻塞！  │      └──────────┘      └──────────────┘      │
│   └──────────┘                                                │
│         │                                                      │
│         │  &lt;span class=&quot;hljs-built_in&quot;&gt;postMessage&lt;/span&gt;({ result: sum })                       │
│         │  ◀─────────────────────────────────────────         │
│         │                                                      │
└─────────┼──────────────────────────────────────────────────────┘
          │
          ▼
┌──────────────────────────────────────────────────────────────┐
│   主线程 onmessage 回调触发                                    │
│   → &lt;span class=&quot;hljs-built_in&quot;&gt;setResult&lt;/span&gt;(result)                                         │
│   → React 重新渲染                                            │
│   → 页面正常更新，无卡顿 ✅                                    │
└──────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：主线程和 Worker 线程各自有独立的调用栈和 Event Loop，互不阻塞。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-21&quot;&gt;5.3 代码实现详解&lt;/h4&gt;
&lt;p&gt;让我们看看项目中的具体实现：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;App.jsx - 主线程代码&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useRef, useState, useEffect } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; workerRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;);  &lt;span class=&quot;hljs-comment&quot;&gt;// 持久化引用 Worker 实例&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [result, setResult] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;);
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [loading, setLoading] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;);

  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-comment&quot;&gt;// 1. 创建 Worker 线程&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(
      &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;URL&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&quot;./worker.js&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;meta&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;url&lt;/span&gt;)
    );

    &lt;span class=&quot;hljs-comment&quot;&gt;// 2. 监听 Worker 发回的消息&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
      &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { result } = e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;;
      &lt;span class=&quot;hljs-title function_&quot;&gt;setResult&lt;/span&gt;(result);
      &lt;span class=&quot;hljs-title function_&quot;&gt;setLoading&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;);
    };

    &lt;span class=&quot;hljs-comment&quot;&gt;// 3. ⭐ 监听 Worker 错误（实际项目必须有）&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;onerror&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
      &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;error&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;Worker 执行出错:&#39;&lt;/span&gt;, e.&lt;span class=&quot;hljs-property&quot;&gt;message&lt;/span&gt;);
      &lt;span class=&quot;hljs-title function_&quot;&gt;setLoading&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;);
      &lt;span class=&quot;hljs-comment&quot;&gt;// 可以在这里做错误上报、降级处理等&lt;/span&gt;
    };

    &lt;span class=&quot;hljs-comment&quot;&gt;// 4. 组件卸载时销毁 Worker&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
      workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;terminate&lt;/span&gt;();
      workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;
    };
  }, []);

  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;startHeavyCalc&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
    &lt;span class=&quot;hljs-title function_&quot;&gt;setLoading&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;);
    &lt;span class=&quot;hljs-comment&quot;&gt;// 5. 向 Worker 发送消息，启动计算&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({
      &lt;span class=&quot;hljs-attr&quot;&gt;num&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;88&lt;/span&gt;,
    });
  };

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;style&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{{&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;padding:&lt;/span&gt; &quot;&lt;span class=&quot;hljs-attr&quot;&gt;30px&lt;/span&gt;&quot; }}&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;h2&lt;/span&gt;&amp;gt;&lt;/span&gt;useRef + WebWorker 耗时运算&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;h2&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;p&lt;/span&gt;&amp;gt;&lt;/span&gt;开启 web worker 线程执行 50 亿次循环，结束后通知主线程&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;p&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{startHeavyCalc}&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;disabled&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{loading}&lt;/span&gt;&amp;gt;&lt;/span&gt;
        {loading ? &quot;正在后台计算...&quot; : &quot;启动繁重计算任务&quot;}
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
      {result &amp;amp;&amp;amp; &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;h3&lt;/span&gt;&amp;gt;&lt;/span&gt;计算结果：{result}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;h3&lt;/span&gt;&amp;gt;&lt;/span&gt;}
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;worker.js - Worker 线程代码&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// Worker 线程中的全局对象是 self，不是 window&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// Worker 无法访问 DOM：没有 document、window 对象&lt;/span&gt;

self.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { num } = e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;Worker 收到主线程任务，参数为：&#39;&lt;/span&gt;, e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;);

    &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; sum = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;
    &lt;span class=&quot;hljs-comment&quot;&gt;// 50 亿次循环 - 这个计算在 Worker 线程执行&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// 主线程完全不受影响，页面依然流畅&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;hljs-number&quot;&gt;5000000000&lt;/span&gt;; i++) {
        sum += num * i;
    }

    &lt;span class=&quot;hljs-comment&quot;&gt;// 计算完成，通知主线程&lt;/span&gt;
    self.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({
        &lt;span class=&quot;hljs-attr&quot;&gt;result&lt;/span&gt;: sum
    });
};

&lt;span class=&quot;hljs-comment&quot;&gt;// ⭐ Worker 内部也可以处理错误&lt;/span&gt;
self.&lt;span class=&quot;hljs-property&quot;&gt;onerror&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;error&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;Worker 内部错误:&#39;&lt;/span&gt;, e);
    self.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;error&lt;/span&gt;: e.&lt;span class=&quot;hljs-property&quot;&gt;message&lt;/span&gt; });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-22&quot;&gt;5.4 主线程与 Worker 的通信时序&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;时间轴 ──────────────────────────────────────────────────────────▶

主线程:  ──postMessage──┐                    ┌──onmessage──
         (发送计算任务)  │                    │  (收到结果)
                        ▼                    ▲
&lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;:                ┌──onmessage── 计算中 ──postMessage──┐
                       │  (收到任务)   &lt;span class=&quot;hljs-number&quot;&gt;50&lt;/span&gt;亿次  (发送结果)   │
                       │                  循环              │
                       └───────────────────────────────────┘

注意：postMessage 传入的数据会被【结构化克隆算法】序列化
      这意味着：
      ✅ 可以传递：对象、数组、&lt;span class=&quot;hljs-title class_&quot;&gt;Map&lt;/span&gt;、&lt;span class=&quot;hljs-title class_&quot;&gt;Set&lt;/span&gt;、&lt;span class=&quot;hljs-title class_&quot;&gt;Date&lt;/span&gt;、&lt;span class=&quot;hljs-title class_&quot;&gt;RegExp&lt;/span&gt;、&lt;span class=&quot;hljs-title class_&quot;&gt;ArrayBuffer&lt;/span&gt;
      ❌ 不能传递：函数、&lt;span class=&quot;hljs-variable constant_&quot;&gt;DOM&lt;/span&gt; 节点、&lt;span class=&quot;hljs-title class_&quot;&gt;Symbol&lt;/span&gt;、&lt;span class=&quot;hljs-title class_&quot;&gt;WeakMap&lt;/span&gt;、&lt;span class=&quot;hljs-title class_&quot;&gt;WeakSet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-23&quot;&gt;5.5 Web Worker 的限制&lt;/h4&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;限制&lt;/th&gt;&lt;th&gt;原因&lt;/th&gt;&lt;th&gt;替代方案&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;无法访问 DOM&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;避免多线程操作 DOM 的竞态条件&lt;/td&gt;&lt;td&gt;在主线程操作 DOM，Worker 只做计算&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;无法访问 window/document&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;这些是主线程专属的全局对象&lt;/td&gt;&lt;td&gt;使用 &lt;code&gt;self&lt;/code&gt; 作为全局对象&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;同源限制&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;安全策略要求&lt;/td&gt;&lt;td&gt;使用 Blob URL 绕过&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;通信成本&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;数据需要序列化/反序列化&lt;/td&gt;&lt;td&gt;使用 Transferable Objects 零拷贝传输&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;无法访问 localStorage&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;localStorage 是同步 API，不在 Worker 中暴露&lt;/td&gt;&lt;td&gt;使用 IndexedDB（异步 API）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;无法访问 Cookie&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;安全限制&lt;/td&gt;&lt;td&gt;通过 postMessage 传递&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;面试追问&lt;/strong&gt;：「如何优化大数据的 Worker 通信性能？」&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;回答&lt;/strong&gt;：使用 &lt;strong&gt;Transferable Objects&lt;/strong&gt;。普通 postMessage 会序列化数据（拷贝），但 Transferable Objects 可以&lt;strong&gt;零拷贝转移&lt;/strong&gt;所有权：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 主线程 - 零拷贝发送 ArrayBuffer&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; buffer = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;ArrayBuffer&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;1024&lt;/span&gt; * &lt;span class=&quot;hljs-number&quot;&gt;1024&lt;/span&gt;); &lt;span class=&quot;hljs-comment&quot;&gt;// 1MB&lt;/span&gt;
worker.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({ buffer }, [buffer]);
&lt;span class=&quot;hljs-comment&quot;&gt;// transfer 后，主线程的 buffer 被置空，所有权转移到 Worker&lt;/span&gt;
&lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(buffer.&lt;span class=&quot;hljs-property&quot;&gt;byteLength&lt;/span&gt;); &lt;span class=&quot;hljs-comment&quot;&gt;// 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-24&quot;&gt;六、浏览器 vs Node.js 的 Event Loop 差异&lt;/h3&gt;
&lt;p&gt;这是面试中的高频追问，很多候选人只知道浏览器的 Event Loop，不知道 Node.js 的实现不同。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-25&quot;&gt;6.1 Node.js Event Loop 的 6 个阶段&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-arduino&quot; lang=&quot;arduino&quot;&gt;┌─────────────────────────────────────────────────────────┐
│              Node.js Event Loop（&lt;span class=&quot;hljs-number&quot;&gt;6&lt;/span&gt; 个阶段）               │
│                                                         │
│   ┌───────────────────────┐                              │
│   │                       │                              │
│   ▼                       │                              │
│   ┌─────────────┐         │                              │
│   │  ① timers    │  执行 setTimeout / setInterval 回调   │
│   └──────┬──────┘         │                              │
│          ▼                │                              │
│   ┌─────────────┐         │                              │
│   │  ② pending   │  执行系统级回调（TCP 错误等）           │
│   └──────┬──────┘         │                              │
│          ▼                │                              │
│   ┌─────────────┐         │                              │
│   │  ③ idle      │  内部使用                            │
│   │    prepare   │                                      │
│   └──────┬──────┘         │                              │
│          ▼                │                              │
│   ┌─────────────┐         │                              │
│   │  ④ poll      │  执行 I/O 回调                       │
│   │             │  计算应该阻塞多久等待 I/O               │
│   └──────┬──────┘         │                              │
│          ▼                │                              │
│   ┌─────────────┐         │                              │
│   │  ⑤ check     │  执行 setImmediate 回调               │
│   └──────┬──────┘         │                              │
│          ▼                │                              │
│   ┌─────────────┐         │                              │
│   │  ⑥ close     │  执行 close 事件回调                  │
│   │    callbacks │  如 socket.&lt;span class=&quot;hljs-built_in&quot;&gt;on&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;close&#39;&lt;/span&gt;)                │
│   └──────┬──────┘         │                              │
│          │                │                              │
│          └────────────────┘                              │
│                                                         │
│   ⭐ 每个阶段之间都会清空微任务队列                        │
│      process.nextTick 优先级高于 Promise.then             │
│                                                         │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-26&quot;&gt;6.2 浏览器 vs Node.js 对比&lt;/h4&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;特性&lt;/th&gt;&lt;th&gt;浏览器&lt;/th&gt;&lt;th&gt;Node.js&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;宏任务分阶段&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;不分，统一队列&lt;/td&gt;&lt;td&gt;分 6 个阶段&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;微任务执行时机&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;每个宏任务后&lt;/td&gt;&lt;td&gt;每个阶段之间&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;process.nextTick&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;不存在&lt;/td&gt;&lt;td&gt;优先级高于 Promise.then&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;setImmediate&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;不存在&lt;/td&gt;&lt;td&gt;在 check 阶段执行&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;requestAnimationFrame&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;每帧渲染前执行&lt;/td&gt;&lt;td&gt;不存在（无渲染）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;渲染&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;每帧可能触发&lt;/td&gt;&lt;td&gt;无渲染&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// Node.js 中的执行顺序&lt;/span&gt;
&lt;span class=&quot;hljs-title function_&quot;&gt;setImmediate&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;1. setImmediate&#39;&lt;/span&gt;));
&lt;span class=&quot;hljs-built_in&quot;&gt;setTimeout&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;2. setTimeout&#39;&lt;/span&gt;), &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;);
&lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;resolve&lt;/span&gt;().&lt;span class=&quot;hljs-title function_&quot;&gt;then&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;3. Promise&#39;&lt;/span&gt;));
process.&lt;span class=&quot;hljs-title function_&quot;&gt;nextTick&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;4. nextTick&#39;&lt;/span&gt;));

&lt;span class=&quot;hljs-comment&quot;&gt;// 输出（在 I/O 回调中可能有差异）：&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 4. nextTick        ← 优先级最高&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 3. Promise         ← 微任务&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 2. setTimeout      ← timers 阶段&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 1. setImmediate    ← check 阶段&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;面试追问&lt;/strong&gt;：「setTimeout(fn, 0) 和 setImmediate 的执行顺序固定吗？」&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;回答&lt;/strong&gt;：不固定。在主模块中，两者的执行顺序取决于系统调度。但在 I/O 回调中，&lt;code&gt;setImmediate&lt;/code&gt; &lt;strong&gt;总是先执行&lt;/strong&gt;，因为 I/O 回调在 poll 阶段执行，下一个阶段就是 check（setImmediate）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-27&quot;&gt;七、面试高频问题深度回答&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-28&quot;&gt;Q1：为什么 JavaScript 是单线程的？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;标准回答&lt;/strong&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;JavaScript 的单线程设计源于其最初的应用场景——浏览器中的 DOM 操作。如果 JS 是多线程的，两个线程同时操作同一个 DOM 节点（一个删除、一个修改），就会产生竞态条件，浏览器不知道该执行哪个操作。&lt;/p&gt;
&lt;p&gt;但这不意味着 JS 不能利用多核 CPU。通过 &lt;strong&gt;Web Worker&lt;/strong&gt;，我们可以在主线程之外创建独立的计算线程，通过消息传递（postMessage）与主线程通信。Node.js 中也有 &lt;strong&gt;cluster 模块&lt;/strong&gt;和 &lt;strong&gt;worker_threads&lt;/strong&gt; 来实现多线程。&lt;/p&gt;
&lt;p&gt;单线程的优势是简化了编程模型——不需要处理锁、死锁等并发问题。配合 Event Loop，单线程也能高效处理异步 I/O 操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-id=&quot;heading-29&quot;&gt;Q2：说说 Event Loop 的执行过程？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;标准回答&lt;/strong&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Event Loop 是浏览器（或 Node.js）协调调用栈、Web APIs 和任务队列的机制。它的核心逻辑是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;从宏任务队列取&lt;strong&gt;一个&lt;/strong&gt;宏任务执行（初始的 script 标签整体就是一个宏任务）&lt;/li&gt;
&lt;li&gt;执行过程中遇到异步 API（如 setTimeout、fetch），将其回调注册到对应的队列&lt;/li&gt;
&lt;li&gt;当前宏任务执行完后，清空&lt;strong&gt;所有微任务&lt;/strong&gt;（包括微任务中新增的微任务）&lt;/li&gt;
&lt;li&gt;如果需要渲染，执行 requestAnimationFrame 回调，然后进行渲染&lt;/li&gt;
&lt;li&gt;回到第 1 步，取下一个宏任务&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;微任务的优先级高于宏任务，且每次宏任务后会&lt;strong&gt;全部清空&lt;/strong&gt;，这就是为什么 Promise.then 的回调总是在 setTimeout 之前执行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-id=&quot;heading-30&quot;&gt;Q3：宏任务和微任务有什么区别？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;标准回答&lt;/strong&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;宏任务&lt;/strong&gt;包括 setTimeout、setInterval、I/O 操作、UI 渲染、script 标签整体代码。每次 Event Loop 只取&lt;strong&gt;一个&lt;/strong&gt;宏任务执行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;微任务&lt;/strong&gt;包括 Promise.then/catch/finally、MutationObserver、queueMicrotask。每次宏任务执行完后，会&lt;strong&gt;清空所有微任务&lt;/strong&gt;，包括执行微任务过程中新产生的微任务。&lt;/p&gt;
&lt;p&gt;关键区别在于粒度：宏任务一次取一个，微任务一次清全部。这也是为什么嵌套的 Promise.then 会在下一个 setTimeout 之前全部执行完毕。&lt;/p&gt;
&lt;p&gt;还有一个容易被忽略的点：&lt;code&gt;async/await&lt;/code&gt; 中 &lt;code&gt;await&lt;/code&gt; 后面的代码本质上是微任务，等价于放在 &lt;code&gt;.then()&lt;/code&gt; 回调中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-id=&quot;heading-31&quot;&gt;Q4：Web Worker 和主线程是怎么通信的？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;标准回答&lt;/strong&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;主线程和 Worker 通过 &lt;strong&gt;postMessage / onmessage&lt;/strong&gt; 进行双向通信。数据传递使用&lt;strong&gt;结构化克隆算法&lt;/strong&gt;（Structured Clone Algorithm），可以传递对象、数组、Map、Set、Date、ArrayBuffer 等复杂数据类型，但不能传递函数、DOM 节点和 Symbol。&lt;/p&gt;
&lt;p&gt;对于大数据传输，可以使用 &lt;strong&gt;Transferable Objects&lt;/strong&gt; 实现零拷贝：将 ArrayBuffer 的所有权从一方转移到另一方，避免序列化开销。传递后，原方的 ArrayBuffer 会被置空。&lt;/p&gt;
&lt;p&gt;另外，还可以使用 &lt;strong&gt;SharedArrayBuffer&lt;/strong&gt; 实现真正的内存共享，配合 &lt;strong&gt;Atomics&lt;/strong&gt; API 进行原子操作，但需要设置跨域隔离策略（Cross-Origin-Isolation）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-id=&quot;heading-32&quot;&gt;Q5：Web Worker 能操作 DOM 吗？为什么？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;标准回答&lt;/strong&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不能。Web Worker 被设计为纯粹的计算线程，无法访问 &lt;code&gt;document&lt;/code&gt;、&lt;code&gt;window&lt;/code&gt; 等 DOM 相关的全局对象。&lt;/p&gt;
&lt;p&gt;原因和 JS 为什么是单线程的一样——避免多线程操作 DOM 的竞态条件。Worker 的设计哲学是：&lt;strong&gt;计算密集型任务放在 Worker，DOM 操作留在主线程&lt;/strong&gt;，两者通过消息传递协作。&lt;/p&gt;
&lt;p&gt;如果需要操作 DOM，可以在 Worker 中完成计算，将结果通过 postMessage 发送给主线程，由主线程负责更新 DOM。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-id=&quot;heading-33&quot;&gt;Q6：Web Worker 有自己的 Event Loop 吗？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;标准回答&lt;/strong&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;是的，每个 Web Worker 都有自己独立的 Event Loop。这意味着 Worker 内部可以使用 &lt;code&gt;setTimeout&lt;/code&gt;、&lt;code&gt;Promise&lt;/code&gt; 等异步 API，它们的执行不会影响主线程的 Event Loop。&lt;/p&gt;
&lt;p&gt;但 Worker 的 Event Loop 比主线程简单——没有渲染相关的步骤（因为 Worker 无法操作 DOM，不需要渲染）。Worker 的 Event Loop 主要处理：消息队列（postMessage）、定时器、网络请求等。&lt;/p&gt;
&lt;p&gt;主线程和 Worker 的 Event Loop 通过 MessageChannel 连接，postMessage 的消息会被放入对方的消息队列中，等待对方的 Event Loop 处理。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-34&quot;&gt;八、实际应用场景与性能对比&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-35&quot;&gt;8.1 性能对比实验&lt;/h4&gt;
&lt;p&gt;在项目中分别用主线程和 Worker 执行 50 亿次循环：&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;/th&gt;&lt;th&gt;执行时间&lt;/th&gt;&lt;th&gt;页面状态&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;主线程直接执行&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;~6 秒&lt;/td&gt;&lt;td&gt;❌ 页面完全卡死，无法点击&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Web Worker 执行&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;~6 秒&lt;/td&gt;&lt;td&gt;✅ 页面流畅，可正常交互&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;计算时间几乎相同（CPU 工作量一样），但&lt;strong&gt;用户体验完全不同&lt;/strong&gt;。这就是 Web Worker 的价值——&lt;strong&gt;不加速计算，但释放主线程&lt;/strong&gt;。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-36&quot;&gt;8.2 适用场景&lt;/h4&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;/th&gt;&lt;th&gt;说明&lt;/th&gt;&lt;th&gt;示例&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;大数据计算&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;数组排序、数据统计、加密解密&lt;/td&gt;&lt;td&gt;处理 100 万条数据的排序&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;图像处理&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Canvas 像素操作、图片滤镜&lt;/td&gt;&lt;td&gt;实时美颜滤镜&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;复杂算法&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;路径规划、物理模拟、AI 推理&lt;/td&gt;&lt;td&gt;游戏中的寻路算法&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;数据预处理&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;JSON 解析、数据格式转换&lt;/td&gt;&lt;td&gt;解析 50MB 的 JSON 文件&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;轮询任务&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;持续检查服务器状态&lt;/td&gt;&lt;td&gt;WebSocket 心跳检测&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;加密解密&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;AES/RSA 加解密操作&lt;/td&gt;&lt;td&gt;端到端加密聊天&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h4 data-id=&quot;heading-37&quot;&gt;8.3 实际场景示例：大数组排序&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// worker-sort.js&lt;/span&gt;
self.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { data, algorithm } = e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;;

  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; startTime = performance.&lt;span class=&quot;hljs-title function_&quot;&gt;now&lt;/span&gt;();

  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; sorted;
  &lt;span class=&quot;hljs-keyword&quot;&gt;switch&lt;/span&gt; (algorithm) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;quickSort&#39;&lt;/span&gt;:
      sorted = &lt;span class=&quot;hljs-title function_&quot;&gt;quickSort&lt;/span&gt;(data);
      &lt;span class=&quot;hljs-keyword&quot;&gt;break&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;mergeSort&#39;&lt;/span&gt;:
      sorted = &lt;span class=&quot;hljs-title function_&quot;&gt;mergeSort&lt;/span&gt;(data);
      &lt;span class=&quot;hljs-keyword&quot;&gt;break&lt;/span&gt;;
    &lt;span class=&quot;hljs-attr&quot;&gt;default&lt;/span&gt;:
      sorted = data.&lt;span class=&quot;hljs-title function_&quot;&gt;sort&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;a, b&lt;/span&gt;) =&amp;gt;&lt;/span&gt; a - b);
  }

  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; endTime = performance.&lt;span class=&quot;hljs-title function_&quot;&gt;now&lt;/span&gt;();

  self.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({
    sorted,
    &lt;span class=&quot;hljs-attr&quot;&gt;timeCost&lt;/span&gt;: endTime - startTime
  });
};

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;quickSort&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;arr&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (arr.&lt;span class=&quot;hljs-property&quot;&gt;length&lt;/span&gt; &amp;lt;= &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; arr;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; pivot = arr[&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;];
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; left = arr.&lt;span class=&quot;hljs-title function_&quot;&gt;slice&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;).&lt;span class=&quot;hljs-title function_&quot;&gt;filter&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-params&quot;&gt;x&lt;/span&gt; =&amp;gt;&lt;/span&gt; x &amp;lt;= pivot);
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; right = arr.&lt;span class=&quot;hljs-title function_&quot;&gt;slice&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;).&lt;span class=&quot;hljs-title function_&quot;&gt;filter&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-params&quot;&gt;x&lt;/span&gt; =&amp;gt;&lt;/span&gt; x &amp;gt; pivot);
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; [...&lt;span class=&quot;hljs-title function_&quot;&gt;quickSort&lt;/span&gt;(left), pivot, ...&lt;span class=&quot;hljs-title function_&quot;&gt;quickSort&lt;/span&gt;(right)];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-38&quot;&gt;8.4 推荐库&lt;/h4&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;库&lt;/th&gt;&lt;th&gt;说明&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fnickersk%2Fcomlink&quot; target=&quot;_blank&quot; title=&quot;https://github.com/nickersk/comlink&quot; ref=&quot;nofollow noopener noreferrer&quot;&gt;comlink&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Google 出品，用 Proxy 让 Worker 调用像本地函数一样简单&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fnickersk%2Fworkerize&quot; target=&quot;_blank&quot; title=&quot;https://github.com/nickersk/workerize&quot; ref=&quot;nofollow noopener noreferrer&quot;&gt;workerize&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;自动将模块转为 Worker&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fnickersk%2Fthreads.js&quot; target=&quot;_blank&quot; title=&quot;https://github.com/nickersk/threads.js&quot; ref=&quot;nofollow noopener noreferrer&quot;&gt;threads.js&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;跨平台 Worker 抽象（浏览器 + Node.js）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fnickersk%2Fworkerpool&quot; target=&quot;_blank&quot; title=&quot;https://github.com/nickersk/workerpool&quot; ref=&quot;nofollow noopener noreferrer&quot;&gt;workerpool&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Worker 线程池管理&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 使用 comlink 简化 Worker 通信&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { wrap } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;comlink&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; worker = &lt;span class=&quot;hljs-title function_&quot;&gt;wrap&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;./worker.js&#39;&lt;/span&gt;));
&lt;span class=&quot;hljs-comment&quot;&gt;// 直接调用 Worker 中的函数，像调用本地函数一样&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; result = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; worker.&lt;span class=&quot;hljs-title function_&quot;&gt;heavyCalculation&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;88&lt;/span&gt;);
&lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(result);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-39&quot;&gt;总结&lt;/h3&gt;
&lt;p&gt;通过这篇文章，我们从 V8 引擎的内部结构出发，完整地梳理了 JavaScript 的运行原理：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-vbnet&quot; lang=&quot;vbnet&quot;&gt;源代码
  │
  ▼
V8 引擎（Parser → Ignition → TurboFan）
  │
  ▼
执行上下文 &amp;amp; 调用栈
  │
  ▼
&lt;span class=&quot;hljs-keyword&quot;&gt;Event&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;Loop&lt;/span&gt;（同步 → 微任务 → 渲染 → 宏任务）
  │
  ├── 单线程限制：耗时计算会阻塞页面
  │
  ▼
Web Worker（独立线程 + 独立 &lt;span class=&quot;hljs-keyword&quot;&gt;Event&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;Loop&lt;/span&gt; + postMessage 通信）
  │
  ▼
Node.js &lt;span class=&quot;hljs-keyword&quot;&gt;Event&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;Loop&lt;/span&gt;（&lt;span class=&quot;hljs-number&quot;&gt;6&lt;/span&gt; 阶段 + process.nextTick）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;面试答题建议&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先说结论&lt;/strong&gt;：不要铺垫太多，直接给出核心答案&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配图解释&lt;/strong&gt;：画调用栈变化、Event Loop 流程图，让面试官看到你的理解&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码验证&lt;/strong&gt;：用具体代码示例证明你的观点&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主动延伸&lt;/strong&gt;：回答完基本问题后，主动提到 Web Worker、Node.js 差异等加分项&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意边界&lt;/strong&gt;：提到精度问题、Transferable Objects 等细节，展示深度&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;最后的建议&lt;/strong&gt;：理解原理比背答案重要。当你真正理解了 V8 引擎如何执行代码、Event Loop 如何调度任务，面试中的任何变体问题都能从容应对。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;如果这篇文章对你有帮助，别忘了点赞收藏哦！&lt;/strong&gt; 👍&lt;/p&gt;</description><link>https://juejin.cn/post/7671120692243070982</link><guid isPermaLink="false">https://juejin.cn/post/7671120692243070982</guid><pubDate>Fri, 07 Aug 2026 15:52:59 GMT</pubDate><author>ReBound</author><category>前端</category><category>JavaScript</category><category>React.js</category></item><item><title>Web Worker 不负责渲染：React 中的线程分工与消息闭环</title><description>&lt;h2 data-id=&quot;heading-0&quot;&gt;Web Worker 不负责渲染：React 中的线程分工与消息闭环&lt;/h2&gt;
&lt;p&gt;摘要：这个 React Demo 的关键不是“开启了多线程”这么简单，而是把职责拆开：主线程负责页面，Worker 负责计算，&lt;code&gt;postMessage&lt;/code&gt; 负责传输，&lt;code&gt;useRef&lt;/code&gt; 负责保存实例，&lt;code&gt;useEffect&lt;/code&gt; 负责生命周期。文章基于本地源码整理，运行未验证。&lt;/p&gt;
&lt;p&gt;当一个 React 组件执行很大的循环时，真正被占用的是页面主线程。Event Loop 能安排异步任务，却不会让当前的大循环自动并行。此时更合适的思路是使用 Web Worker，并把 Worker 当作一个需要管理的外部资源。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-1&quot;&gt;这个 Demo 的核心判断&lt;/h3&gt;
&lt;p&gt;不要把 Worker 写在组件函数体里，否则每次重新渲染都有重复创建的风险。应当：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot; lang=&quot;text&quot;&gt;useEffect 创建
useRef 保存
postMessage 发送任务
onmessage 接收结果
cleanup terminate
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-2&quot;&gt;创建线程和保存实例是两件事&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; workerRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;)

&lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(
    &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;URL&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;../worker.js&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;meta&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;url&lt;/span&gt;)
  )
}, [])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里最容易混淆：&lt;code&gt;useRef&lt;/code&gt; 并没有创建线程，真正创建线程的是 &lt;code&gt;new Worker()&lt;/code&gt;。ref 只是提供了一个稳定的 &lt;code&gt;{ current }&lt;/code&gt; 容器。&lt;/p&gt;
&lt;p&gt;Worker 实例不需要参与页面渲染，所以不适合用 state 保存。按钮点击时可以拿出同一个实例：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;num&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;88&lt;/span&gt; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-3&quot;&gt;消息协议：两次 postMessage，方向相反&lt;/h3&gt;
&lt;p&gt;主线程发送：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({
  &lt;span class=&quot;hljs-attr&quot;&gt;num&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;88&lt;/span&gt;,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Worker 接收并计算：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;self.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { num } = e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; sum = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;hljs-number&quot;&gt;5000000000&lt;/span&gt;; i++) {
    sum += num * i
  }

  self.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;result&lt;/span&gt;: sum })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Worker 返回的数据由主线程接收：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-title function_&quot;&gt;setResult&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;result&lt;/span&gt;)
  &lt;span class=&quot;hljs-title function_&quot;&gt;setLoading&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此要记住：&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;代码&lt;/th&gt;&lt;th&gt;方向&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;worker.postMessage(data)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;主线程到 Worker&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;self.onmessage&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Worker 接收主线程消息&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;self.postMessage(data)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Worker 到主线程&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;worker.onmessage&lt;/code&gt;&lt;/td&gt;&lt;td&gt;主线程接收 Worker 消息&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 data-id=&quot;heading-4&quot;&gt;为什么 Worker 不能直接改页面？&lt;/h3&gt;
&lt;p&gt;Worker 与页面主线程相互独立，不能直接访问 React 组件或 DOM。它只返回普通消息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;self.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;result&lt;/span&gt;: sum })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主线程收到后再执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-title function_&quot;&gt;setResult&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;result&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;页面更新仍属于 React 主线程的职责。也就是说，Worker 的价值是“分担计算”，不是“替代渲染”。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-5&quot;&gt;生命周期清理是必要步骤&lt;/h3&gt;
&lt;p&gt;Worker 创建后必须考虑关闭：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; worker = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(
    &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;URL&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;../worker.js&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;meta&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;url&lt;/span&gt;)
  )

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    worker.&lt;span class=&quot;hljs-title function_&quot;&gt;terminate&lt;/span&gt;()
  }
}, [])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;组件卸载时调用 &lt;code&gt;terminate()&lt;/code&gt;，可以停止后台线程。真实代码还会把 &lt;code&gt;workerRef.current&lt;/code&gt; 置为 &lt;code&gt;null&lt;/code&gt;，表示当前没有可用实例。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-6&quot;&gt;这个示例还应该检查什么？&lt;/h3&gt;
&lt;p&gt;静态代码中存在几个需要继续验证的边界：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;页面提示“五亿次”，实际循环上限是 &lt;code&gt;5000000000&lt;/code&gt;，即 50 亿次。&lt;/li&gt;
&lt;li&gt;计算结果可能超出 JavaScript 安全整数范围，普通 &lt;code&gt;Number&lt;/code&gt; 不能保证精确整数。&lt;/li&gt;
&lt;li&gt;当前未见 &lt;code&gt;onerror&lt;/code&gt; 或 &lt;code&gt;onmessageerror&lt;/code&gt;，Worker 失败后 loading 的状态需要运行验证。&lt;/li&gt;
&lt;li&gt;如果结果用 &lt;code&gt;result &amp;amp;&amp;amp;&lt;/code&gt; 条件渲染，结果为 &lt;code&gt;0&lt;/code&gt; 时不会显示。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些是源码检查结论，不代表已经在浏览器中复现；本文代码运行未验证。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-7&quot;&gt;复用这套设计时的检查顺序&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-text&quot; lang=&quot;text&quot;&gt;是否是 CPU 密集任务？
  ↓ 是
是否能拆成纯计算、无需 DOM 的函数？
  ↓ 是
useEffect 创建 Worker
  ↓
useRef 保存 Worker
  ↓
约定输入和输出消息结构
  ↓
onmessage 更新 state
  ↓
cleanup terminate
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-8&quot;&gt;结尾&lt;/h3&gt;
&lt;p&gt;React 中使用 Web Worker 的重点不是把所有异步工作都放到新线程，而是识别真正会阻塞主线程的 CPU 计算，并建立清晰的消息协议。&lt;code&gt;useRef&lt;/code&gt; 解决实例保存，&lt;code&gt;useEffect&lt;/code&gt; 解决资源生命周期，&lt;code&gt;useState&lt;/code&gt; 解决界面状态，Worker 解决计算隔离。下一步可以给 Demo 补充错误消息和取消任务能力，再验证不同输入规模下的行为。&lt;/p&gt;</description><link>https://juejin.cn/post/7671153017660473407</link><guid isPermaLink="false">https://juejin.cn/post/7671153017660473407</guid><pubDate>Fri, 07 Aug 2026 15:15:47 GMT</pubDate><author>BreezeJiang</author><category>前端</category><category>JavaScript</category><category>React.js</category></item><item><title>React Context + 自定义 Hooks：告别 Props 地狱，一行代码跨层级传数据</title><description>&lt;h2 data-id=&quot;heading-0&quot;&gt;React Context + 自定义 Hooks：告别 Props 地狱，一行代码跨层级传数据&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;从&quot;层层传递 props&quot;的噩梦讲起，用主题切换和鼠标追踪两个实例，带你彻底搞懂 React Context 和自定义 Hooks。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-id=&quot;heading-1&quot;&gt;前言&lt;/h3&gt;
&lt;p&gt;你有没有遇到过这种情况：一个数据在最外层组件获取，但真正用到它的却是第五层嵌套的子组件？你不得不在中间的每一层都加上 props 传递，哪怕那些中间组件根本不关心这个数据。&lt;/p&gt;
&lt;p&gt;React Context 就是来解决这个问题的。这篇文章适合&lt;strong&gt;刚接触 React、已经会写 useState 和父子组件通信，但还没搞懂&quot;跨层级传数据&quot;怎么优雅实现&lt;/strong&gt;的你。&lt;/p&gt;
&lt;p&gt;读完你将收获：理解 Context 三件套（&lt;code&gt;createContext&lt;/code&gt; / &lt;code&gt;Provider&lt;/code&gt; / &lt;code&gt;useContext&lt;/code&gt;）、学会封装自定义 Hooks、看懂一个完整项目的架构思路。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文末附有完整可运行项目代码，可以直接 clone 下来跑起来看效果。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 data-id=&quot;heading-2&quot;&gt;项目概览&lt;/h3&gt;
&lt;p&gt;这个项目用两个 Demo 展示了 Context 和自定义 Hooks 的用法：&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Demo&lt;/th&gt;&lt;th&gt;文件&lt;/th&gt;&lt;th&gt;演示内容&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;鼠标追踪&lt;/td&gt;&lt;td&gt;&lt;code&gt;App.jsx&lt;/code&gt; + &lt;code&gt;useMouse.js&lt;/code&gt;&lt;/td&gt;&lt;td&gt;自定义 Hook 封装鼠标坐标&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;主题切换&lt;/td&gt;&lt;td&gt;&lt;code&gt;App2.jsx&lt;/code&gt; + &lt;code&gt;Theme.Context.jsx&lt;/code&gt; + &lt;code&gt;useTheme.js&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Context 跨层级共享主题&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h4 data-id=&quot;heading-3&quot;&gt;组件树&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-css&quot; lang=&quot;css&quot;&gt;App
├── ThemeContext&lt;span class=&quot;hljs-selector-class&quot;&gt;.Provider&lt;/span&gt;（提供主题数据）
│   └── Page（消费主题，显示当前 theme 值）
│       └── Child（消费主题，按钮随主题变色）
└── &lt;span class=&quot;hljs-selector-tag&quot;&gt;button&lt;/span&gt;（点击切换 light / dark）
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-4&quot;&gt;用到的核心技术&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;createContext&lt;/code&gt; — 创建上下文容器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.Provider&lt;/code&gt; — 提供数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useContext&lt;/code&gt; — 消费数据&lt;/li&gt;
&lt;li&gt;自定义 Hooks（&lt;code&gt;useTheme&lt;/code&gt;、&lt;code&gt;useMouse&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useState&lt;/code&gt; + &lt;code&gt;useEffect&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-id=&quot;heading-5&quot;&gt;核心知识点一：Context — 跨层级的数据管道&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-6&quot;&gt;问题：Props 地狱长什么样？&lt;/h4&gt;
&lt;p&gt;假设没有 Context，你要把一个 &lt;code&gt;theme&lt;/code&gt; 数据从 &lt;code&gt;App&lt;/code&gt; 传给最深层的 &lt;code&gt;Child&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ❌ props 层层搬运，中间组件被迫传递自己不用的数据&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-string&quot;&gt;&#39;dark&#39;&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Page&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;theme&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Page&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;{ theme }&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Child&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;theme&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;;  &lt;span class=&quot;hljs-comment&quot;&gt;// Page 自己根本不用 theme&lt;/span&gt;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Child&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;{ theme }&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;className&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt;&amp;gt;&lt;/span&gt;按钮&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;;  &lt;span class=&quot;hljs-comment&quot;&gt;// 只有它真正需要&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;项目 &lt;code&gt;App2.jsx&lt;/code&gt; 里有一段被注释掉的代码，恰好就是这种&quot;逐层传递&quot;的写照：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// App2.jsx 注释代码&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// &amp;lt;Parent&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;//   &amp;lt;Child&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;//     &amp;lt;GrandChild&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;//       &amp;lt;GreatGrandChild&amp;gt;&amp;lt;/GreatGrandChild&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;//     &amp;lt;/GrandChild&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;//   &amp;lt;/Child&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// &amp;lt;/Parent&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果嵌套到第四层、第五层，每层都要做 props &quot;搬运工&quot;，代码又乱又容易出错。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-7&quot;&gt;解决：Context 三件套&lt;/h4&gt;
&lt;p&gt;React Context 就像一条&lt;strong&gt;直达管道&lt;/strong&gt;——数据在最外层注入，任意深层组件都能直接读取，中间层无需参与。&lt;/p&gt;
&lt;h5 data-id=&quot;heading-8&quot;&gt;第一步：&lt;code&gt;createContext&lt;/code&gt; 创建管道&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// Theme.Context.jsx&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { createContext } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt; = &lt;span class=&quot;hljs-title function_&quot;&gt;createContext&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&quot;light&quot;&lt;/span&gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;createContext(&quot;light&quot;)&lt;/code&gt; 做了两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个上下文对象（管道）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&quot;light&quot;&lt;/code&gt; 作为默认值——当组件树没有被 Provider 包裹时，取这个值&lt;/li&gt;
&lt;/ol&gt;
&lt;h5 data-id=&quot;heading-9&quot;&gt;第二步：&lt;code&gt;Provider&lt;/code&gt; 注入数据&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// App2.jsx&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [theme, setTheme] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;);

    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
        &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Page&lt;/span&gt; /&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{()&lt;/span&gt; =&amp;gt;&lt;/span&gt;
                setTheme(theme === &#39;light&#39; ? &#39;dark&#39; : &#39;light&#39;)
            }&amp;gt;切换主题&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Provider&lt;/code&gt; 是一个&quot;数据发射器&quot;，&lt;code&gt;value={theme}&lt;/code&gt; 告诉它当前要共享的数据是什么。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重点&lt;/strong&gt;：Provider 不需要放在全局——你想让哪个子树共享数据，就在哪里包裹。这里的 &lt;code&gt;ThemeContext.Provider&lt;/code&gt; 只包裹了 &lt;code&gt;&amp;lt;Page /&amp;gt;&lt;/code&gt; 和按钮，这就是它的&quot;作用范围&quot;。&lt;/p&gt;
&lt;h5 data-id=&quot;heading-10&quot;&gt;第三步：&lt;code&gt;useContext&lt;/code&gt; 消费数据&lt;/h5&gt;
&lt;p&gt;项目中 Page 和 Child 都直接读取了 theme，但&lt;strong&gt;没有经过任何中间层传递&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// Page.jsx&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Page&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;();   &lt;span class=&quot;hljs-comment&quot;&gt;// 直接用，不需要从 App 传下来&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
        &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&amp;gt;&lt;/span&gt;
            Page {theme}
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;br&lt;/span&gt; /&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Child&lt;/span&gt; /&amp;gt;&lt;/span&gt;             {/* 传给 Child 时根本没写 theme prop */}
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
    );
};

&lt;span class=&quot;hljs-comment&quot;&gt;// Child.jsx&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Child&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;();   &lt;span class=&quot;hljs-comment&quot;&gt;// 直接取！&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;className&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt;&amp;gt;&lt;/span&gt;按钮{theme}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Context 的数据流转图：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-scss&quot; lang=&quot;scss&quot;&gt;ThemeContext&lt;span class=&quot;hljs-selector-class&quot;&gt;.Provider&lt;/span&gt; value=&quot;dark&quot;
        │
        ├── Page ──&lt;span class=&quot;hljs-built_in&quot;&gt;useContext&lt;/span&gt;()──→ &quot;dark&quot;  ✅ 跳过 App，直接读
        │     │
        │     └── Child ──&lt;span class=&quot;hljs-built_in&quot;&gt;useContext&lt;/span&gt;()──→ &quot;dark&quot;  ✅ 跳过 Page，直接读
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;每一层都跳过了 props，直接&quot;伸手&quot;从 Context 里取。这就是跨层级的本质。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 data-id=&quot;heading-11&quot;&gt;核心知识点二：自定义 Hooks — 把&quot;能力&quot;封装成一行调用&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-12&quot;&gt;是什么？&lt;/h4&gt;
&lt;p&gt;自定义 Hook 就是一个以 &lt;code&gt;use&lt;/code&gt; 开头的函数，它可以调用 React 内置 Hooks（useState、useEffect 等），然后把响应式逻辑打包成一个可复用的&quot;工具箱&quot;。&lt;/p&gt;
&lt;p&gt;项目中封装了两个自定义 Hook：&lt;/p&gt;
&lt;h4 data-id=&quot;heading-13&quot;&gt;&lt;code&gt;useTheme&lt;/code&gt;：让 Context 消费更优雅&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// hooks/useTheme.js&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;../Theme.Context.jsx&#39;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useContext } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么多此一举封装一层？&lt;/strong&gt; 直接在每个组件里写 &lt;code&gt;useContext(ThemeContext)&lt;/code&gt; 不就完了？&lt;/p&gt;
&lt;p&gt;三个原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;语义化&lt;/strong&gt;：&lt;code&gt;useTheme()&lt;/code&gt; 比 &lt;code&gt;useContext(ThemeContext)&lt;/code&gt; 读起来更直观&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解耦&lt;/strong&gt;：假如将来 Context 从 &lt;code&gt;ThemeContext&lt;/code&gt; 换成 &lt;code&gt;Redux&lt;/code&gt; 或 &lt;code&gt;Zustand&lt;/code&gt;，只需要改 &lt;code&gt;useTheme.js&lt;/code&gt; 一个文件，所有组件无感知&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IDE 友好&lt;/strong&gt;：输入 &lt;code&gt;useTheme()&lt;/code&gt; 自动提示清晰，不会跟其他 Context 搞混&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-id=&quot;heading-14&quot;&gt;&lt;code&gt;useMouse&lt;/code&gt;：封装副作用逻辑&lt;/h4&gt;
&lt;p&gt;这是项目另一个 Demo —— 追踪鼠标坐标：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// hooks/useMouse.js&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useState, useEffect } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useMouse&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [x, setX] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;);
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [y, setY] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;);

    &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
        &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;handleMouseMove&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt; {
            &lt;span class=&quot;hljs-title function_&quot;&gt;setX&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;clientX&lt;/span&gt;);
            &lt;span class=&quot;hljs-title function_&quot;&gt;setY&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;clientY&lt;/span&gt;);
        };
        &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;addEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove);

        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
            &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;removeEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove);
        };
    }, []);

    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; { x, y };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逐行拆解：&lt;/p&gt;





































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;代码&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;const [x, setX] = useState(0)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;响应式存储鼠标 X 坐标，初始值 0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;const [y, setY] = useState(0)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Y 坐标同上&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;useEffect(..., [])&lt;/code&gt;&lt;/td&gt;&lt;td&gt;组件挂载时执行一次，&lt;code&gt;[]&lt;/code&gt; 表示不依赖任何变量&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;document.addEventListener(&#39;mousemove&#39;, handleMouseMove)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;监听全局鼠标移动事件&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;handleMouseMove&lt;/code&gt;&lt;/td&gt;&lt;td&gt;每次鼠标移动，更新 x、y 状态&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;return () =&amp;gt; removeEventListener(...)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;清理函数&lt;/strong&gt;——组件卸载时移除监听，防止内存泄漏&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;return { x, y }&lt;/code&gt;&lt;/td&gt;&lt;td&gt;对外暴露坐标，让调用方想怎么用就怎么用&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;使用方极其简洁&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// App.jsx&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { x, y } = &lt;span class=&quot;hljs-title function_&quot;&gt;useMouse&lt;/span&gt;();

    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
        &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;
            {x &amp;amp;&amp;amp; y ? `x:${x}, y:${y}` : &#39;鼠标未移动&#39;}
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一行 &lt;code&gt;useMouse()&lt;/code&gt; 就搞定了所有状态管理 + 事件监听 + 清理逻辑。这就是自定义 Hook 的威力——&lt;strong&gt;把复杂的响应式逻辑封装成一个&quot;黑盒&quot;，使用时只需要一行调用。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 data-id=&quot;heading-15&quot;&gt;知识串联：Context + 自定义 Hooks = 完美搭档&lt;/h3&gt;
&lt;p&gt;把两者结合起来，就形成了项目中这套架构模式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-scss&quot; lang=&quot;scss&quot;&gt;Theme&lt;span class=&quot;hljs-selector-class&quot;&gt;.Context&lt;/span&gt;&lt;span class=&quot;hljs-selector-class&quot;&gt;.jsx&lt;/span&gt;          ←  createContext 建管道
        ↓
hooks/useTheme&lt;span class=&quot;hljs-selector-class&quot;&gt;.js&lt;/span&gt;          ←  自定义 Hook 封装 useContext
        ↓
components/Page&lt;span class=&quot;hljs-selector-class&quot;&gt;.jsx&lt;/span&gt;        ←  调用 &lt;span class=&quot;hljs-built_in&quot;&gt;useTheme&lt;/span&gt;()，一行消费
components/Child&lt;span class=&quot;hljs-selector-class&quot;&gt;.jsx&lt;/span&gt;       ←  同上，完全不需要 props
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这就是 React 推荐的&quot;逻辑层 → 视图层&quot;分层方式&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Context 负责&quot;数据管道&quot;&lt;/li&gt;
&lt;li&gt;自定义 Hooks 负责&quot;消费数据的逻辑&quot;&lt;/li&gt;
&lt;li&gt;组件只负责&quot;渲染 UI&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-id=&quot;heading-16&quot;&gt;总结&lt;/h3&gt;
&lt;p&gt;回顾你从这个项目能掌握的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Context 是跨层级共享数据的管道&lt;/strong&gt;——&lt;code&gt;createContext&lt;/code&gt; 建管道，&lt;code&gt;Provider&lt;/code&gt; 注数据，&lt;code&gt;useContext&lt;/code&gt; 取数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不需要把 Provider 包在全局&lt;/strong&gt;——包在哪个子树，数据就在哪个范围共享&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自定义 Hook 本质是逻辑封装&lt;/strong&gt;——把 useState、useEffect 等打包成一行调用的&quot;黑盒&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;useTheme&lt;/code&gt; 展示了&quot;解耦&quot;的价值&lt;/strong&gt;——组件不需要知道 Context 的具体实现&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;useMouse&lt;/code&gt; 展示了&quot;副作用管理&quot;的规范写法&lt;/strong&gt;——监听、更新、清理三步走，useEffect 里不要忘记 return 清理函数&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-id=&quot;heading-17&quot;&gt;下一步可以尝试的扩展&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;给 &lt;code&gt;useMouse&lt;/code&gt; 加一个节流（throttle），减少 setState 频率&lt;/li&gt;
&lt;li&gt;用 Context 实现全局的用户登录状态（&lt;code&gt;UserContext&lt;/code&gt;，提供 &lt;code&gt;user&lt;/code&gt; 和 &lt;code&gt;setUser&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;学习 &lt;code&gt;useReducer&lt;/code&gt; + Context 的组合，处理更复杂的状态逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-id=&quot;heading-18&quot;&gt;完整项目代码&lt;/h3&gt;
&lt;p&gt;项目托管在 Gitee，可以直接 clone 运行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot; lang=&quot;bash&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;# 1. 克隆仓库&lt;/span&gt;
git &lt;span class=&quot;hljs-built_in&quot;&gt;clone&lt;/span&gt; git@gitee.com:dcx2758/ai_doubao_dcx.git
&lt;span class=&quot;hljs-built_in&quot;&gt;cd&lt;/span&gt; ai_doubao_dcx/dcx/fe/react/basic/context-demo/context-demo/

&lt;span class=&quot;hljs-comment&quot;&gt;# 2. 安装依赖&lt;/span&gt;
npm install

&lt;span class=&quot;hljs-comment&quot;&gt;# 3. 启动开发服务器&lt;/span&gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-19&quot;&gt;项目文件树&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot; lang=&quot;bash&quot;&gt;context-demo/
├── index.html
├── package.json
├── vite.config.js
├── eslint.config.js
├── src/
│   ├── main.jsx               &lt;span class=&quot;hljs-comment&quot;&gt;# 入口文件&lt;/span&gt;
│   ├── App.jsx                &lt;span class=&quot;hljs-comment&quot;&gt;# Demo1: useMouse 鼠标追踪&lt;/span&gt;
│   ├── App2.jsx               &lt;span class=&quot;hljs-comment&quot;&gt;# Demo2: Context 主题切换（手动改为 main.jsx 引入）&lt;/span&gt;
│   ├── Theme.Context.jsx      &lt;span class=&quot;hljs-comment&quot;&gt;# createContext 创建主题上下文&lt;/span&gt;
│   ├── index.css              &lt;span class=&quot;hljs-comment&quot;&gt;# 全局样式（CSS 变量 + 暗色模式）&lt;/span&gt;
│   ├── App.css                &lt;span class=&quot;hljs-comment&quot;&gt;# App 样式&lt;/span&gt;
│   ├── hooks/
│   │   ├── useMouse.js        &lt;span class=&quot;hljs-comment&quot;&gt;# 自定义 Hook: 鼠标坐标&lt;/span&gt;
│   │   └── useTheme.js        &lt;span class=&quot;hljs-comment&quot;&gt;# 自定义 Hook: 消费主题上下文&lt;/span&gt;
│   └── components/
│       ├── Page.jsx           &lt;span class=&quot;hljs-comment&quot;&gt;# 页面组件，消费主题&lt;/span&gt;
│       └── Child.jsx          &lt;span class=&quot;hljs-comment&quot;&gt;# 子组件，消费主题&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-20&quot;&gt;依赖及版本&lt;/h4&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;依赖&lt;/th&gt;&lt;th&gt;版本&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;react&lt;/td&gt;&lt;td&gt;^19.2.6&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;react-dom&lt;/td&gt;&lt;td&gt;^19.2.6&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;vite&lt;/td&gt;&lt;td&gt;^8.0.12&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;@vitejs/plugin-react&lt;/td&gt;&lt;td&gt;^6.0.1&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;提示&lt;/strong&gt;：默认 &lt;code&gt;main.jsx&lt;/code&gt; 引入的是 &lt;code&gt;App.jsx&lt;/code&gt;（鼠标追踪 Demo）。如果想体验主题切换 Demo，把 &lt;code&gt;import App from &#39;./App.jsx&#39;&lt;/code&gt; 改为 &lt;code&gt;import App from &#39;./App2.jsx&#39;&lt;/code&gt;，然后在切换主题的页面中观察 Page 和 Child 如何跨层级拿到数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;你觉得 Context 这种方式怎么样？在你的项目里有没有遇到过&quot;props 地狱&quot;？欢迎在评论区交流 👏&lt;/p&gt;</description><link>https://juejin.cn/post/7671189079686594569</link><guid isPermaLink="false">https://juejin.cn/post/7671189079686594569</guid><pubDate>Fri, 07 Aug 2026 15:12:14 GMT</pubDate><author>橘子星</author><category>前端</category><category>JavaScript</category></item><item><title>useRef + Web Worker：React 中的多线程计算</title><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;摘要&lt;/strong&gt;：JS 是单线程语言，但浏览器提供了 Web Worker 开辟独立线程跑纯计算任务。本文用 useRef 持久化 Worker 实例避免渲染重置，useEffect 在挂载时初始化并在卸载时 terminate，postMessage 发任务给子线程，五千万次循环不卡主界面。消息机制串联双线程通信，loading 状态承接用户等待。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-0&quot;&gt;目录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671100905954705444#%E4%B8%80web-worker-%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88&quot; title=&quot;#%E4%B8%80web-worker-%E8%A7%A3%E5%86%B3%E4%BB%80%E4%B9%88&quot;&gt;Web Worker 解决什么？&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671100905954705444#%E4%BA%8Cuseref%E5%9C%A8-react-%E4%B8%AD%E6%8C%81%E4%B9%85%E5%8C%96-worker-%E5%AE%9E%E4%BE%8B&quot; title=&quot;#%E4%BA%8Cuseref%E5%9C%A8-react-%E4%B8%AD%E6%8C%81%E4%B9%85%E5%8C%96-worker-%E5%AE%9E%E4%BE%8B&quot;&gt;useRef：在 React 中持久化 Worker 实例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671100905954705444#%E4%B8%89%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6%E4%B8%BB%E7%BA%BF%E7%A8%8B%E4%B8%8E-worker-%E7%9A%84%E5%8F%8C%E5%90%91%E9%80%9A%E4%BF%A1&quot; title=&quot;#%E4%B8%89%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6%E4%B8%BB%E7%BA%BF%E7%A8%8B%E4%B8%8E-worker-%E7%9A%84%E5%8F%8C%E5%90%91%E9%80%9A%E4%BF%A1&quot;&gt;消息机制：主线程与 Worker 的双向通信&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671100905954705444#%E5%9B%9B%E5%AE%8C%E6%95%B4%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%88%9D%E5%A7%8B%E5%8C%96%E9%80%9A%E4%BF%A1%E9%94%80%E6%AF%81&quot; title=&quot;#%E5%9B%9B%E5%AE%8C%E6%95%B4%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%88%9D%E5%A7%8B%E5%8C%96%E9%80%9A%E4%BF%A1%E9%94%80%E6%AF%81&quot;&gt;完整生命周期：初始化、通信、销毁&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671100905954705444#%E4%BA%94%E6%80%BB%E7%BB%93&quot; title=&quot;#%E4%BA%94%E6%80%BB%E7%BB%93&quot;&gt;总结&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-1&quot;&gt;一、Web Worker 解决什么？&lt;/h2&gt;
&lt;p&gt;JS 主线程调用栈里，一段 &lt;code&gt;for (let i = 0; i &amp;lt; 50000000; i++)&lt;/code&gt; 的循环会&lt;strong&gt;阻塞所有 UI 交互&lt;/strong&gt;——用户的点击、输入、滚动全部卡死，直到循环结束。这不是 JS 写得不好，而是 JS 单线程 Event Loop 的物理限制：主线程既要管 DOM 渲染，又要管用户交互，现在还要管五千万次计算——它忙不过来。&lt;/p&gt;
&lt;p&gt;Web Worker 是浏览器提供的方案：&lt;strong&gt;在主线程之外开辟一个独立的后台线程，专门跑纯计算任务。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/77c0736c2cbd4f0cb97de0c21491a08a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYm9uZWNoaXBz:q75.awebp?rk3s=f64ab15b&amp;amp;x-expires=1786719665&amp;amp;x-signature=9x7cmyYUB%2F1PQsfWy5NSnUfqTrM%3D&quot; alt=&quot;image.png&quot; loading=&quot;lazy&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;Web Worker 有严格的限制：&lt;strong&gt;不能访问 DOM&lt;/strong&gt;、不能操作 &lt;code&gt;window&lt;/code&gt; 对象、只能通过消息机制和主线程通信。它本质上是浏览器（C++ 多线程）提供给 JS 的辅助线程——JS 本身的单线程机制并未改变，页面渲染和组件更新仍然只能在主线程上运行。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-2&quot;&gt;二、useRef：在 React 中持久化 Worker 实例&lt;/h2&gt;
&lt;p&gt;Worker 的创建成本很高——每次 &lt;code&gt;new Worker()&lt;/code&gt; 都会启动一个独立线程。在 React 组件中，如果直接 &lt;code&gt;useState&lt;/code&gt; 存放 Worker 实例，组件每次重渲染都会丢失引用并重新创建。&lt;code&gt;useRef&lt;/code&gt; 是正确选择：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; workerRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;);  &lt;span class=&quot;hljs-comment&quot;&gt;// 组件重渲染不会重置这个值&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Hook&lt;/th&gt;&lt;th&gt;重渲染时&lt;/th&gt;&lt;th&gt;适合存放&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/td&gt;&lt;td&gt;保持不变（但修改会触发重渲染）&lt;/td&gt;&lt;td&gt;驱动 UI 的状态&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;useRef&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;current&lt;/code&gt; 属性值保持不变&lt;/td&gt;&lt;td&gt;不需要触发渲染的持久引用（计时器、Worker、DOM 节点）&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;useRef&lt;/code&gt; 返回一个 &lt;code&gt;{ current: null }&lt;/code&gt; 对象，这个对象在整个组件生命周期中只有一份，修改 &lt;code&gt;current&lt;/code&gt; 不会触发重渲染——完美适合存放 Worker 实例。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-3&quot;&gt;三、消息机制：主线程与 Worker 的双向通信&lt;/h2&gt;
&lt;p&gt;主线程和 Worker 线程是隔离的——不能共享变量，不能互相调用函数。通信只能靠&lt;strong&gt;消息机制&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-4&quot;&gt;主线程发、Worker 收&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 主线程：发任务&lt;/span&gt;
workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;num&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;88&lt;/span&gt; });
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// Worker 线程：收任务&lt;/span&gt;
self.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { num } = e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; sum = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;
    &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;hljs-number&quot;&gt;50000000&lt;/span&gt;; i++) {
        sum += num * i;
    }
    &lt;span class=&quot;hljs-comment&quot;&gt;// 算完发回去&lt;/span&gt;
    self.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;result&lt;/span&gt;: sum });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-5&quot;&gt;Worker 发、主线程收&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 主线程：收结果&lt;/span&gt;
workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { result } = e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;;
    &lt;span class=&quot;hljs-title function_&quot;&gt;setResult&lt;/span&gt;(result);     &lt;span class=&quot;hljs-comment&quot;&gt;// 更新 UI&lt;/span&gt;
    &lt;span class=&quot;hljs-title function_&quot;&gt;setLoading&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;);     &lt;span class=&quot;hljs-comment&quot;&gt;// 关闭 loading&lt;/span&gt;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/15a48ac990684607967b15f2236a70a0~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgYm9uZWNoaXBz:q75.awebp?rk3s=f64ab15b&amp;amp;x-expires=1786719665&amp;amp;x-signature=sVmE8APKM%2BacDCYKiTOu1trPSXw%3D&quot; alt=&quot;image.png&quot; loading=&quot;lazy&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;p&gt;整个计算过程中，主线程的 DOM 渲染、用户交互完全不受影响——五千万次循环在 Worker 线程里跑，主线程只负责&quot;发任务&quot;和&quot;收结果&quot;两件事。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-6&quot;&gt;四、完整生命周期：初始化、通信、销毁&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; workerRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;);
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [result, setResult] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;);
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [loading, setLoading] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;);

    &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
        &lt;span class=&quot;hljs-comment&quot;&gt;// 挂载：创建 Worker&lt;/span&gt;
        workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(
            &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;URL&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;./worker.js&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;meta&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;url&lt;/span&gt;)
        );
        &lt;span class=&quot;hljs-comment&quot;&gt;// 监听 Worker 结果&lt;/span&gt;
        workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
            &lt;span class=&quot;hljs-title function_&quot;&gt;setResult&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;result&lt;/span&gt;);
            &lt;span class=&quot;hljs-title function_&quot;&gt;setLoading&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;);
        };

        &lt;span class=&quot;hljs-comment&quot;&gt;// 卸载：销毁 Worker&lt;/span&gt;
        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
            workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;terminate&lt;/span&gt;();
            workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;
        };
    }, []);  &lt;span class=&quot;hljs-comment&quot;&gt;// 只在挂载时执行一次&lt;/span&gt;

    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;startHeavyCalc&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
        &lt;span class=&quot;hljs-title function_&quot;&gt;setLoading&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;);
        workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;num&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;88&lt;/span&gt; });
    };

    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
        &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;style&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{{&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;padding:&lt;/span&gt; &#39;&lt;span class=&quot;hljs-attr&quot;&gt;30px&lt;/span&gt;&#39; }}&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;h2&lt;/span&gt;&amp;gt;&lt;/span&gt;useRef + WebWorker 耗时运算&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;h2&lt;/span&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;
                &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{startHeavyCalc}&lt;/span&gt;
                &lt;span class=&quot;hljs-attr&quot;&gt;disabled&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{loading}&lt;/span&gt;
            &amp;gt;&lt;/span&gt;
                {loading ? &quot;正在后台计算...&quot; : &quot;启动繁重计算任务&quot;}
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
            {result &amp;amp;&amp;amp; &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;h3&lt;/span&gt;&amp;gt;&lt;/span&gt;计算结果：{result}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;h3&lt;/span&gt;&amp;gt;&lt;/span&gt;}
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-7&quot;&gt;生命周期四个阶段&lt;/h3&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;阶段&lt;/th&gt;&lt;th&gt;代码&lt;/th&gt;&lt;th&gt;做了什么&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;挂载&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;useEffect(() =&amp;gt; {...}, [])&lt;/code&gt;&lt;/td&gt;&lt;td&gt;创建 Worker，绑定 onmessage&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;通信&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;postMessage&lt;/code&gt; + &lt;code&gt;onmessage&lt;/code&gt;&lt;/td&gt;&lt;td&gt;主线程发任务，Worker 算完发回结果&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;更新&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;setResult&lt;/code&gt; + &lt;code&gt;setLoading&lt;/code&gt;&lt;/td&gt;&lt;td&gt;收到结果后更新 UI 状态&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;卸载&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;return () =&amp;gt; { terminate() }&lt;/code&gt;&lt;/td&gt;&lt;td&gt;销毁 Worker 线程，回收内存&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;useEffect&lt;/code&gt; 的 &lt;code&gt;return&lt;/code&gt; 是清理函数——组件卸载时自动调用。&lt;code&gt;workerRef.current.terminate()&lt;/code&gt; 关闭 Worker 线程，&lt;code&gt;workerRef.current = null&lt;/code&gt; 手动回收内存引用。如果不做清理，组件卸载后 Worker 线程仍然在后台运行，造成资源泄漏。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-8&quot;&gt;状态流转&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-mermaid&quot; lang=&quot;mermaid&quot;&gt;flowchart LR
    A[初始 state&amp;lt;br/&amp;gt;loading false&amp;lt;br/&amp;gt;result null] --&amp;gt; B[点击按钮&amp;lt;br/&amp;gt;setLoading true]
    B --&amp;gt; C[postMessage 发任务]
    C --&amp;gt; D[Worker 计算中&amp;lt;br/&amp;gt;主线程不阻塞]
    D --&amp;gt; E[Worker 返回结果&amp;lt;br/&amp;gt;onmessage 触发]
    E --&amp;gt; F[setResult 显示结果&amp;lt;br/&amp;gt;setLoading false&amp;lt;br/&amp;gt;按钮恢复可点击]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;disabled={loading}&lt;/code&gt; 在计算期间禁用按钮防止重复提交。&quot;正在后台计算...&quot; 文案让用户知道任务在跑，不是页面卡死了。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-9&quot;&gt;五、总结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Web Worker 是浏览器的多线程能力&lt;/strong&gt;：主线程跑 UI，Worker 线程跑计算。Worker 不能访问 DOM，只能纯计算，通过消息机制通信。JS 仍然是单线程语言。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;useRef 持久化 Worker&lt;/strong&gt;：&lt;code&gt;workerRef.current&lt;/code&gt; 在组件重渲染时不会被重置，适合存放非响应式的持久引用。修改 &lt;code&gt;current&lt;/code&gt; 不触发重渲染。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消息机制是唯一的通信方式&lt;/strong&gt;：主线程 &lt;code&gt;postMessage&lt;/code&gt; 发任务 → Worker &lt;code&gt;self.onmessage&lt;/code&gt; 收 → 计算 → &lt;code&gt;self.postMessage&lt;/code&gt; 返回结果 → 主线程 &lt;code&gt;onmessage&lt;/code&gt; 收。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;useEffect 管生命周期&lt;/strong&gt;：挂载时 &lt;code&gt;new Worker()&lt;/code&gt;，卸载时 &lt;code&gt;terminate()&lt;/code&gt; + 手动置空。不做清理会造成资源泄漏。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;loading 状态防重复提交&lt;/strong&gt;：&lt;code&gt;disabled={loading}&lt;/code&gt; 禁按钮，文案切换提示后台计算中。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;—— 主线程是前台，Worker 是后厨。你点菜就走，做好了铃响再取。&lt;/em&gt;&lt;/p&gt;</description><link>https://juejin.cn/post/7671100905954705444</link><guid isPermaLink="false">https://juejin.cn/post/7671100905954705444</guid><pubDate>Fri, 07 Aug 2026 15:01:05 GMT</pubDate><author>bonechips</author><category>前端</category><category>JavaScript</category></item><item><title>声明式与命令式的边界：从鉴权路由守卫到 useRef 的引用哲学</title><description>&lt;h3 data-id=&quot;heading-0&quot;&gt;声明式与命令式的边界：从鉴权路由守卫到 useRef 的引用哲学&lt;/h3&gt;
&lt;p&gt;React 帮你托管了两件大事：数据用 &lt;code&gt;useState&lt;/code&gt; 做响应式渲染，页面用路由随 URL 切换组件。写业务的时候，我们大多数时候只需要“声明式”地描述想要什么，剩下的交给框架。&lt;/p&gt;
&lt;p&gt;但真实应用里，总有两件事框架只能帮你到这里，必须自己出手：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;访问控制&lt;/strong&gt;：支付页不能随便进，得有一道“门禁”，登录过才放行——这就是&lt;strong&gt;鉴权路由守卫&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;直接引用&lt;/strong&gt;：偶尔就是要拿到 DOM 节点、一个可变的值、甚至一条 Worker 线程——这就是 &lt;strong&gt;&lt;code&gt;useRef&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两件事看似无关，其实共用同一条分界线：&lt;strong&gt;&lt;code&gt;useState&lt;/code&gt; 与路由负责“声明式地描述”，守卫与 &lt;code&gt;useRef&lt;/code&gt; 负责“命令式地出手”。&lt;/strong&gt; 理解这条边界，才算真正理解 React 的框架哲学。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-1&quot;&gt;一、HTTP 无状态：鉴权到底在解决什么问题&lt;/h2&gt;
&lt;p&gt;先问一个问题：&lt;strong&gt;为什么需要登录？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为 HTTP 是**无状态（Stateless）**的。每一次请求之间互相独立，服务器默认不记得你是谁、上次说了什么。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-lua&quot; lang=&quot;lua&quot;&gt;客户端                 服务器
  |&lt;span class=&quot;hljs-comment&quot;&gt;---- 请求 1 --------&amp;gt;|  不记得你是谁&lt;/span&gt;
  |&lt;span class=&quot;hljs-comment&quot;&gt;---- 请求 2 --------&amp;gt;|  还是不记得&lt;/span&gt;
  |&lt;span class=&quot;hljs-comment&quot;&gt;---- 请求 3 --------&amp;gt;|  依然不记得&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可业务需要“有状态”——购物车、登录态、支付权限。前端要补上这个记忆，常见有三种手段：&lt;/p&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;手段&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;说明&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;请求头 Token / Authorization&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;登录后服务器签发 Token，每次请求带在请求头里&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Cookie&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;浏览器自动携带，服务器种下会话标识&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;localStorage&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;前端本地存储，存一个“登录状态”标志&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;SPA 里最朴素、也最常用的方式就是最后一种。登录成功后存一个标志：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-variable language_&quot;&gt;localStorage&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;setItem&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;isLogin&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&#39;true&#39;&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以后每次判断“能不能进”，就是查这个标志。&lt;strong&gt;前端鉴权的本质：根据已知的状态，决定放不放行。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-2&quot;&gt;二、路由守卫：ProtectRoute 门禁组件&lt;/h2&gt;
&lt;p&gt;现在有这样一个需求：&lt;code&gt;/pay&lt;/code&gt; 支付页，必须登录后才能访问。用户直接敲 URL、或没登录就点导航，都应该被拦下来送到登录页。&lt;/p&gt;
&lt;p&gt;React 的做法是把“要保护的页面”包进一个门禁组件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-title class_&quot;&gt;Route&lt;/span&gt; path=&lt;span class=&quot;hljs-string&quot;&gt;&quot;/pay&quot;&lt;/span&gt; element={
  &lt;span class=&quot;hljs-comment&quot;&gt;// 门禁保安&lt;/span&gt;
  &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ProtectRoute&lt;/span&gt;&amp;gt;&lt;/span&gt;
    {/* children：Pay 就是要进的房间 */}
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Pay&lt;/span&gt; /&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ProtectRoute&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
} /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ProtectRoute&lt;/code&gt; 这个“保安”负责两件事：&lt;strong&gt;检查状态、决定放行还是拦截。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;Navigate&lt;/span&gt;, useLocation } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react-router-dom&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;ProtectRoute&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;{ children }&lt;/span&gt;) {
  &lt;span class=&quot;hljs-comment&quot;&gt;// 拦截请求 鉴权&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// HTML5 本地存储 域名沙盒&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; isAuth = &lt;span class=&quot;hljs-variable language_&quot;&gt;localStorage&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;getItem&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;isLogin&#39;&lt;/span&gt;) === &lt;span class=&quot;hljs-string&quot;&gt;&#39;true&#39;&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// useLocation 返回 React Router 的 location（纯对象，可序列化）&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; location = &lt;span class=&quot;hljs-title function_&quot;&gt;useLocation&lt;/span&gt;()

  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (!isAuth) {
    &lt;span class=&quot;hljs-comment&quot;&gt;// 未登录：跳转登录页，并记住是从哪来的&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// replace：替换当前 /pay 历史，避免登录后还能后退回登录页&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Navigate&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;to&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;/login&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;state&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{{&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;from:&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;location&lt;/span&gt; }} &lt;span class=&quot;hljs-attr&quot;&gt;replace&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
  }

  &lt;span class=&quot;hljs-comment&quot;&gt;// 已登录：放行，渲染房间里的内容&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;{children}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拆开看它做的三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;查状态&lt;/strong&gt;：&lt;code&gt;localStorage.getItem(&#39;isLogin&#39;) === &#39;true&#39;&lt;/code&gt;——有状态才放行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;记来源&lt;/strong&gt;：&lt;code&gt;state={{ from: location }}&lt;/code&gt;——把“我是从 &lt;code&gt;/pay&lt;/code&gt; 来的”这个信息带过去，登录成功后好把人送回原处。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;声明式拦截&lt;/strong&gt;：未登录直接返回 &lt;code&gt;&amp;lt;Navigate to=&quot;/login&quot;&amp;gt;&lt;/code&gt;，由 React Router 完成跳转。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;为什么用 &lt;code&gt;useLocation&lt;/code&gt;，而不是 &lt;code&gt;window.location&lt;/code&gt;？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;注释里写得很清楚：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;window.location&lt;/code&gt; 含原型/方法，&lt;code&gt;pushState&lt;/code&gt; 无法克隆，会报 &lt;code&gt;DataCloneError&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;window.location&lt;/code&gt; 是浏览器原生的全局对象，带着一堆方法和原型链，不能序列化传给路由的 &lt;code&gt;state&lt;/code&gt;；而 &lt;code&gt;useLocation()&lt;/code&gt; 返回的是 React Router 维护的纯对象，安全可序列化。&lt;strong&gt;凡是准备跨路由传递的信息，都要用 React Router 的对象，而不是浏览器原生对象。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-3&quot;&gt;三、props.children：让组件能“填空”&lt;/h2&gt;
&lt;p&gt;注意对 &lt;code&gt;ProtectRoute&lt;/code&gt; 的用法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-title class_&quot;&gt;ProtectRoute&lt;/span&gt;&amp;gt;
  &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Pay&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&amp;lt;/&lt;span class=&quot;hljs-title class_&quot;&gt;ProtectRoute&lt;/span&gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;被保护的页面写在了组件标签的&lt;strong&gt;内部&lt;/strong&gt;。这个内部内容，组件通过 &lt;code&gt;props.children&lt;/code&gt; 就能拿到：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;ProtectRoute&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;{ children }&lt;/span&gt;) {
  &lt;span class=&quot;hljs-comment&quot;&gt;// ...&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;{children}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;   &lt;span class=&quot;hljs-comment&quot;&gt;// children 就是 &amp;lt;Pay /&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 React 组合能力的核心：&lt;strong&gt;外壳组件负责“布局和逻辑”，内部内容由使用者决定。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;同样的思想在弹窗组件里最典型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-title class_&quot;&gt;Modal&lt;/span&gt;&amp;gt;
  {&lt;span class=&quot;hljs-comment&quot;&gt;/* children 定制 */&lt;/span&gt;}
  &amp;lt;p&amp;gt;确定要删除这条记录吗？&amp;lt;/p&amp;gt;
&amp;lt;/&lt;span class=&quot;hljs-title class_&quot;&gt;Modal&lt;/span&gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;弹窗的蒙层（Mask）、窗体、头部、尾部都是固定的外壳，唯独“主体内容”通过 &lt;code&gt;children&lt;/code&gt; 传进来——同一个 &lt;code&gt;Modal&lt;/code&gt; 组件，可以塞表单、塞确认文案、塞任何东西，而不用为每种场景单独写一个弹窗。&lt;strong&gt;定制性，是 &lt;code&gt;children&lt;/code&gt; 存在的原因。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-4&quot;&gt;四、登录页：useNavigate 命令式跳转 + 表单&lt;/h2&gt;
&lt;p&gt;被拦住的用户会被送到 &lt;code&gt;/login&lt;/code&gt;。登录页要做的事：收集账号密码、校验、写状态、回跳原页面。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useNavigate, useLocation } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react-router-dom&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Login&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; navigate = &lt;span class=&quot;hljs-title function_&quot;&gt;useNavigate&lt;/span&gt;()
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; location = &lt;span class=&quot;hljs-title function_&quot;&gt;useLocation&lt;/span&gt;()

  &lt;span class=&quot;hljs-comment&quot;&gt;// /login 直接访问时 from 为空&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// 从 /pay 被拦过来时 from 是 { pathname: &#39;/pay&#39; }&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// 可选链运算符（ES11）&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; = location.&lt;span class=&quot;hljs-property&quot;&gt;state&lt;/span&gt;?.&lt;span class=&quot;hljs-property&quot;&gt;from&lt;/span&gt;?.&lt;span class=&quot;hljs-property&quot;&gt;pathname&lt;/span&gt; || &lt;span class=&quot;hljs-string&quot;&gt;&#39;/&#39;&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;handleSubmit&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) {
    e.&lt;span class=&quot;hljs-title function_&quot;&gt;preventDefault&lt;/span&gt;() &lt;span class=&quot;hljs-comment&quot;&gt;// 阻止表单默认提交行为&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// 原生表单数据对象：按 name 取值&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; formData = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;FormData&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;currentTarget&lt;/span&gt;)
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; username = formData.&lt;span class=&quot;hljs-title function_&quot;&gt;get&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;username&#39;&lt;/span&gt;)
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; password = formData.&lt;span class=&quot;hljs-title function_&quot;&gt;get&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;password&#39;&lt;/span&gt;)

    &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (username === &lt;span class=&quot;hljs-string&quot;&gt;&#39;admin&#39;&lt;/span&gt; &amp;amp;&amp;amp; password === &lt;span class=&quot;hljs-string&quot;&gt;&#39;123456&#39;&lt;/span&gt;) {
      &lt;span class=&quot;hljs-variable language_&quot;&gt;localStorage&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;setItem&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;isLogin&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&#39;true&#39;&lt;/span&gt;)
      &lt;span class=&quot;hljs-comment&quot;&gt;// 登录成功，回跳原页面（replace 替换历史，防后退）&lt;/span&gt;
      &lt;span class=&quot;hljs-title function_&quot;&gt;navigate&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt;, { &lt;span class=&quot;hljs-attr&quot;&gt;replace&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; })
    } &lt;span class=&quot;hljs-keyword&quot;&gt;else&lt;/span&gt; {
      &lt;span class=&quot;hljs-title function_&quot;&gt;alert&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;用户名或密码错误&#39;&lt;/span&gt;)
    }
  }

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onSubmit&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{handleSubmit}&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;h1&lt;/span&gt;&amp;gt;&lt;/span&gt;登录&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;h1&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;username&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;placeholder&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;请输入用户名&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;required&lt;/span&gt; /&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;password&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;placeholder&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;请输入密码&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;required&lt;/span&gt; /&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;submit&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;登录&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;form&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;几个值得学的点：&lt;/strong&gt;&lt;/p&gt;
&lt;h3 data-id=&quot;heading-5&quot;&gt;4.1 &lt;code&gt;e.preventDefault()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;表单不阻止默认行为，点击提交会刷新整个页面——SPA 里这是大忌。阻止之后，才轮到我们自己用 JS 处理。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-6&quot;&gt;4.2 &lt;code&gt;FormData&lt;/code&gt; 取表单值&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; formData = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;FormData&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;currentTarget&lt;/span&gt;)
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; username = formData.&lt;span class=&quot;hljs-title function_&quot;&gt;get&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;username&#39;&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不用给每个输入框绑 &lt;code&gt;value&lt;/code&gt; 和 &lt;code&gt;onChange&lt;/code&gt;，原生 &lt;code&gt;FormData&lt;/code&gt; 按 &lt;code&gt;name&lt;/code&gt; 属性就能拿到全部字段，表单控件多时特别省事。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-7&quot;&gt;4.3 回跳原页面&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; = location.&lt;span class=&quot;hljs-property&quot;&gt;state&lt;/span&gt;?.&lt;span class=&quot;hljs-property&quot;&gt;from&lt;/span&gt;?.&lt;span class=&quot;hljs-property&quot;&gt;pathname&lt;/span&gt; || &lt;span class=&quot;hljs-string&quot;&gt;&#39;/&#39;&lt;/span&gt;
&lt;span class=&quot;hljs-title function_&quot;&gt;navigate&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt;, { &lt;span class=&quot;hljs-attr&quot;&gt;replace&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;location.state&lt;/code&gt; 是 &lt;code&gt;ProtectRoute&lt;/code&gt; 传过来的 &lt;code&gt;{ from: location }&lt;/code&gt;，取出 &lt;code&gt;pathname&lt;/code&gt; 就是来时的地址。&lt;code&gt;?.&lt;/code&gt; 可选链（ES11）保证 &lt;code&gt;state&lt;/code&gt; 为空时不会报错，直接兜底回首页 &lt;code&gt;/&lt;/code&gt;。&lt;strong&gt;用户从哪来，登录完就回哪去。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 data-id=&quot;heading-8&quot;&gt;4.4 为什么要 &lt;code&gt;replace&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;见第六节详述。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-9&quot;&gt;五、Navigate vs useNavigate：声明式与命令式&lt;/h2&gt;
&lt;p&gt;同一天里我们见到了两种跳转方式，很多人会搞混，这里直接对比：&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;维度&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;&lt;code&gt;Navigate&lt;/code&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;&lt;code&gt;useNavigate&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;类型&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;组件（声明式）&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Hook 返回的函数（命令式）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;写法&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;return &amp;lt;Navigate to=&quot;/login&quot; replace /&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;navigate(&#39;/login&#39;, { replace: true })&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;触发时机&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;渲染时就跳转&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;在事件处理、副作用、条件判断里调用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;典型场景&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;路由守卫：未登录就渲染到登录页&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;登录成功后跳转、点按钮跳转、后退 &lt;code&gt;navigate(-1)&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;ProtectRoute&lt;/code&gt; 里用的是声明式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (!isAuth) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Navigate&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;to&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;/login&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;state&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{{&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;from:&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;location&lt;/span&gt; }} &lt;span class=&quot;hljs-attr&quot;&gt;replace&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;组件一渲染到这一步，重定向立刻发生——它天生适合“守卫”这种场景。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Login&lt;/code&gt; 里用的是命令式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-title function_&quot;&gt;navigate&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt;, { &lt;span class=&quot;hljs-attr&quot;&gt;replace&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; })   &lt;span class=&quot;hljs-comment&quot;&gt;// 校验通过后才跳转&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;跳转的时机握在自己手里——它天生适合“某个动作发生后”再跳转。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一句话记：守卫用 &lt;code&gt;Navigate&lt;/code&gt;，动作后用 &lt;code&gt;useNavigate&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-10&quot;&gt;六、路由历史与 replace：登录后为什么不能后退&lt;/h2&gt;
&lt;p&gt;浏览器维护一个 &lt;code&gt;history&lt;/code&gt; 栈，前进后退就是在栈上移动：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot; lang=&quot;bash&quot;&gt;初始      /pay 被拦 → /login    登录成功 navigate(from)
栈: [首页, /pay, /login]    →    如果 push：[首页, /pay, /login, /pay]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果登录成功后用普通的 &lt;code&gt;navigate(from)&lt;/code&gt;（&lt;code&gt;push&lt;/code&gt; 一条新记录），会发生什么？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户在登录页按下“后退”&lt;/li&gt;
&lt;li&gt;又回到 &lt;code&gt;/login&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;而 &lt;code&gt;/login&lt;/code&gt; 没有任何守卫，用户一脸懵：“我不是登录了吗？怎么又回来了？”&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以注释里说得很直白：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;把用户当小白。登录成功后，如果还能返回登录页面，用户就会懵。&lt;code&gt;replace&lt;/code&gt; 跳转到新页面的同时，将新页面的历史记录替换掉 &lt;code&gt;/login&lt;/code&gt; 的记录。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-title function_&quot;&gt;navigate&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt;, { &lt;span class=&quot;hljs-attr&quot;&gt;replace&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;replace&lt;/code&gt; 的意思是把当前栈顶（&lt;code&gt;/login&lt;/code&gt;）替换成新地址（&lt;code&gt;/pay&lt;/code&gt;），而不是在上面叠加：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot; lang=&quot;bash&quot;&gt;栈：[首页, /pay]          ← /login 被替换掉了，后退直接回首页
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;登录页不会残留在历史里，用户怎么按后退都回不到它。&lt;strong&gt;凡是“校验通过后不应该再回头”的跳转，都该用 &lt;code&gt;replace&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个 &lt;code&gt;replace&lt;/code&gt; 的思想，正是 &lt;code&gt;Link&lt;/code&gt; 组件第二属性的来源——&lt;code&gt;Link&lt;/code&gt; 的 &lt;code&gt;to&lt;/code&gt; 决定去哪，&lt;code&gt;replace&lt;/code&gt; 决定是否替换历史：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-title class_&quot;&gt;Link&lt;/span&gt; to=&lt;span class=&quot;hljs-string&quot;&gt;&quot;/pay&quot;&lt;/span&gt; replace&amp;gt;支付&amp;lt;/&lt;span class=&quot;hljs-title class_&quot;&gt;Link&lt;/span&gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-11&quot;&gt;七、路由对象与两种选型&lt;/h2&gt;
&lt;p&gt;前端路由这条路上，还有几个绕不开的基础概念。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-12&quot;&gt;7.1 路由对象三件套&lt;/h3&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;对象&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;角色&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;navigate&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;导航栏——编程式发起跳转&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;location&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;地址栏——当前地址、参数、state&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;history&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;历史记录——前进、后退的栈&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 data-id=&quot;heading-13&quot;&gt;7.2 路由两种选型&lt;/h3&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;维度&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;&lt;code&gt;HashRouter&lt;/code&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;&lt;code&gt;BrowserRouter&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;原理&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;URL 的 hash（&lt;code&gt;#&lt;/code&gt; 部分）局部改变，不触发页面刷新&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;不用 hash，用 History API 实现 SPA&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;地址长这样&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/pay&lt;/code&gt; → &lt;code&gt;#/pay&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;/pay&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;缺点&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;URL 有点丑，带着 &lt;code&gt;#&lt;/code&gt;，和后端路由不太一样&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;刷新/直达时需要服务端配合（否则 404）&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot; lang=&quot;bash&quot;&gt;HashRouter:    http://site.com/&lt;span class=&quot;hljs-comment&quot;&gt;#/pay&lt;/span&gt;
BrowserRouter: http://site.com/pay
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Demo 里用的是 &lt;code&gt;HashRouter&lt;/code&gt;——实现简单、无需服务端配置，作为学习演示刚刚好。真实项目选型时再根据部署环境权衡。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-14&quot;&gt;八、useRef：为什么需要“引用”&lt;/h2&gt;
&lt;p&gt;路由讲完了，切换到 React 的另一条主线：Hooks。&lt;/p&gt;
&lt;p&gt;先回顾 &lt;code&gt;useState&lt;/code&gt; 带来的变化。React 之前，前端是“原生 JS 做 DOM 编程”：拿到节点、改属性、插子节点，全手动。而 React/Vue 这类框架直接规避了 DOM 编程——你声明数据，框架帮你更新界面。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;JS 在 V8 引擎执行，DOM 在渲染引擎里。跨引擎直接操作 DOM → 非常耗费性能。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;useState&lt;/code&gt; 数据绑定 + 响应式编程 → 前端开发方式直接改变。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那“如果非要去 DOM 呢”？笔记里有一问一答：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不是不可以做 DOM 编程，而是交给 React。如果必须，DOM 用 &lt;code&gt;useRef&lt;/code&gt; 来拿。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;还有别的需求：想在组件里存一个&lt;strong&gt;可变但不影响渲染的值&lt;/strong&gt;；想引用一条 &lt;strong&gt;Worker 线程&lt;/strong&gt;。这些都是 &lt;code&gt;useState&lt;/code&gt; 管不了的——因为它们不是“业务状态”，不该触发重新渲染。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-15&quot;&gt;九、useRef 是什么&lt;/h2&gt;
&lt;p&gt;先记定义：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;useRef&lt;/code&gt; 是 React 提供的、返回持久可变对象的 Hook 函数，经常用来引用 DOM 节点对象。它有一个 &lt;code&gt;current&lt;/code&gt; 属性，可以指向任何值或对象，&lt;strong&gt;不会触发渲染&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最小用法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; inputRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;)
&lt;span class=&quot;hljs-comment&quot;&gt;// inputRef 是 { current: null }&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 赋值后：inputRef.current = &amp;lt;某个对象&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它和 &lt;code&gt;useState&lt;/code&gt; 的异同：&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;维度&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;&lt;code&gt;useRef&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;都能改变&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✅ 都能存一个值并更新&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;✅ 都能通过 &lt;code&gt;.current&lt;/code&gt; 更新&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;定位&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;聚焦数据业务状态&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;聚焦 DOM 对象引用、可变对象等&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;改变后&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;触发组件重新渲染&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;不触发渲染&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;典型用途&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;计数、表单、接口数据&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;绑定 DOM、存定时器/线程、跨渲染缓存值&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;一句话记住分工：&lt;code&gt;useState&lt;/code&gt; 管“数据”，&lt;code&gt;useRef&lt;/code&gt; 管“引用”。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;数据变了要重新画界面，所以它必须触发渲染；引用变了界面不用变，所以它保持安静。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-16&quot;&gt;十、绑定 DOM：自动聚焦&lt;/h2&gt;
&lt;p&gt;最经典的使用场景：组件挂载后自动聚焦到输入框。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useRef, useEffect, useState } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [count, setCount] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)
  &lt;span class=&quot;hljs-comment&quot;&gt;// ref 对象引用&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; inputRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;)

  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-comment&quot;&gt;// 组件挂载完成后，自动聚焦到输入框&lt;/span&gt;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(inputRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;)   &lt;span class=&quot;hljs-comment&quot;&gt;// 打印出来就是那个 DOM 节点对象&lt;/span&gt;
    inputRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;focus&lt;/span&gt;()
  }, [])

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      {/* DOM 节点对象 */}
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;text&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;placeholder&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;请输入用户名&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;ref&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{inputRef}&lt;/span&gt; /&amp;gt;&lt;/span&gt;
      {count}
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{()&lt;/span&gt; =&amp;gt;&lt;/span&gt; setCount(count + 1)}&amp;gt;增加&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三步就走完了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;const inputRef = useRef(null)&lt;/code&gt;——先造一个 &lt;code&gt;{ current: null }&lt;/code&gt; 的引用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;input ref={inputRef}&amp;gt;&lt;/code&gt;——JSX 的 &lt;code&gt;ref&lt;/code&gt; 属性把这个引用绑定到 DOM 节点，挂载后 &lt;code&gt;inputRef.current&lt;/code&gt; 就指向那个 &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; 节点对象。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useEffect&lt;/code&gt; 里 &lt;code&gt;inputRef.current.focus()&lt;/code&gt;——组件挂载完成后调用原生 DOM 方法。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;对比一下原生方案里的 &lt;code&gt;autoFocus&lt;/code&gt; 属性——&lt;/strong&gt; 它能满足“默认聚焦”，但只能做“聚焦”这一件预设好的事。&lt;code&gt;useRef&lt;/code&gt; 给的是&lt;strong&gt;完整的 DOM 节点对象&lt;/strong&gt;，聚焦、取值、量尺寸……想干什么都行。属性只能开现成的口子，引用能拿到整把钥匙。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-17&quot;&gt;十一、引用一个值：可变，但不触发渲染&lt;/h2&gt;
&lt;p&gt;第二个场景：存一个会变、但不希望在每次变化时重绘的值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; numRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)      &lt;span class=&quot;hljs-comment&quot;&gt;// 引用一个值&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [, forceRender] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)  &lt;span class=&quot;hljs-comment&quot;&gt;// 响应式&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{()&lt;/span&gt; =&amp;gt;&lt;/span&gt; { 
        numRef.current++         // 值变了，但页面不重绘
        forceRender()            // 手动触发重绘
      }}&amp;gt;
        {numRef.current}
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里的组合拳：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;numRef.current++&lt;/code&gt;——值确实变了，但页面纹丝不动，因为 &lt;code&gt;useRef&lt;/code&gt; 不触发渲染。&lt;/li&gt;
&lt;li&gt;想让页面更新，得手动调 &lt;code&gt;forceRender()&lt;/code&gt; 用 &lt;code&gt;useState&lt;/code&gt; 强刷一次。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这恰恰证明了前面那句结论：&lt;strong&gt;&lt;code&gt;useRef&lt;/code&gt; 可改变、可持久，但不响应式。&lt;/strong&gt; 想让它出现在界面上，必须借助 &lt;code&gt;useState&lt;/code&gt; 的渲染机制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;什么时候需要这种“变了也不重绘”的值？&lt;/strong&gt; 比如一个不断累计的临时计数器、一个不想引起大范围重新渲染的缓存中间值。&lt;code&gt;useState&lt;/code&gt; 是“声明状态”，&lt;code&gt;useRef&lt;/code&gt; 是“持有引用”——两者的触达方式完全不同。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-18&quot;&gt;十二、useRef 与 Worker 线程&lt;/h2&gt;
&lt;p&gt;第三个场景最有意思：引用一条 Worker 线程。要说清楚它，得先回到 JavaScript 的单线程模型。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-19&quot;&gt;12.1 为什么 JS 是单线程&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;做一些前端交互、脚本工作，简单，显示和操作的页面，需要一致性，不能出问题。JS 如果是多线程可能会有冲突。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;同一个页面上，多个线程同时读写同一个 DOM，结果不可预测。所以浏览器规定 JS 主线程是单线程的。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-20&quot;&gt;12.2 Event Loop：异步无阻塞&lt;/h3&gt;
&lt;p&gt;但页面会越来越复杂，有很多耗时任务。单线程意味着：一个大任务卡在主线程上，用户滚动、点击全被阻塞。解法是 Event Loop：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Event Loop JS 执行机制——异步无阻塞。前端要尽快去响应用户交互（滚动屏幕、点击...），不要卡在这里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主线程&lt;/strong&gt;：用户交互（滚动、点击）→ 必须立刻响应。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;耗时任务&lt;/strong&gt;：交给 Event Loop 异步排队，不阻塞交互。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-id=&quot;heading-21&quot;&gt;12.3 计算密集型的出路：Worker 线程&lt;/h3&gt;
&lt;p&gt;异步能解决“等待”（网络请求、定时器），但解决不了&lt;strong&gt;真正耗 CPU 的计算&lt;/strong&gt;——比如 LLM 游戏、非界面的页面逻辑，非常耗费计算时间，Event Loop 异步搞不定。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用 Worker 线程：接下更耗时、复杂的任务。浏览器独立开辟的内存，进行复杂计算，完成后告知主线程（消息机制）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-lua&quot; lang=&quot;lua&quot;&gt;主线程                 Worker 线程
  |&lt;span class=&quot;hljs-comment&quot;&gt;---- new Worker() ---&amp;gt;|  独立内存，跑复杂计算&lt;/span&gt;
  |  （主线程不卡，继续响应交互）  |
  |&amp;lt;&lt;span class=&quot;hljs-comment&quot;&gt;---- postMessage -----|  算完通过消息机制告知&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Demo 里，Worker 线程的引用就交给了 &lt;code&gt;useRef&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-tsx&quot; lang=&quot;tsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; workerRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;)

  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-comment&quot;&gt;// 组件挂载完成后，创建 Worker 线程，开销比较大的操作&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// ref 引用了 Worker 线程，避免了主线程阻塞&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(
      &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;URL&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;./worker.js&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;meta&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;url&lt;/span&gt;)
    )
  }, [])

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么不用 &lt;code&gt;useState&lt;/code&gt; 存 Worker？&lt;/strong&gt; 因为引用一条线程不是“业务状态”——线程实例变了，界面不需要重绘，反而触发渲染只会白白浪费性能。&lt;code&gt;useRef&lt;/code&gt; 恰好是“持有一个引用、但不关心渲染”的工具。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-22&quot;&gt;12.4 三个场景凑齐了 useRef 的完整画像&lt;/h3&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;场景&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;&lt;code&gt;useRef&lt;/code&gt; 存的是什么&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;为什么不用 &lt;code&gt;useState&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;自动聚焦&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;DOM 节点对象&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;拿到节点不等于状态变化&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;可变计数&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;一个值 &lt;code&gt;numRef.current&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;变化时不需要重绘&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Worker 线程&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;线程实例&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;引用改变与 UI 无关&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-23&quot;&gt;十三、面试高频问题与答题框架&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-24&quot;&gt;Q1：前端鉴权路由是怎么实现的？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;回答框架&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;用一个“门禁”组件包住受保护的页面。组件里从 &lt;code&gt;localStorage&lt;/code&gt;（或 Token/Cookie）读登录状态：已登录就渲染 &lt;code&gt;children&lt;/code&gt; 放行；未登录就返回 &lt;code&gt;&amp;lt;Navigate to=&quot;/login&quot; state={{ from: location }} replace /&amp;gt;&lt;/code&gt; 声明式跳转到登录页，并把来源地址记在 &lt;code&gt;state&lt;/code&gt; 里，登录成功后回跳。本质是“根据已知状态决定放不放行”。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-25&quot;&gt;Q2：&lt;code&gt;Navigate&lt;/code&gt; 和 &lt;code&gt;useNavigate&lt;/code&gt; 有什么区别？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;回答框架&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Navigate&lt;/code&gt; 是组件（声明式），渲染时立即重定向，适合路由守卫这种“到了就跳”的场景；&lt;code&gt;useNavigate()&lt;/code&gt; 是 Hook 返回的函数（命令式），在事件处理、副作用、条件判断里手动调用，适合“某个动作发生后”再跳转，比如登录成功后 &lt;code&gt;navigate(from, { replace: true })&lt;/code&gt;。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-26&quot;&gt;Q3：为什么登录成功后要用 &lt;code&gt;replace&lt;/code&gt; 而不是普通跳转？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;回答框架&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;普通跳转会往 &lt;code&gt;history&lt;/code&gt; 栈里 &lt;code&gt;push&lt;/code&gt; 一条新记录，用户按下“后退”就又回到登录页，明明登录过了却像没登录。&lt;code&gt;replace&lt;/code&gt; 把栈顶的 &lt;code&gt;/login&lt;/code&gt; 替换成目标地址，登录页不会残留在历史里，用户怎么后退都回不去。凡是“校验通过后不该再回头”的跳转都用 &lt;code&gt;replace&lt;/code&gt;。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-27&quot;&gt;Q4：&lt;code&gt;useRef&lt;/code&gt; 和 &lt;code&gt;useState&lt;/code&gt; 有什么区别？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;回答框架&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;两者都能存值并更新。但 &lt;code&gt;useState&lt;/code&gt; 聚焦“数据业务状态”，值改变会触发组件重新渲染；&lt;code&gt;useRef&lt;/code&gt; 返回一个持久可变对象，通过 &lt;code&gt;current&lt;/code&gt; 属性指向任意值/DOM/线程，改变时&lt;strong&gt;不触发渲染&lt;/strong&gt;。&lt;code&gt;useState&lt;/code&gt; 管数据，&lt;code&gt;useRef&lt;/code&gt; 管引用。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-28&quot;&gt;Q5：&lt;code&gt;useRef&lt;/code&gt; 为什么改变时不触发渲染？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;回答框架&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;因为 &lt;code&gt;useRef&lt;/code&gt; 的定位是“持有引用”而不是“声明状态”。它存的是 DOM 节点、线程实例这类与 UI 无关的引用，界面不需要因此重绘。如果你想让一个 &lt;code&gt;useRef&lt;/code&gt; 的值出现在界面上，需要借助 &lt;code&gt;useState&lt;/code&gt; 手动触发渲染（比如 &lt;code&gt;forceRender&lt;/code&gt;）。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-29&quot;&gt;Q6：JavaScript 为什么是单线程？复杂计算怎么办？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;回答框架&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;页面交互（滚动、点击、DOM 操作）需要一致性，多线程同时操作 DOM 会有冲突，所以主线程是单线程的。耗时任务通过 Event Loop 异步处理，不阻塞交互；但真正耗 CPU 的计算（如 LLM 推理）连异步也搞不定，就交给 Worker 线程——浏览器独立开辟内存跑复杂计算，完成后通过消息机制（&lt;code&gt;postMessage&lt;/code&gt;）告知主线程。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-30&quot;&gt;结语：声明式与命令式的边界&lt;/h2&gt;
&lt;p&gt;回看这一整天，其实只有一个主线：&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;工具&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;模式&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;职责&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;useState&lt;/code&gt; / 路由&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;声明式&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;描述“想要什么”，框架负责实现&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;鉴权守卫&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;命令式&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;用状态做判断，决定放不放行&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;useRef&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;命令式&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;直接持有 DOM、值、线程的引用&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;React 的哲学是把声明式的部分做到底——你只管说“数据是这样、URL 是那样”，渲染、匹配、切换全是它的。但它把命令式的出口也留好了：鉴权让你在“谁进来”上把住门，&lt;code&gt;useRef&lt;/code&gt; 让你在“直接拿东西”上有抓手。&lt;strong&gt;这不是框架的缺陷，恰恰是它的设计。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;写下一个真实应用前，可以顺手检查：&lt;/p&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled=&quot;&quot;&gt; 受保护的页面是否都包了门禁组件，未登录会被 &lt;code&gt;Navigate&lt;/code&gt; 拦到登录页？&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled=&quot;&quot;&gt; 登录成功是否用 &lt;code&gt;replace&lt;/code&gt; 回跳原页面，而不是让用户能后退回登录页？&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled=&quot;&quot;&gt; 守卫用的是声明式 &lt;code&gt;Navigate&lt;/code&gt;，动作后用命令式 &lt;code&gt;useNavigate&lt;/code&gt;，分清楚了吗？&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled=&quot;&quot;&gt; 需要“跨渲染缓存但不用重绘”的值，用的是 &lt;code&gt;useRef&lt;/code&gt; 而不是 &lt;code&gt;useState&lt;/code&gt;？&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled=&quot;&quot;&gt; 复杂的计算任务是否交给了 Worker 线程，避免卡住主线程交互？&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled=&quot;&quot;&gt; 路由选型（&lt;code&gt;HashRouter&lt;/code&gt; vs &lt;code&gt;BrowserRouter&lt;/code&gt;）是否结合了部署环境？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;框架托管的边界，正是真实应用最关键的地方。把这两处“自己出手”练熟，React 才算真正上手。&lt;/p&gt;</description><link>https://juejin.cn/post/7671153017660391487</link><guid isPermaLink="false">https://juejin.cn/post/7671153017660391487</guid><pubDate>Fri, 07 Aug 2026 14:51:12 GMT</pubDate><author>胡萝卜术</author><category>开发工具</category><category>前端</category><category>JavaScript</category><category>面试</category></item><item><title>🎯 从零彻底搞懂 React Context API —— 一篇带你穿越&quot;组件树&quot;的状态共享方案</title><description>&lt;h2 data-id=&quot;heading-0&quot;&gt;🎯 从零彻底搞懂 React Context API —— 一篇带你穿越&quot;组件树&quot;的状态共享方案&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;学习日志 | React 进阶必修课&lt;/strong&gt;&lt;br&gt;
通过一个完整的 &lt;code&gt;context-demo&lt;/code&gt; 项目，从&quot;为什么需要 Context&quot;到&quot;如何封装自定义 Hook&quot;，从源码逐行解析到架构思维升级，彻底掌握 React 跨层级数据传递。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-1&quot;&gt;📋 目录&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671122376885289000#1-%E5%89%8D%E8%A8%80%E4%BB%8E%E4%B8%80%E4%B8%AA%E7%9C%9F%E5%AE%9E%E7%97%9B%E7%82%B9%E8%AF%B4%E8%B5%B7&quot; title=&quot;#1-%E5%89%8D%E8%A8%80%E4%BB%8E%E4%B8%80%E4%B8%AA%E7%9C%9F%E5%AE%9E%E7%97%9B%E7%82%B9%E8%AF%B4%E8%B5%B7&quot;&gt;前言：从一个真实痛点说起&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671122376885289000#2-%E9%A1%B9%E7%9B%AE%E5%85%A8%E6%99%AFcontext-demo-%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88&quot; title=&quot;#2-%E9%A1%B9%E7%9B%AE%E5%85%A8%E6%99%AFcontext-demo-%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88&quot;&gt;项目全景：context-demo 做了什么&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671122376885289000#3-%E7%AC%AC%E4%B8%80%E5%85%B3%E5%88%9B%E5%BB%BA%E4%B8%8A%E4%B8%8B%E6%96%87--createcontext&quot; title=&quot;#3-%E7%AC%AC%E4%B8%80%E5%85%B3%E5%88%9B%E5%BB%BA%E4%B8%8A%E4%B8%8B%E6%96%87--createcontext&quot;&gt;第一关：创建上下文 —— &lt;code&gt;createContext&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671122376885289000#4-%E7%AC%AC%E4%BA%8C%E5%85%B3%E6%8F%90%E4%BE%9B%E6%95%B0%E6%8D%AE--provider&quot; title=&quot;#4-%E7%AC%AC%E4%BA%8C%E5%85%B3%E6%8F%90%E4%BE%9B%E6%95%B0%E6%8D%AE--provider&quot;&gt;第二关：提供数据 —— &lt;code&gt;Provider&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671122376885289000#5-%E7%AC%AC%E4%B8%89%E5%85%B3%E6%B6%88%E8%B4%B9%E6%95%B0%E6%8D%AE--usecontext&quot; title=&quot;#5-%E7%AC%AC%E4%B8%89%E5%85%B3%E6%B6%88%E8%B4%B9%E6%95%B0%E6%8D%AE--usecontext&quot;&gt;第三关：消费数据 —— &lt;code&gt;useContext&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671122376885289000#6-%E7%AC%AC%E5%9B%9B%E5%85%B3%E5%B0%81%E8%A3%85%E8%87%AA%E5%AE%9A%E4%B9%89-hook--%E6%80%9D%E7%BB%B4%E5%8D%87%E7%BA%A7&quot; title=&quot;#6-%E7%AC%AC%E5%9B%9B%E5%85%B3%E5%B0%81%E8%A3%85%E8%87%AA%E5%AE%9A%E4%B9%89-hook--%E6%80%9D%E7%BB%B4%E5%8D%87%E7%BA%A7&quot;&gt;第四关：封装自定义 Hook —— 思维升级&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671122376885289000#7-%E7%AC%AC%E4%BA%94%E5%85%B3%E5%AE%9E%E6%88%98%E5%BB%B6%E4%BC%B8--usemouse-%E8%87%AA%E5%AE%9A%E4%B9%89-hook&quot; title=&quot;#7-%E7%AC%AC%E4%BA%94%E5%85%B3%E5%AE%9E%E6%88%98%E5%BB%B6%E4%BC%B8--usemouse-%E8%87%AA%E5%AE%9A%E4%B9%89-hook&quot;&gt;第五关：实战延伸 —— &lt;code&gt;useMouse&lt;/code&gt; 自定义 Hook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671122376885289000#8-%E9%A1%B9%E7%9B%AE%E7%BB%84%E4%BB%B6%E6%A0%91%E5%85%A8%E6%99%AF%E5%9B%BE&quot; title=&quot;#8-%E9%A1%B9%E7%9B%AE%E7%BB%84%E4%BB%B6%E6%A0%91%E5%85%A8%E6%99%AF%E5%9B%BE&quot;&gt;项目组件树全景图&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671122376885289000#9-%E5%B8%B8%E8%A7%81%E8%AF%AF%E5%8C%BA%E4%B8%8E%E8%B8%A9%E5%9D%91%E6%8C%87%E5%8D%97&quot; title=&quot;#9-%E5%B8%B8%E8%A7%81%E8%AF%AF%E5%8C%BA%E4%B8%8E%E8%B8%A9%E5%9D%91%E6%8C%87%E5%8D%97&quot;&gt;常见误区与踩坑指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://juejin.cn/post/7671122376885289000#10-%E6%80%BB%E7%BB%93%E4%B8%80%E5%BC%A0%E5%9B%BE%E6%B6%88%E5%8C%96%E5%85%A8%E9%83%A8%E7%9F%A5%E8%AF%86%E7%82%B9&quot; title=&quot;#10-%E6%80%BB%E7%BB%93%E4%B8%80%E5%BC%A0%E5%9B%BE%E6%B6%88%E5%8C%96%E5%85%A8%E9%83%A8%E7%9F%A5%E8%AF%86%E7%82%B9&quot;&gt;总结：一张图消化全部知识点&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-2&quot;&gt;1. 前言：从一个真实痛点说起&lt;/h3&gt;
&lt;p&gt;想象这样一个场景：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-markdown&quot; lang=&quot;markdown&quot;&gt;App
 └── Page
&lt;span class=&quot;hljs-code&quot;&gt;      └── Child
           └── GrandChild（需要用到 theme 状态）
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;如果没有 Context&lt;/strong&gt;，&lt;code&gt;App&lt;/code&gt; 中的 &lt;code&gt;theme&lt;/code&gt; 要传到 &lt;code&gt;GrandChild&lt;/code&gt;，你必须一层层手动传递 props：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 😫 可怕的 Props Drilling（属性钻探）&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [theme, setTheme] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;);
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Page&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;theme&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;setTheme&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{setTheme}&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Page&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;{ theme, setTheme }&lt;/span&gt;) {   &lt;span class=&quot;hljs-comment&quot;&gt;// 虽然我不需要 theme，但我必须接收并转发&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Child&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;theme&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;setTheme&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{setTheme}&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Child&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;{ theme, setTheme }&lt;/span&gt;) {  &lt;span class=&quot;hljs-comment&quot;&gt;// 我也不需要，但还得继续转发 😩&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;GrandChild&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;theme&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;setTheme&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{setTheme}&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;GrandChild&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;{ theme, setTheme }&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;className&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt;&amp;gt;&lt;/span&gt;当前主题：{theme}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就像是你在公司里，CEO 想把一份文件交给 4 楼的小张，结果你得先给 2 楼完全不相关的李经理，李经理再转交给 3 楼的王主任，王主任再转交……&lt;strong&gt;中间每个人都成了&quot;人肉路由器&quot;&lt;/strong&gt;。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-3&quot;&gt;🎉 React Context 的解决方案&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 优雅的 Context 方案&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [theme, setTheme] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;);
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Page&lt;/span&gt; /&amp;gt;&lt;/span&gt;          {/* Page 不需要接收任何 props！ */}
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  );
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;GrandChild&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;);  &lt;span class=&quot;hljs-comment&quot;&gt;// 直接从这里拿！&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;className&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt;&amp;gt;&lt;/span&gt;当前主题：{theme}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Context 就像在 React 组件树中挖了一条&quot;虫洞&quot;&lt;/strong&gt;，数据可以直接跨越中间所有层级，到达目标组件。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-4&quot;&gt;2. 项目全景：context-demo 做了什么&lt;/h3&gt;
&lt;p&gt;我们先来看看这个项目的完整结构：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot; lang=&quot;bash&quot;&gt;context-demo/
├── src/
│   ├── main.jsx              &lt;span class=&quot;hljs-comment&quot;&gt;# 应用入口，挂载到 #root&lt;/span&gt;
│   ├── App.jsx               &lt;span class=&quot;hljs-comment&quot;&gt;# 演示：useMouse 自定义 Hook（鼠标追踪）&lt;/span&gt;
│   ├── App2.jsx              &lt;span class=&quot;hljs-comment&quot;&gt;# 演示：ThemeContext 主题切换（核心！）&lt;/span&gt;
│   ├── ThemeContext.jsx       &lt;span class=&quot;hljs-comment&quot;&gt;# 上下文定义：createContext&lt;/span&gt;
│   ├── hooks/
│   │   ├── useTheme.js       &lt;span class=&quot;hljs-comment&quot;&gt;# 自定义 Hook：封装 useContext&lt;/span&gt;
│   │   └── useMouse.js       &lt;span class=&quot;hljs-comment&quot;&gt;# 自定义 Hook：封装鼠标位置追踪&lt;/span&gt;
│   └── components/
│       ├── Page.jsx           &lt;span class=&quot;hljs-comment&quot;&gt;# 中间组件：直接使用 useContext&lt;/span&gt;
│       └── Child.jsx          &lt;span class=&quot;hljs-comment&quot;&gt;# 深层组件：使用自定义 useTheme Hook&lt;/span&gt;
├── index.html
├── package.json               &lt;span class=&quot;hljs-comment&quot;&gt;# React 19 + Vite 8&lt;/span&gt;
└── vite.config.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;技术栈&lt;/strong&gt;：React 19.2.6 + Vite 8 + ESLint&lt;/p&gt;
&lt;p&gt;项目包含了 &lt;strong&gt;两条独立的学习路线&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;🅰️ &lt;strong&gt;路线 A&lt;/strong&gt;（&lt;code&gt;App2.jsx&lt;/code&gt; → &lt;code&gt;ThemeContext.jsx&lt;/code&gt; → &lt;code&gt;Page.jsx&lt;/code&gt; → &lt;code&gt;Child.jsx&lt;/code&gt; → &lt;code&gt;useTheme.js&lt;/code&gt;）：完整展示 Context 的创建、提供、消费、封装&lt;/li&gt;
&lt;li&gt;🅱️ &lt;strong&gt;路线 B&lt;/strong&gt;（&lt;code&gt;App.jsx&lt;/code&gt; → &lt;code&gt;useMouse.js&lt;/code&gt;）：演示自定义 Hook 的通用封装模式&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;小提示&lt;/strong&gt;：文章默认走的是路线 A（App2.jsx），因为 &lt;code&gt;main.jsx&lt;/code&gt; 中虽然写的是 &lt;code&gt;import App from &#39;./App.jsx&#39;&lt;/code&gt;，但你可以手动改成 &lt;code&gt;import App from &#39;./App2.jsx&#39;&lt;/code&gt; 来体验 Context 的完整功能。下文我们会在两套方案间对照讲解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-5&quot;&gt;3. 第一关：创建上下文 —— &lt;code&gt;createContext&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 将创建一个 Theme 上下文，为深层次的组件树提供主题共享数据&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { createContext } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt; = &lt;span class=&quot;hljs-title function_&quot;&gt;createContext&lt;/span&gt;({
  &lt;span class=&quot;hljs-attr&quot;&gt;theme&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;,
  &lt;span class=&quot;hljs-attr&quot;&gt;setTheme&lt;/span&gt;: &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {},
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-6&quot;&gt;🔍 逐行解析&lt;/h4&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;代码&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;import { createContext } from &#39;react&#39;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;从 React 中引入 &lt;code&gt;createContext&lt;/code&gt; API&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;createContext({...})&lt;/code&gt;&lt;/td&gt;&lt;td&gt;创建一个上下文对象，&lt;strong&gt;括号里的对象是默认值&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;theme: &#39;light&#39;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;默认主题为亮色&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;setTheme: () =&amp;gt; {}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;默认的 setter 是一个空函数（占位符）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;export const ThemeContext&lt;/code&gt;&lt;/td&gt;&lt;td&gt;导出上下文，供其他组件使用&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h4 data-id=&quot;heading-7&quot;&gt;💡 核心理解：默认值的作用&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;createContext(defaultValue)&lt;/code&gt; 中的 &lt;strong&gt;默认值&lt;/strong&gt; 是什么时候生效的？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;答案&lt;/strong&gt;：当组件在 &lt;strong&gt;没有找到对应的 Provider&lt;/strong&gt; 时，&lt;code&gt;useContext&lt;/code&gt; 返回的就是这个默认值。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 场景：如果组件这样写（没有包裹 Provider）&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Page&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;;  &lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 没有 ThemeContext.Provider&lt;/span&gt;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Page&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; ctx = &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;);
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(ctx);  &lt;span class=&quot;hljs-comment&quot;&gt;// { theme: &#39;light&#39;, setTheme: () =&amp;gt; {} } ← 默认值生效！&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认值相当于一个 &lt;strong&gt;&quot;兜底方案&quot;&lt;/strong&gt;，确保即使忘记写 Provider，组件也不会崩溃（当然，&lt;code&gt;setTheme&lt;/code&gt; 是个空函数，实际上无法切换主题）。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-8&quot;&gt;4. 第二关：提供数据 —— &lt;code&gt;Provider&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;./ThemeContext&#39;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Page&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;./components/Page&#39;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useState } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [theme, setTheme] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;);

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Page&lt;/span&gt; /&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{()&lt;/span&gt; =&amp;gt;&lt;/span&gt; setTheme(&#39;dark&#39;)}&amp;gt;切换主题&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
  );
}

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;App&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-9&quot;&gt;🔍 逐行解析&lt;/h4&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;代码&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;const [theme, setTheme] = useState(&#39;light&#39;)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;在 App 组件中维护 &lt;code&gt;theme&lt;/code&gt; 状态，初始值为 &lt;code&gt;&#39;light&#39;&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;ThemeContext.Provider value={theme}&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;核心！&lt;/strong&gt; 把 &lt;code&gt;theme&lt;/code&gt; 值注入到上下文，所有子组件都能拿到&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;Page /&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;子组件无需 props，直接在 Context 中读取&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;button onClick={() =&amp;gt; setTheme(&#39;dark&#39;)}&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;点击后把 &lt;code&gt;theme&lt;/code&gt; 改为 &lt;code&gt;&#39;dark&#39;&lt;/code&gt;，触发所有消费者重渲染&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h4 data-id=&quot;heading-10&quot;&gt;💡 核心理解：Provider 的工作原理&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ini&quot; lang=&quot;ini&quot;&gt;┌─────────────────────────────────────────────┐
│  ThemeContext.Provider  ← 数据发射塔        │
│  &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;={theme}  → &lt;span class=&quot;hljs-string&quot;&gt;&quot;light&quot;&lt;/span&gt;                   │
│         │                                   │
│         ├── Page  ← 可以接收                 │
│         │    └── Child  ← 可以接收           │
│         ├── 其他组件  ← 都可以接收            │
│         │                                    │
│         └── 任何后代组件都能通过 useContext   │
│             拿到 &quot;light&quot; 这个值              │
└─────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-11&quot;&gt;🔑 Provider 的三个关键特性&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;可以嵌套&lt;/strong&gt;：内层 Provider 会覆盖外层 Provider 的 value&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可以动态改变 value&lt;/strong&gt;：value 变化时，所有消费者都会自动重渲染&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不局限于全局&lt;/strong&gt;：Provider 可以放在组件树的 &lt;strong&gt;任意位置&lt;/strong&gt;，只对该子树有效&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-id=&quot;heading-12&quot;&gt;⚠️ 注意源码中的一个小设计&lt;/h4&gt;
&lt;p&gt;项目中 &lt;code&gt;createContext&lt;/code&gt; 的默认值是 &lt;strong&gt;一个对象&lt;/strong&gt; &lt;code&gt;{theme: &#39;light&#39;, setTheme: () =&amp;gt; {}}&lt;/code&gt;，但 Provider 传递的 &lt;code&gt;value&lt;/code&gt; 是 &lt;strong&gt;一个字符串&lt;/strong&gt; &lt;code&gt;&#39;light&#39;&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 默认值：对象&lt;/span&gt;
&lt;span class=&quot;hljs-title function_&quot;&gt;createContext&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;theme&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;setTheme&lt;/span&gt;: &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {} });

&lt;span class=&quot;hljs-comment&quot;&gt;// Provider 传递：字符串&lt;/span&gt;
&lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt;&amp;gt;&lt;/span&gt;   // theme = &#39;light&#39;（字符串）
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 &lt;strong&gt;刻意的设计&lt;/strong&gt;——Provider 的 &lt;code&gt;value&lt;/code&gt; 会 &lt;strong&gt;完全覆盖&lt;/strong&gt; 默认值。在这个 demo 中，作者选择只传递主题字符串而不是对象，从而让代码简洁地演示&quot;值被覆盖&quot;的行为。实际项目中，你通常 &lt;strong&gt;保持 value 的类型与默认值一致&lt;/strong&gt;，避免类型混乱。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-13&quot;&gt;5. 第三关：消费数据 —— &lt;code&gt;useContext&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Child&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;./Child&#39;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;../ThemeContext&#39;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useContext } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Page&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;);
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(theme);        &lt;span class=&quot;hljs-comment&quot;&gt;// 输出当前 theme 值，如 &quot;light&quot; 或 &quot;dark&quot;&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      Page{theme}            {/* 渲染 &quot;Pagelight&quot; 或 &quot;Pagedark&quot; */}
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;br&lt;/span&gt; /&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Child&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
  );
};

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Page&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-14&quot;&gt;🔍 逐行解析&lt;/h4&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;代码&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;import { useContext } from &#39;react&#39;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;引入 &lt;code&gt;useContext&lt;/code&gt; Hook&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;const theme = useContext(ThemeContext)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;核心！&lt;/strong&gt; 从 &lt;code&gt;ThemeContext&lt;/code&gt; 中读取当前值&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;console.log(theme)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;在控制台打印，方便调试&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;Page{theme}&lt;/code&gt;&lt;/td&gt;&lt;td&gt;将主题值渲染到页面上&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;深层组件 Child&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; useTheme &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;../hooks/useTheme&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Child&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;();
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(theme);

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;h1&lt;/span&gt;&amp;gt;&lt;/span&gt;Child&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;h1&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;className&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{theme}&lt;/span&gt;&amp;gt;&lt;/span&gt;按钮{theme}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;
  );
}

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Child&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-15&quot;&gt;🔍 注意：Child 用了自定义 Hook！&lt;/h4&gt;
&lt;p&gt;Child 没有直接调用 &lt;code&gt;useContext&lt;/code&gt;，而是用了 &lt;code&gt;useTheme()&lt;/code&gt;。这是项目设计的一个巧妙之处——&lt;strong&gt;同一个 Context，两种消费方式&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Page&lt;/strong&gt;：直接 &lt;code&gt;useContext(ThemeContext)&lt;/code&gt; —— 原始写法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Child&lt;/strong&gt;：通过 &lt;code&gt;useTheme()&lt;/code&gt; —— 封装后的优雅写法&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个方法 &lt;strong&gt;完全等价&lt;/strong&gt;，但后者更好（下一节详解为什么）。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-16&quot;&gt;6. 第四关：封装自定义 Hook —— 思维升级&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// react 全面 hooks 编程，可以使用 react, react-router-dom 等&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 提供的 hooks 还可以自定义 hook use 开头函数，自己封装的，简单好用，封装&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 比普通函数的封装，多的地方是可以将 React 响应式、副作用业务等封装进去&lt;/span&gt;

&lt;span class=&quot;hljs-comment&quot;&gt;// 在 Provider 里任何层级的组件多个地方消费数据，模块化抽离放到 hooks&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;../ThemeContext&#39;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useContext } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-comment&quot;&gt;// 约定以 use 开头&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-17&quot;&gt;🔍 这段注释本身就是一份学习笔记！&lt;/h4&gt;
&lt;p&gt;作者在文件中留下了非常珍贵的思考（我把它翻译整理如下）：&lt;/p&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;原始注释&lt;/th&gt;&lt;th&gt;理解&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&quot;react 全面 hooks 编程&quot;&lt;/td&gt;&lt;td&gt;React 已经从 Class 组件全面转向 Hooks 函数组件&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;还可以自定义 hook，use 开头函数&quot;&lt;/td&gt;&lt;td&gt;自定义 Hook 必须用 &lt;code&gt;use&lt;/code&gt; 前缀，这是 React 的约定&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;简单好用，封装&quot;&lt;/td&gt;&lt;td&gt;自定义 Hook 的核心价值就是 &lt;strong&gt;封装复用&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;比普通函数的封装，多的地方是可以将 React 响应式、副作用业务等封装进去&quot;&lt;/td&gt;&lt;td&gt;自定义 Hook 可以用 &lt;code&gt;useState&lt;/code&gt;、&lt;code&gt;useEffect&lt;/code&gt; 等 Hooks，普通函数不行&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;模块化抽离放到 hooks&quot;&lt;/td&gt;&lt;td&gt;把逻辑从 UI 组件中抽离到独立的 hooks 文件中&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h4 data-id=&quot;heading-18&quot;&gt;💡 为什么要封装自定义 Hook？&lt;/h4&gt;
&lt;h5 data-id=&quot;heading-19&quot;&gt;方案对比&lt;/h5&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 方案一：每个组件都直接写（代码重复、耦合度高）&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Page&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;);   &lt;span class=&quot;hljs-comment&quot;&gt;// 需要同时引入 ThemeContext 和 useContext&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// ...&lt;/span&gt;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Child&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;);   &lt;span class=&quot;hljs-comment&quot;&gt;// 重复代码&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// ...&lt;/span&gt;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;GrandChild&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;);   &lt;span class=&quot;hljs-comment&quot;&gt;// 又重复了&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// ...&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 方案二：封装成自定义 Hook（一次封装，到处使用）&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;);
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Page&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;();   &lt;span class=&quot;hljs-comment&quot;&gt;// 简洁、语义化&lt;/span&gt;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Child&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;();   &lt;span class=&quot;hljs-comment&quot;&gt;// 你甚至不需要知道 Context 的存在&lt;/span&gt;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;GrandChild&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;();   &lt;span class=&quot;hljs-comment&quot;&gt;// 统一入口，改一处全改&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 data-id=&quot;heading-20&quot;&gt;封装带来的好处&lt;/h5&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;维度&lt;/th&gt;&lt;th&gt;直接 &lt;code&gt;useContext&lt;/code&gt;&lt;/th&gt;&lt;th&gt;自定义 &lt;code&gt;useTheme&lt;/code&gt; Hook&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;代码量&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;每个组件都要引入 &lt;code&gt;useContext&lt;/code&gt; + &lt;code&gt;ThemeContext&lt;/code&gt;&lt;/td&gt;&lt;td&gt;只需引入 &lt;code&gt;useTheme&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;语义化&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&quot;从 ThemeContext 中取值&quot;&lt;/td&gt;&lt;td&gt;&quot;获取主题&quot;，更符合业务语义&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;可维护性&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;如果 Context 结构变了，N 个组件都要改&lt;/td&gt;&lt;td&gt;只需改 &lt;code&gt;useTheme&lt;/code&gt; 一处&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;类型安全&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;无类型提示（JS 环境）&lt;/td&gt;&lt;td&gt;可以在 Hook 中添加类型断言&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;扩展性&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;每个组件各自处理&lt;/td&gt;&lt;td&gt;可以在 Hook 中统一加日志、错误处理&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h4 data-id=&quot;heading-21&quot;&gt;🔑 自定义 Hook 的命名铁律&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;必须用 &lt;code&gt;use&lt;/code&gt; 开头！&lt;/strong&gt; 这不是建议，是 React 的硬性规则。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 正确&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) { ... }
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useMouse&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) { ... }

&lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 错误（React 会报 lint 警告，Hooks 规则检查也会失败）&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;getTheme&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) { ... }
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;themeHook&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) { ... }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React 的 ESLint 插件会检测所有 &lt;code&gt;useXxx&lt;/code&gt; 函数，确保它们遵循 &lt;strong&gt;Hook 规则&lt;/strong&gt;（不可在条件语句、循环中调用）。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-22&quot;&gt;7. 第五关：实战延伸 —— &lt;code&gt;useMouse&lt;/code&gt; 自定义 Hook&lt;/h3&gt;
&lt;p&gt;虽然 &lt;code&gt;useMouse&lt;/code&gt; 跟 Context 无关，但它是自定义 Hook 的 &lt;strong&gt;另一个经典范例&lt;/strong&gt;，展示了 Hook 如何封装 &lt;strong&gt;副作用逻辑&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useState, useEffect } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useMouse&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [x, setX] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;);
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [y, setY] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;);

  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;addEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove);
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
      &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;removeEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove);
    };

    &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;handleMouseMove&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) {
      &lt;span class=&quot;hljs-title function_&quot;&gt;setX&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;clientX&lt;/span&gt;);
      &lt;span class=&quot;hljs-title function_&quot;&gt;setY&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;clientY&lt;/span&gt;);
    }
  }, []);   &lt;span class=&quot;hljs-comment&quot;&gt;// 空依赖数组 = 只在挂载时注册一次&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; { x, y };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-23&quot;&gt;🔍 逐行解析&lt;/h4&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;代码&lt;/th&gt;&lt;th&gt;含义&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;const [x, setX] = useState(null)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;鼠标 X 坐标，初始为 &lt;code&gt;null&lt;/code&gt;（还没移动过）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;const [y, setY] = useState(null)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;鼠标 Y 坐标&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;useEffect(() =&amp;gt; {...}, [])&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;副作用核心&lt;/strong&gt;，&lt;code&gt;[]&lt;/code&gt; 表示只在组件挂载时执行一次&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;document.addEventListener(&#39;mousemove&#39;, handleMouseMove)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;注册全局鼠标移动监听&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;return () =&amp;gt; { removeEventListener(...) }&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;清理函数&lt;/strong&gt;，组件卸载时移除监听，防止内存泄漏&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;return { x, y }&lt;/code&gt;&lt;/td&gt;&lt;td&gt;把鼠标坐标暴露给调用方&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h4 data-id=&quot;heading-24&quot;&gt;📝 使用场景&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useMouse } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;./hooks/useMouse&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { x, y } = &lt;span class=&quot;hljs-title function_&quot;&gt;useMouse&lt;/span&gt;();

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;style&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{{&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;height:&lt;/span&gt; &#39;&lt;span class=&quot;hljs-attr&quot;&gt;100vh&lt;/span&gt;&#39;, &lt;span class=&quot;hljs-attr&quot;&gt;display:&lt;/span&gt; &#39;&lt;span class=&quot;hljs-attr&quot;&gt;flex&lt;/span&gt;&#39;,
      &lt;span class=&quot;hljs-attr&quot;&gt;alignItems:&lt;/span&gt; &#39;&lt;span class=&quot;hljs-attr&quot;&gt;center&lt;/span&gt;&#39;, &lt;span class=&quot;hljs-attr&quot;&gt;justifyContent:&lt;/span&gt; &#39;&lt;span class=&quot;hljs-attr&quot;&gt;center&lt;/span&gt;&#39; }}&amp;gt;&lt;/span&gt;
      {x &amp;amp;&amp;amp; y ? `x: ${x}, y: ${y}` : &#39;鼠标未移动&#39;}
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  );
}

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;App&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-25&quot;&gt;💡 这个 Hook 的巧妙之处&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;状态封装&lt;/strong&gt;：&lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 的状态管理完全被封装在 Hook 内部，App 只需解构使用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;副作用隔离&lt;/strong&gt;：&lt;code&gt;addEventListener&lt;/code&gt; / &lt;code&gt;removeEventListener&lt;/code&gt; 的配对逻辑不污染组件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可复用性&lt;/strong&gt;：任何一个需要追踪鼠标位置的组件，只需 &lt;code&gt;const {x, y} = useMouse()&lt;/code&gt; 一行代码&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;清理得当&lt;/strong&gt;：&lt;code&gt;useEffect&lt;/code&gt; 的 &lt;code&gt;return&lt;/code&gt; 函数保证了组件销毁时自动移除监听器&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-id=&quot;heading-26&quot;&gt;🔗 与 useTheme 的对比&lt;/h4&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;对比维度&lt;/th&gt;&lt;th&gt;&lt;code&gt;useTheme&lt;/code&gt;&lt;/th&gt;&lt;th&gt;&lt;code&gt;useMouse&lt;/code&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;依赖的 React API&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;useContext&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;useState&lt;/code&gt; + &lt;code&gt;useEffect&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;封装的内容&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;状态读取&lt;/strong&gt;（从 Context 中取值）&lt;/td&gt;&lt;td&gt;&lt;strong&gt;副作用处理&lt;/strong&gt;（DOM 事件监听）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;复杂度&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;极简（一行代码）&lt;/td&gt;&lt;td&gt;中等（注册/注销/状态更新）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;共同点&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;都是 &lt;code&gt;use&lt;/code&gt; 开头，都是自定义 Hook&lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-27&quot;&gt;8. 项目组件树全景图&lt;/h3&gt;
&lt;p&gt;将两套方案合并到一张图中，你就看清了整个项目的设计脉络：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-less&quot; lang=&quot;less&quot;&gt;📦 &lt;span class=&quot;hljs-selector-tag&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;hljs-selector-class&quot;&gt;.jsx&lt;/span&gt;
   │
   ├── 导入 &lt;span class=&quot;hljs-selector-tag&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;hljs-selector-class&quot;&gt;.jsx&lt;/span&gt;（路线 &lt;span class=&quot;hljs-selector-tag&quot;&gt;B&lt;/span&gt;：&lt;span class=&quot;hljs-selector-tag&quot;&gt;useMouse&lt;/span&gt; 演示）
   │    └── &lt;span class=&quot;hljs-selector-tag&quot;&gt;App&lt;/span&gt; 组件
   │         └── &lt;span class=&quot;hljs-selector-tag&quot;&gt;useMouse&lt;/span&gt;() &lt;span class=&quot;hljs-selector-tag&quot;&gt;Hook&lt;/span&gt;
   │              ├── &lt;span class=&quot;hljs-selector-tag&quot;&gt;useState&lt;/span&gt;(x, y)
   │              └── &lt;span class=&quot;hljs-selector-tag&quot;&gt;useEffect&lt;/span&gt;（&lt;span class=&quot;hljs-selector-tag&quot;&gt;mousemove&lt;/span&gt; 监听）
   │
   └── 导入 &lt;span class=&quot;hljs-selector-tag&quot;&gt;App2&lt;/span&gt;&lt;span class=&quot;hljs-selector-class&quot;&gt;.jsx&lt;/span&gt;（路线 &lt;span class=&quot;hljs-selector-tag&quot;&gt;A&lt;/span&gt;：&lt;span class=&quot;hljs-selector-tag&quot;&gt;Context&lt;/span&gt; 演示）★核心
        └── &lt;span class=&quot;hljs-selector-tag&quot;&gt;App2&lt;/span&gt; 组件
             ├── &lt;span class=&quot;hljs-selector-tag&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;)         ← 状态源头
             ├── &lt;span class=&quot;hljs-selector-tag&quot;&gt;ThemeContext&lt;/span&gt;&lt;span class=&quot;hljs-selector-class&quot;&gt;.Provider&lt;/span&gt;     ← 数据发射塔
             │    &lt;span class=&quot;hljs-selector-tag&quot;&gt;value&lt;/span&gt;={&lt;span class=&quot;hljs-selector-tag&quot;&gt;theme&lt;/span&gt;}
             │    │
             │    ├── &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;Page&lt;/span&gt; /&amp;gt;            ← 消费者（直接 &lt;span class=&quot;hljs-selector-tag&quot;&gt;useContext&lt;/span&gt;）
             │    │    └── &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;Child&lt;/span&gt; /&amp;gt;      ← 消费者（通过 &lt;span class=&quot;hljs-selector-tag&quot;&gt;useTheme&lt;/span&gt; &lt;span class=&quot;hljs-selector-tag&quot;&gt;Hook&lt;/span&gt;）
             │    │
             │    └── &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;button&lt;/span&gt;&amp;gt;切换主题&amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;button&lt;/span&gt;&amp;gt;  ← 状态修改者
             │
             └── &lt;span class=&quot;hljs-selector-tag&quot;&gt;ThemeContext&lt;/span&gt;              ← 上下文定义（&lt;span class=&quot;hljs-selector-tag&quot;&gt;createContext&lt;/span&gt;）
                 └── &lt;span class=&quot;hljs-selector-tag&quot;&gt;hooks&lt;/span&gt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;useTheme&lt;/span&gt;&lt;span class=&quot;hljs-selector-class&quot;&gt;.js&lt;/span&gt;     ← 自定义 &lt;span class=&quot;hljs-selector-tag&quot;&gt;Hook&lt;/span&gt; 封装
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-28&quot;&gt;数据流动轨迹&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-scss&quot; lang=&quot;scss&quot;&gt;用户点击 &quot;切换主题&quot;
        │
        ▼
  &lt;span class=&quot;hljs-built_in&quot;&gt;setTheme&lt;/span&gt;(&#39;dark&#39;)          ← App2 中的 state 更新
        │
        ▼
  Provider value 变化        ← React 检测到 Provider value 改变
        │
        ├────▶ Page 重渲染   ← &lt;span class=&quot;hljs-built_in&quot;&gt;useContext&lt;/span&gt;(ThemeContext) → 拿到 &#39;dark&#39;
        │        │
        │        └────▶ Child 重渲染  ← &lt;span class=&quot;hljs-built_in&quot;&gt;useTheme&lt;/span&gt;() → 拿到 &#39;dark&#39;
        │
        └────▶ 所有消费者都自动更新！ ← 不需要 props 传递
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-29&quot;&gt;9. 常见误区与踩坑指南&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-30&quot;&gt;❌ 误区 1：Context 默认值 = Provider 的 value&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt; = &lt;span class=&quot;hljs-title function_&quot;&gt;createContext&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;theme&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;setTheme&lt;/span&gt;: &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {} });

&lt;span class=&quot;hljs-comment&quot;&gt;// 有人以为 Provider 会自动用默认值：&lt;/span&gt;

&lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Page&lt;/span&gt; /&amp;gt;&lt;/span&gt;       {/* ❌ 没有 value prop！Page 拿到的还是默认值 */}
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;正解&lt;/strong&gt;：&lt;code&gt;value&lt;/code&gt; 是 &lt;strong&gt;必传&lt;/strong&gt; 的（不传就还是默认值，等于没提供新值）。你必须显式地写：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;Provider&lt;/span&gt; value={&lt;span class=&quot;hljs-comment&quot;&gt;/* 你真正想传的数据 */&lt;/span&gt;}&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-31&quot;&gt;❌ 误区 2：把 Provider 放错位置&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;);  &lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 返回默认值！&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;dark&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Page&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;正解&lt;/strong&gt;：&lt;code&gt;App&lt;/code&gt; 是 Provider 的 &lt;strong&gt;兄弟或父级&lt;/strong&gt;，不在 Provider 内部，所以拿不到 Provider 的 value。Provider 只对 &lt;strong&gt;其 JSX 子树&lt;/strong&gt; 生效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 拆成两个组件&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;dark&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ThemedPage&lt;/span&gt; /&amp;gt;&lt;/span&gt;   {/* 在这个内部才能拿到 value */}
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  );
}

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;ThemedPage&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; theme = &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;);  &lt;span class=&quot;hljs-comment&quot;&gt;// ✅ &#39;dark&#39;&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// ...&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-32&quot;&gt;❌ 误区 3：过度使用 Context&lt;/h4&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;/th&gt;&lt;th&gt;建议&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;全局主题、语言、用户信息&lt;/td&gt;&lt;td&gt;✅ 适合 Context&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;组件间的临时共享状态&lt;/td&gt;&lt;td&gt;❌ 用 props 或状态提升更合适&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;高频变化的状态（如动画帧、输入框值）&lt;/td&gt;&lt;td&gt;❌ 会导致大量不必要重渲染，考虑状态管理库&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;只在父子间传递的数据&lt;/td&gt;&lt;td&gt;❌ 直接用 props 即可，不要过度设计&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h4 data-id=&quot;heading-33&quot;&gt;❌ 误区 4：Provider value 用对象字面量导致不必要渲染&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 每次 App 重渲染，{theme, setTheme} 都是新对象，导致所有消费者重渲染&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [theme, setTheme] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;);
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{{&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;theme&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;setTheme&lt;/span&gt; }}&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Page&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  );
}

&lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 用 useMemo 缓存 value 对象&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [theme, setTheme] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;);
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; value = &lt;span class=&quot;hljs-title function_&quot;&gt;useMemo&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; ({ theme, setTheme }), [theme]);
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{value}&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Page&lt;/span&gt; /&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ThemeContext.Provider&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;不过这个 demo 项目中传的是字符串 &lt;code&gt;&#39;light&#39;&lt;/code&gt; / &lt;code&gt;&#39;dark&#39;&lt;/code&gt;（原始值），不会有这个性能问题 👍&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-34&quot;&gt;10. 总结：一张图消化全部知识点&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-35&quot;&gt;📊 React Context 完整使用流程&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ini&quot; lang=&quot;ini&quot;&gt;┌───────────────────────────────────────────────────────────┐
│  步骤 1: 创建上下文                                       │
│  ┌───────────────────────────────────────────────────┐    │
│  │  createContext(defaultValue)                      │    │
│  │  → 返回一个 Context 对象                           │    │
│  │  → 包含 Provider 组件 和 Consumer（历史遗留）      │    │
│  └───────────────────────────────────────────────────┘    │
│                          ↓                                │
│  步骤 2: 提供数据（在祖先组件中）                          │
│  ┌──────────────────────────────────────────────────┐    │
│  │  &amp;lt;ThemeContext.Provider &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;={数据}&amp;gt;            │    │
│  │    {children}                                    │    │
│  │  &amp;lt;/ThemeContext.Provider&amp;gt;                        │    │
│  └──────────────────────────────────────────────────┘    │
│                          ↓                               │
│  步骤 3: 消费数据（在任意后代组件中）                      │
│  ┌───────────────────────────────────────────────────┐    │
│  │  方式 A: const &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt; = useContext(ThemeContext)   │    │
│  │  方式 B: 封装自定义 Hook → const &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt; = useTheme()│   │
│  └───────────────────────────────────────────────────┘    │
│                          ↓                                │
│  步骤 4: 封装自定义 Hook（进阶）                           │
│  ┌───────────────────────────────────────────────────┐    │
│  │  function useXxx() { return useContext(XxxCtx)&lt;span class=&quot;hljs-comment&quot;&gt;; } │    │&lt;/span&gt;
│  │  → 语义化、可维护、可扩展                          │    │
│  └───────────────────────────────────────────────────┘    │
└───────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-36&quot;&gt;🎯 三个核心 API + 一个最佳实践&lt;/h4&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;API&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;th&gt;类比&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;createContext(defaultValue)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;创建上下文，定义&quot;通道&quot;&lt;/td&gt;&lt;td&gt;修建管道&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;&amp;lt;Ctx.Provider value={...}&amp;gt;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;提供数据，注入&quot;水源&quot;&lt;/td&gt;&lt;td&gt;接通水源&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;useContext(Ctx)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;消费数据，读取&quot;水流&quot;&lt;/td&gt;&lt;td&gt;打开水龙头&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;最佳实践&lt;/strong&gt;：封装自定义 Hook&lt;/td&gt;&lt;td&gt;包装 &lt;code&gt;useContext&lt;/code&gt;，提供更干净的接口&lt;/td&gt;&lt;td&gt;安装智能水表 🚿&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h4 data-id=&quot;heading-37&quot;&gt;📝 从这个小项目中你学到了什么&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Context 解决了 Props Drilling&lt;/strong&gt;：不再需要一层层手动传递数据&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Provider 决定范围&lt;/strong&gt;：只有被 Provider 包裹的组件才能访问 Context&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;useContext 是消费方式&lt;/strong&gt;：简洁、Hook 风格的数据读取&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自定义 Hook 是灵魂&lt;/strong&gt;：将 Context 消费逻辑封装成语义化的自定义 Hook，真正做到&quot;关注点分离&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自定义 Hook ≠ 只能用 Context&lt;/strong&gt;：&lt;code&gt;useMouse&lt;/code&gt; 展示了 Hook 如何封装状态+副作用，和 Context 完全解耦&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;📚 &lt;strong&gt;延伸阅读&lt;/strong&gt;：如果你想继续深入，建议学习 &lt;code&gt;useReducer&lt;/code&gt; + Context 组合模式（轻量级状态管理方案），以及 &lt;code&gt;useMemo&lt;/code&gt; / &lt;code&gt;React.memo&lt;/code&gt; 如何优化 Context 引发的重渲染问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;</description><link>https://juejin.cn/post/7671122376885289000</link><guid isPermaLink="false">https://juejin.cn/post/7671122376885289000</guid><pubDate>Fri, 07 Aug 2026 14:23:10 GMT</pubDate><author>何时梦醒</author><category>前端</category><category>JavaScript</category><category>React.js</category></item><item><title>从 react-bits 看动效组件化：别把视觉效果写成一次性页面代码</title><description>&lt;h2 data-id=&quot;heading-0&quot;&gt;从 react-bits 看动效组件化：别把视觉效果写成一次性页面代码&lt;/h2&gt;
&lt;p&gt;前端页面里的动效，经常处在一个尴尬的位置。&lt;/p&gt;
&lt;p&gt;做得少，页面容易显得平；做得多，又容易变成互相抢戏的视觉噪音。更常见的情况是，某个活动页或官网首屏需要一点“特别的东西”，于是开发者临时写出一段动画：效果当下能跑，页面也变得更亮眼，但等到下一次改版、换文案、适配移动端时，这段代码就成了谁都不太想碰的角落。&lt;/p&gt;
&lt;p&gt;这也是动画交互组件库值得被单独观察的原因。&lt;/p&gt;
&lt;p&gt;react-bits 是一个 GitHub 公开项目，定位为动画交互式 React 组件库，并以“36K stars 的酷炫组件”这一角度受到关注。它提供了一个有意思的观察样本：视觉表达不一定只能作为某个页面的一次性实现，也可以尝试被整理为可复用的组件能力。&lt;/p&gt;
&lt;p&gt;这不意味着组件化会自动让动效变好，更不意味着把组件复制进页面就能获得同样的体验。真正值得讨论的是，视觉效果一旦成为组件，开发者应该如何重新理解它的边界、职责和引入成本。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-1&quot;&gt;“酷炫组件”为何值得单独看&lt;/h3&gt;
&lt;p&gt;一个动效效果能吸引人，往往因为它在极短时间内传递了情绪。&lt;/p&gt;
&lt;p&gt;动态背景可以让首屏不再空白，文字变化可以制造节奏，鼠标或触摸反馈可以让界面显得更有回应。对于作品集、品牌页、产品介绍页、活动页这类强调第一印象的场景，视觉表达本身就是内容的一部分。&lt;/p&gt;
&lt;p&gt;但单个效果是否好看，和它是否适合作为组件，是两回事。&lt;/p&gt;
&lt;p&gt;一次性页面实现通常只服务于一个确定的布局：文案长度固定、容器尺寸已知、配色无需变化、交互路径也足够单一。这种情况下，开发者可以为了最终画面做大量针对性调整。&lt;/p&gt;
&lt;p&gt;组件却要面对更多不确定性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它可能被放在不同宽度的容器中；&lt;/li&gt;
&lt;li&gt;它需要承受不同长度、不同语言的内容；&lt;/li&gt;
&lt;li&gt;它要和既有的按钮、图片、表单、导航一起工作；&lt;/li&gt;
&lt;li&gt;它可能需要跟随主题色、深浅模式或品牌规范变化；&lt;/li&gt;
&lt;li&gt;它还要能被移除、替换和维护。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，动效组件库真正考验的不是“能不能做出惊艳画面”，而是“能不能把惊艳画面拆成可管理的界面单元”。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-2&quot;&gt;react-bits：把动画交互放进 React 的组件选择中&lt;/h3&gt;
&lt;p&gt;react-bits 的已知定位是动画交互式 React 组件库。仅从这个定位出发，就可以看出它试图解决的不是某一个具体页面的视觉问题，而是把一类动画与交互表达带进 React 开发时的组件选择范围。&lt;/p&gt;
&lt;p&gt;平时提到组件库，人们很自然会想到按钮、输入框、弹窗、表格。这些组件承担的是操作和信息组织的基础职责。而动画交互组件承担的职责更偏向于表现：建立氛围、强调层级、提供状态反馈、增加探索感。&lt;/p&gt;
&lt;p&gt;两者的共同点在于，它们都应该有边界。&lt;/p&gt;
&lt;p&gt;按钮不应顺带控制整页布局；弹窗不应绑定某个业务接口；同样，一个视觉效果也不应悄悄决定页面所有层级、内容结构和样式规则。只有职责足够聚焦，它才能被安全地使用在不同场景中。&lt;/p&gt;
&lt;p&gt;因此，观察 react-bits 时，与其把注意力停留在“有哪些效果”，不如把它看作一个问题的答案尝试：视觉表达能否像其他前端能力一样，被封装、组合和替换？&lt;/p&gt;
&lt;h3 data-id=&quot;heading-3&quot;&gt;第一种视角：先看接口，而不是先看结果&lt;/h3&gt;
&lt;p&gt;看动效演示时，结果往往最先进入视线：光影、位移、渐变、粒子、文字节奏。判断组件价值时，却应该反过来，先关注它可能需要提供怎样的使用接口。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-4&quot;&gt;内容和效果应当分开&lt;/h4&gt;
&lt;p&gt;一个可复用的视觉组件，最好允许内容保持自己的语义。&lt;/p&gt;
&lt;p&gt;标题仍然是标题，说明仍然是说明，按钮仍然是按钮；动画只是改变它们被呈现或被感知的方式，而不应该把内容完全锁进某个固定结构。这样做的好处是，内容变更时不必反复重写效果，效果替换时也不会破坏页面的信息层级。&lt;/p&gt;
&lt;p&gt;如果某个动效只有在特定句子、特定 DOM 结构、特定绝对定位下才能成立，那么它更像一个展示片段，而不是容易复用的组件。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-5&quot;&gt;常见变化需要可控&lt;/h4&gt;
&lt;p&gt;实际页面里最常见的需求，并不是重写一个新动效，而是调整已有动效：颜色要换成品牌色、节奏要更慢、交互范围要缩小、某些设备上要减弱表现、特定状态下要暂停或关闭。&lt;/p&gt;
&lt;p&gt;因此，组件化的关键不在于提供无限多配置，而在于让合理的变化有清晰入口。开发者应能分辨哪些是内容配置，哪些是视觉配置，哪些是交互配置，而不是通过修改内部实现去猜测影响范围。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-6&quot;&gt;组件要允许“被克制地使用”&lt;/h4&gt;
&lt;p&gt;好的动效组件不应逼迫开发者使用它的全部表现力。&lt;/p&gt;
&lt;p&gt;一个页面有时只需要轻微过渡，有时才需要明显互动。若一个效果只能以最强形态存在，使用范围会被大幅压缩。相反，如果它能被放在背景层、装饰层或局部反馈层，页面就更容易保持信息和视觉之间的平衡。&lt;/p&gt;
&lt;p&gt;组件的成熟度，常常体现在它是否允许自己退后一步。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-7&quot;&gt;第二种视角：动效、交互和内容，各自该负责什么&lt;/h3&gt;
&lt;p&gt;动画不是“加在页面上”的独立装饰，它会改变用户阅读和操作页面的节奏。因此，在引入之前，最好先厘清三种职责。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-8&quot;&gt;内容负责说清楚&lt;/h4&gt;
&lt;p&gt;用户进入页面后，首先要理解页面在讲什么：产品价值是什么、这段信息和自己有什么关系、下一步可以做什么。&lt;/p&gt;
&lt;p&gt;这些信息不应该依赖动画完成后才能出现，也不应该只能通过颜色、移动方向或其他纯视觉线索来理解。内容本身必须站得住。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-9&quot;&gt;交互负责回应&lt;/h4&gt;
&lt;p&gt;当用户悬停、点击、滚动、输入或切换状态时，界面需要给出可预测的回应。动效在这里可以发挥作用：告诉用户某个操作被接收、某个区域可继续探索、某个状态正在改变。&lt;/p&gt;
&lt;p&gt;这类反馈的价值不在于华丽，而在于及时、清楚和不误导。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-10&quot;&gt;动效负责组织注意力&lt;/h4&gt;
&lt;p&gt;动效最适合承担的是注意力管理。&lt;/p&gt;
&lt;p&gt;它可以把视觉焦点带到标题或行动按钮上，可以让内容按照阅读顺序出现，也可以在状态变化时帮助用户感知差异。但它不该持续要求用户观看，更不该压过核心任务。&lt;/p&gt;
&lt;p&gt;一个简单的判断方式是：把动画暂时拿掉后，页面是否仍然能被读懂和操作？如果答案是否定的，说明关键语义可能被错误地交给了效果。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-11&quot;&gt;第三种视角：不同页面对“动”的需求并不一样&lt;/h3&gt;
&lt;p&gt;同样的动效，在不同页面里的意义可能完全不同。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-12&quot;&gt;适合表达的页面&lt;/h4&gt;
&lt;p&gt;官网首屏、作品集、活动专题、产品故事页，通常需要建立气质和情绪。这里可以容纳更具表现力的背景、文字进入方式或局部互动，因为用户进入这些页面时，本来就带着探索和感受的预期。&lt;/p&gt;
&lt;p&gt;但表达型页面也有底线：标题要可读，入口要明确，信息不能被背景吞没。动效应该把页面主题放大，而不是制造一段与内容无关的视觉表演。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-13&quot;&gt;需要效率的页面&lt;/h4&gt;
&lt;p&gt;工具后台、设置页、复杂表单、数据查询和长内容阅读，往往更强调稳定性和效率。它们并非不能使用动画，但更适合使用低干扰的状态变化和必要反馈。&lt;/p&gt;
&lt;p&gt;在这些场景中，用户希望快速完成任务。任何让人等待、分心或难以定位信息的效果，都可能抵消原本的体验收益。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-14&quot;&gt;介于两者之间的页面&lt;/h4&gt;
&lt;p&gt;产品介绍页、内容专题和部分数据展示页通常处于中间地带：既需要表达，也需要清楚传递信息。&lt;/p&gt;
&lt;p&gt;这类页面更考验动效的分寸。开发者可以让动态服务于章节切换、重点提示或叙事节奏，但需要避免每一个区块都采用不同的运动逻辑。统一的节奏往往比效果数量更重要。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-15&quot;&gt;引入前需要自行核对的五件事&lt;/h3&gt;
&lt;p&gt;第三方组件库是否适合自己的项目，不能只从展示效果判断。对 react-bits 这类动画交互组件库而言，下面这些问题都值得在使用前逐一核对。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-16&quot;&gt;1. 设计一致性&lt;/h4&gt;
&lt;p&gt;它是否符合当前产品的视觉语言？色彩、字体、间距、圆角、阴影和交互节奏能否协调？&lt;/p&gt;
&lt;p&gt;动效如果脱离设计系统，即使单看很精致，也容易让页面产生拼贴感。应当先决定页面需要怎样的情绪，再选择相应的表达，而不是先挑中一个效果，再让页面为它让路。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-17&quot;&gt;2. 依赖与集成边界&lt;/h4&gt;
&lt;p&gt;引入组件时，需要确认它能否和项目已有的 React 使用方式、构建流程、样式策略以及其他 UI 组件协作。&lt;/p&gt;
&lt;p&gt;重点不是追求“引入越少越好”，而是明确影响范围：它会进入哪些页面？样式会如何管理？若未来不再使用，替换路径是否清楚？把这些问题提前想明白，比把效果散落在多个页面后再收拾更轻松。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-18&quot;&gt;3. 可访问性&lt;/h4&gt;
&lt;p&gt;所有动态效果都应考虑用户是否能够舒适地理解和操作页面。&lt;/p&gt;
&lt;p&gt;需要检查的不是抽象口号，而是具体体验：重要信息是否在不依赖动画时仍可获取，持续移动是否可能造成干扰，键盘用户是否仍能清晰地看到焦点与操作路径，减少动态偏好下是否有合理的表现。&lt;/p&gt;
&lt;p&gt;动效服务的是体验，不应把一部分用户排除在体验之外。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-19&quot;&gt;4. 性能验证&lt;/h4&gt;
&lt;p&gt;任何动画都可能带来额外的渲染与交互负担。不能因为组件库受关注，就直接推断它在所有页面和设备上都有相同表现。&lt;/p&gt;
&lt;p&gt;更可靠的做法，是把性能当成项目内需要自行观察的维度：首屏内容是否仍能及时出现，滚动和点击是否保持顺畅，移动端是否仍有合理体验，当页面同时加载其他资源时是否需要做取舍。&lt;/p&gt;
&lt;p&gt;这里没有通用答案，只有和自身页面预算相匹配的答案。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-20&quot;&gt;5. 维护成本&lt;/h4&gt;
&lt;p&gt;视觉效果最容易在需求变化时暴露成本。换一段更长的文案、适配一个更窄的屏幕、切换品牌主题、调整页面层级，都可能影响原有表现。&lt;/p&gt;
&lt;p&gt;因此，优先把动效放在边界清晰、可独立替换的区域，通常更稳妥。一个组件真正带来复用价值的前提，是它不会在未来变成难以理解的遗留代码。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-21&quot;&gt;把“收藏”变成一次具体选择&lt;/h3&gt;
&lt;p&gt;react-bits 的意义，不必被理解成“为所有 React 页面增加动效”的方案。它更像一个提醒：视觉表达可以被模块化，动画交互也可以被放进组件设计的讨论中。&lt;/p&gt;
&lt;p&gt;面对这类库，可以先选一个小场景，而不是一次改造整个页面。明确这个区域到底需要什么：是建立首屏印象、强调一段内容、回应一次操作，还是帮助用户理解状态变化？再判断它是否能与页面语义、设计系统和维护方式相容。&lt;/p&gt;
&lt;p&gt;当动效被当成可复用组件，而不是一次性的视觉魔法，选择才会变得更清醒。酷炫只是入口；真正决定它能否留下来的，是组件边界是否清楚、页面职责是否匹配，以及团队是否愿意承担相应的接入与维护成本。&lt;/p&gt;
&lt;p&gt;项目地址：&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2FDavidHDev%2Freact-bits&quot; target=&quot;_blank&quot; title=&quot;https://github.com/DavidHDev/react-bits&quot; ref=&quot;nofollow noopener noreferrer&quot;&gt;github.com/DavidHDev/r…&lt;/a&gt;&lt;/p&gt;</description><link>https://juejin.cn/post/7671100905954508836</link><guid isPermaLink="false">https://juejin.cn/post/7671100905954508836</guid><pubDate>Fri, 07 Aug 2026 14:11:49 GMT</pubDate><author>吴琼琼</author><category>开发工具</category><category>React.js</category><category>JavaScript</category><category>开源</category></item><item><title>拒绝页面假死！React 并发编程实战：Web Worker + useRef 深度解析与高性能计算架构</title><description>&lt;h2 data-id=&quot;heading-0&quot;&gt;前言：当 React 遇上“算力危机”&lt;/h2&gt;
&lt;p&gt;在现代前端开发中，我们习惯了用&amp;nbsp;&lt;code&gt;useState&lt;/code&gt;&amp;nbsp;驱动视图，用&amp;nbsp;&lt;code&gt;useEffect&lt;/code&gt;&amp;nbsp;处理副作用。但在面对海量数据渲染、复杂数学运算、图像处理甚至端侧 AI 模型推理时，React 的响应式机制往往会成为性能的瓶颈。&lt;/p&gt;
&lt;p&gt;你是否遇到过这样的场景：点击一个“导出报表”或“开始计算”按钮后，整个页面瞬间卡死，按钮点击效果消失，滚动条拖不动，直到几秒后结果才突然蹦出来？这就是典型的&amp;nbsp;&lt;strong&gt;主线程阻塞&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;JavaScript 是单线程语言，它的执行与 UI 渲染共享同一个线程。一旦 JS 执行栈被长任务占据，浏览器就无法进行下一帧的绘制（Paint）。为了打破这一瓶颈，我们需要引入&amp;nbsp;&lt;strong&gt;Web Worker&lt;/strong&gt;&amp;nbsp;开启多线程并行计算。但 Worker 的生命周期管理、与 React 组件状态的同步，往往让开发者头疼不已。&lt;/p&gt;
&lt;p&gt;本文将结合&amp;nbsp;&lt;code&gt;useRef&lt;/code&gt;&amp;nbsp;的持久化特性与 Web Worker 的消息机制，带你从零构建一个&lt;strong&gt;生产级的高性能计算架构&lt;/strong&gt;。我们将深入 V8 引擎与浏览器渲染原理，并补充完整的 TypeScript 实战代码。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-1&quot;&gt;第一章：核心原理——为什么我们需要“双线程”？&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-2&quot;&gt;1.1 JS 单线程与 Event Loop 的局限&lt;/h3&gt;
&lt;p&gt;在浏览器的多进程架构中，渲染进程（Renderer Process）包含了 DOM 树构建、样式计算、布局（Layout）以及 JavaScript 执行。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GUI 渲染线程&lt;/strong&gt;：负责绘制页面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JS 引擎线程&lt;/strong&gt;：负责解析和执行脚本（如 V8）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个线程是互斥的。当 JS 引擎在执行一段耗时脚本（例如遍历 1000 万次循环）时，GUI 渲染线程会被挂起。这就是为什么我们在做复杂计算时，页面会“掉帧”甚至“假死”。&lt;/p&gt;
&lt;p&gt;虽然 JS 提供了异步机制（Promise, setTimeout），但它们本质上依然是&lt;strong&gt;非阻塞的单线程调度&lt;/strong&gt;。异步任务只是被推迟到了任务队列中等待执行，并没有真正利用多核 CPU 的并行计算能力。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-3&quot;&gt;1.2 Web Worker：浏览器的“后台算力中心”&lt;/h3&gt;
&lt;p&gt;HTML5 引入的 Web Worker 标准，允许我们在浏览器中开辟一个完全独立的线程。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;独立性&lt;/strong&gt;：Worker 拥有独立的 Global Scope（&lt;code&gt;self&lt;/code&gt;），不共享主线程的内存堆栈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隔离性&lt;/strong&gt;：Worker&amp;nbsp;&lt;strong&gt;无法访问 DOM&lt;/strong&gt;、&lt;code&gt;window&lt;/code&gt;、&lt;code&gt;document&lt;/code&gt;&amp;nbsp;等对象。它只能进行纯计算、网络请求（fetch）或定时器操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通信成本&lt;/strong&gt;：由于内存隔离，主线程与 Worker 之间通过&amp;nbsp;&lt;code&gt;postMessage&lt;/code&gt;&amp;nbsp;进行通信。数据传递通常采用&lt;strong&gt;结构化克隆算法（Structured Clone）&lt;/strong&gt; ，这意味着数据会被深拷贝（除非使用&amp;nbsp;&lt;code&gt;Transferable Objects&lt;/code&gt;&amp;nbsp;转移所有权），这在大数据量传输时也是需要考虑的性能点。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-4&quot;&gt;第二章：关键 Hook 解析——useRef 与 useState 的本质区别&lt;/h2&gt;
&lt;p&gt;在 React 中集成 Worker，最大的痛点在于：&lt;strong&gt;如何持有一个不随组件渲染而重置的 Worker 实例？&lt;/strong&gt;&lt;/p&gt;
&lt;h3 data-id=&quot;heading-5&quot;&gt;2.1 useState vs useRef&lt;/h3&gt;
&lt;p&gt;很多初学者会尝试用&amp;nbsp;&lt;code&gt;useState&lt;/code&gt;&amp;nbsp;来存储 Worker 实例，这是错误的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;useState&lt;/strong&gt;：是&lt;strong&gt;响应式&lt;/strong&gt;的。当你调用&amp;nbsp;&lt;code&gt;setWorker(new Worker(...))&lt;/code&gt;&amp;nbsp;时，React 会触发组件重新渲染。且每次渲染如果依赖处理不当，可能会导致状态更新的不一致。更重要的是，我们不需要 Worker 实例的变化去触发 UI 更新。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;useRef&lt;/strong&gt;：返回一个&lt;strong&gt;可变对象&lt;/strong&gt;&amp;nbsp;&lt;code&gt;{ current: ... }&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;持久化&lt;/strong&gt;：在组件的整个生命周期内，&lt;code&gt;ref.current&lt;/code&gt;&amp;nbsp;的值保持不变，不会因为组件的重新渲染（Re-render）而丢失或重置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;非响应式&lt;/strong&gt;：修改&amp;nbsp;&lt;code&gt;ref.current&lt;/code&gt;&amp;nbsp;&lt;strong&gt;不会&lt;/strong&gt;触发组件更新。这正是我们想要的——Worker 在后台默默运行，只有当它计算出结果并通过&amp;nbsp;&lt;code&gt;setState&lt;/code&gt;&amp;nbsp;通知 React 时，UI 才需要更新。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-id=&quot;heading-6&quot;&gt;2.2 为什么 Worker 必须放在 useRef 中？&lt;/h3&gt;
&lt;p&gt;如果在组件函数体内直接&amp;nbsp;&lt;code&gt;const worker = new Worker(...)&lt;/code&gt;，那么每次组件因为任何状态变化（比如用户输入了一个字符）而重渲染时，都会创建一个新的 Worker，导致内存泄漏和逻辑混乱。&lt;/p&gt;
&lt;p&gt;使用&amp;nbsp;&lt;code&gt;useRef&lt;/code&gt;&amp;nbsp;配合&amp;nbsp;&lt;code&gt;useEffect&lt;/code&gt;，我们可以确保：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Worker 仅在组件挂载（Mount）时创建一次。&lt;/li&gt;
&lt;li&gt;组件卸载（Unmount）时，能够准确找到该实例并进行销毁（&lt;code&gt;terminate&lt;/code&gt;），防止内存泄漏。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-7&quot;&gt;第三章：实战演练——构建高性能计算组件&lt;/h2&gt;
&lt;p&gt;光有理论不够，我们直接上代码。为了演示真实场景，我们将模拟一个 &lt;strong&gt;“斐波那契数列大数计算”&lt;/strong&gt; 或 &lt;strong&gt;“海量数据加密”&lt;/strong&gt; 任务，这种任务极易阻塞主线程。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-8&quot;&gt;3.1 目录结构规划&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-bash&quot; lang=&quot;bash&quot;&gt;src/
├── hooks/
│   └── useHeavyCalc.ts      &lt;span class=&quot;hljs-comment&quot;&gt;# 封装 Worker 逻辑的自定义 Hook&lt;/span&gt;
├── workers/
│   └── calc.worker.ts       &lt;span class=&quot;hljs-comment&quot;&gt;# Worker 线程的具体执行逻辑&lt;/span&gt;
├── components/
│   └── Calculator.tsx       &lt;span class=&quot;hljs-comment&quot;&gt;# UI 展示组件&lt;/span&gt;
└── App.tsx                  &lt;span class=&quot;hljs-comment&quot;&gt;# 入口&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-9&quot;&gt;3.2 第一步：编写 Worker 线程逻辑&lt;/h3&gt;
&lt;p&gt;Worker 文件是独立运行的脚本。我们需要监听主线程发来的消息，执行计算，然后回传结果。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ini&quot; lang=&quot;ini&quot;&gt;// 定义 Worker 接收的消息类型
interface CalcMessage {
  type: &#39;CALCULATE_FIB&#39;&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;
  payload: number&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;
}

// 模拟一个极度耗时的递归计算（故意阻塞）
const &lt;span class=&quot;hljs-attr&quot;&gt;heavyFibonacci&lt;/span&gt; = (n: number): number =&amp;gt; {
  if (n &amp;lt;= 1) return n&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;
  return heavyFibonacci(n - 1) + heavyFibonacci(n - 2)&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;
}&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;

// self 代表 Worker 的全局作用域
&lt;span class=&quot;hljs-attr&quot;&gt;self.onmessage&lt;/span&gt; = function (e: MessageEvent&amp;lt;CalcMessage&amp;gt;) {
  const { type, payload } = e.data&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;

  console.log(`&lt;span class=&quot;hljs-section&quot;&gt;[Worker]&lt;/span&gt; 收到任务: ${type}, 参数: ${payload}`)&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;

  if (&lt;span class=&quot;hljs-attr&quot;&gt;type&lt;/span&gt; === &lt;span class=&quot;hljs-string&quot;&gt;&#39;CALCULATE_FIB&#39;&lt;/span&gt;) {
    const &lt;span class=&quot;hljs-attr&quot;&gt;startTime&lt;/span&gt; = performance.now()&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;
    
    // 执行耗时计算
    const &lt;span class=&quot;hljs-attr&quot;&gt;result&lt;/span&gt; = heavyFibonacci(payload)&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;
    
    const &lt;span class=&quot;hljs-attr&quot;&gt;endTime&lt;/span&gt; = performance.now()&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;
    const &lt;span class=&quot;hljs-attr&quot;&gt;duration&lt;/span&gt; = (endTime - startTime).toFixed(&lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;)&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;

    // 将结果发送回主线程
    self.postMessage({
      type: &#39;RESULT&#39;,
      data: { result, duration }
    })&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;
  }
}&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;

export {}&lt;span class=&quot;hljs-comment&quot;&gt;; // 确保这是一个模块&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-10&quot;&gt;3.3 第二步：封装 Custom Hook (核心)&lt;/h3&gt;
&lt;p&gt;为了让组件更干净，我们将 Worker 的创建、通信、销毁逻辑封装到&amp;nbsp;&lt;code&gt;useHeavyCalc&lt;/code&gt;&amp;nbsp;中。这里我们用到了&amp;nbsp;&lt;code&gt;useRef&lt;/code&gt;&amp;nbsp;来保存实例，用&amp;nbsp;&lt;code&gt;useState&lt;/code&gt;&amp;nbsp;来管理 UI 状态。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-typescript&quot; lang=&quot;typescript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useState, useEffect, useRef, useCallback } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-comment&quot;&gt;// 定义返回结果的状态接口&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;CalcState&lt;/span&gt; {
  &lt;span class=&quot;hljs-attr&quot;&gt;loading&lt;/span&gt;: &lt;span class=&quot;hljs-built_in&quot;&gt;boolean&lt;/span&gt;;
  &lt;span class=&quot;hljs-attr&quot;&gt;result&lt;/span&gt;: &lt;span class=&quot;hljs-built_in&quot;&gt;number&lt;/span&gt; | &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;
  &lt;span class=&quot;hljs-attr&quot;&gt;duration&lt;/span&gt;: &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; | &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;
  &lt;span class=&quot;hljs-attr&quot;&gt;error&lt;/span&gt;: &lt;span class=&quot;hljs-built_in&quot;&gt;string&lt;/span&gt; | &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useHeavyCalc&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-comment&quot;&gt;// 1. 使用 useRef 持久化保存 Worker 实例&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// 初始值为 null，避免在 SSR 或非浏览器环境报错&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; workerRef = useRef&amp;lt;&lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt; | &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;&amp;gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;);
  
  &lt;span class=&quot;hljs-comment&quot;&gt;// 2. 使用 useState 管理响应式的 UI 状态&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [state, setState] = useState&amp;lt;&lt;span class=&quot;hljs-title class_&quot;&gt;CalcState&lt;/span&gt;&amp;gt;({
    &lt;span class=&quot;hljs-attr&quot;&gt;loading&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;result&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;duration&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;error&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;,
  });

  &lt;span class=&quot;hljs-comment&quot;&gt;// 3. 初始化 Worker&lt;/span&gt;
  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-comment&quot;&gt;// 使用 Vite 特有的 new URL 语法导入 Worker，方便打包处理&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// 如果是 CRA 环境，通常使用 new Worker(new URL(&#39;./workers/calc.worker.ts&#39;, import.meta.url))&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(
      &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;URL&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;../workers/calc.worker.ts&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;meta&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;url&lt;/span&gt;)
    );

    &lt;span class=&quot;hljs-comment&quot;&gt;// 4. 监听 Worker 的回信&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
      &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { &lt;span class=&quot;hljs-keyword&quot;&gt;type&lt;/span&gt;, data } = e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;;
      
      &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;type&lt;/span&gt; === &lt;span class=&quot;hljs-string&quot;&gt;&#39;RESULT&#39;&lt;/span&gt;) {
        &lt;span class=&quot;hljs-title function_&quot;&gt;setState&lt;/span&gt;({
          &lt;span class=&quot;hljs-attr&quot;&gt;loading&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;,
          &lt;span class=&quot;hljs-attr&quot;&gt;result&lt;/span&gt;: data.&lt;span class=&quot;hljs-property&quot;&gt;result&lt;/span&gt;,
          &lt;span class=&quot;hljs-attr&quot;&gt;duration&lt;/span&gt;: data.&lt;span class=&quot;hljs-property&quot;&gt;duration&lt;/span&gt;,
          &lt;span class=&quot;hljs-attr&quot;&gt;error&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;,
        });
      }
    };

    &lt;span class=&quot;hljs-comment&quot;&gt;// 错误处理&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;onerror&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;err&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
      &lt;span class=&quot;hljs-title function_&quot;&gt;setState&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-params&quot;&gt;prev&lt;/span&gt; =&amp;gt;&lt;/span&gt; ({ ...prev, &lt;span class=&quot;hljs-attr&quot;&gt;loading&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;error&lt;/span&gt;: err.&lt;span class=&quot;hljs-property&quot;&gt;message&lt;/span&gt; }));
    };

    &lt;span class=&quot;hljs-comment&quot;&gt;// 5. 清理函数：组件卸载时终止 Worker，防止内存泄漏&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
      &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;) {
        workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;terminate&lt;/span&gt;();
        workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;
      }
    };
  }, []); &lt;span class=&quot;hljs-comment&quot;&gt;// 空依赖数组，确保只在挂载时执行一次&lt;/span&gt;

  &lt;span class=&quot;hljs-comment&quot;&gt;// 6. 暴露给组件调用的方法&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; startCalculation = &lt;span class=&quot;hljs-title function_&quot;&gt;useCallback&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;num: &lt;span class=&quot;hljs-built_in&quot;&gt;number&lt;/span&gt;&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (!workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;) &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt;;
    
    &lt;span class=&quot;hljs-title function_&quot;&gt;setState&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-params&quot;&gt;prev&lt;/span&gt; =&amp;gt;&lt;/span&gt; ({ ...prev, &lt;span class=&quot;hljs-attr&quot;&gt;loading&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;error&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt; }));
    
    &lt;span class=&quot;hljs-comment&quot;&gt;// 向 Worker 发送消息&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({
      &lt;span class=&quot;hljs-attr&quot;&gt;type&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;CALCULATE_FIB&#39;&lt;/span&gt;,
      &lt;span class=&quot;hljs-attr&quot;&gt;payload&lt;/span&gt;: num
    });
  }, []);

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; {
    ...state,
    startCalculation
  };
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-11&quot;&gt;3.4 第三步：UI 组件实现&lt;/h3&gt;
&lt;p&gt;现在，我们的 UI 组件变得非常纯粹，只负责展示和交互，完全不感知底层的线程通信细节。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-css&quot; lang=&quot;css&quot;&gt;import React, { useState } &lt;span class=&quot;hljs-selector-tag&quot;&gt;from&lt;/span&gt; &#39;react&#39;;
import { useHeavyCalc } &lt;span class=&quot;hljs-selector-tag&quot;&gt;from&lt;/span&gt; &#39;../hooks/useHeavyCalc&#39;;

const Calculator: React.FC = () =&amp;gt; {
  const &lt;span class=&quot;hljs-selector-attr&quot;&gt;[inputNum, setInputNum]&lt;/span&gt; = useState&amp;lt;number&amp;gt;(&lt;span class=&quot;hljs-number&quot;&gt;40&lt;/span&gt;);
  const { loading, result, duration, error, startCalculation } = useHeavyCalc();

  const handleStart = () =&amp;gt; {
    startCalculation(inputNum);
  };

  return (
    &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;div&lt;/span&gt; style={{ &lt;span class=&quot;hljs-attribute&quot;&gt;padding&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;20px&#39;&lt;/span&gt;, border: &lt;span class=&quot;hljs-string&quot;&gt;&#39;1px solid #ddd&#39;&lt;/span&gt;, borderRadius: &lt;span class=&quot;hljs-string&quot;&gt;&#39;8px&#39;&lt;/span&gt; }}&amp;gt;
      &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;h2&lt;/span&gt;&amp;gt;🚀 高性能计算器 (Web Worker)&amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;h2&lt;/span&gt;&amp;gt;
      &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;p&lt;/span&gt;&amp;gt;输入数字计算斐波那契数列（测试主线程阻塞情况）：&amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;p&lt;/span&gt;&amp;gt;
      
      &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;div&lt;/span&gt; style={{ &lt;span class=&quot;hljs-attribute&quot;&gt;display&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;flex&#39;&lt;/span&gt;, gap: &lt;span class=&quot;hljs-string&quot;&gt;&#39;10px&#39;&lt;/span&gt;, marginBottom: &lt;span class=&quot;hljs-string&quot;&gt;&#39;20px&#39;&lt;/span&gt; }}&amp;gt;
        &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;input&lt;/span&gt; 
          type=&quot;number&quot; 
          value={inputNum} 
          onChange={(e) =&amp;gt; setInputNum(Number(e&lt;span class=&quot;hljs-selector-class&quot;&gt;.target&lt;/span&gt;&lt;span class=&quot;hljs-selector-class&quot;&gt;.value&lt;/span&gt;))}
          disabled={loading}
          style={{ &lt;span class=&quot;hljs-attribute&quot;&gt;padding&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;8px&#39;&lt;/span&gt; }}
        /&amp;gt;
        &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;button&lt;/span&gt; 
          onClick={handleStart} 
          disabled={loading}
          style={{ 
            &lt;span class=&quot;hljs-attribute&quot;&gt;padding&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;8px 16px&#39;&lt;/span&gt;, 
            backgroundColor: loading ? &lt;span class=&quot;hljs-string&quot;&gt;&#39;#ccc&#39;&lt;/span&gt; : &lt;span class=&quot;hljs-string&quot;&gt;&#39;#007bff&#39;&lt;/span&gt;, 
            color: &lt;span class=&quot;hljs-string&quot;&gt;&#39;#fff&#39;&lt;/span&gt;, 
            border: &lt;span class=&quot;hljs-string&quot;&gt;&#39;none&#39;&lt;/span&gt;, 
            borderRadius: &lt;span class=&quot;hljs-string&quot;&gt;&#39;4px&#39;&lt;/span&gt;,
            cursor: loading ? &lt;span class=&quot;hljs-string&quot;&gt;&#39;not-allowed&#39;&lt;/span&gt; : &lt;span class=&quot;hljs-string&quot;&gt;&#39;pointer&#39;&lt;/span&gt;
          }}
        &amp;gt;
          {loading ? &#39;⏳ 计算中...&#39; : &lt;span class=&quot;hljs-string&quot;&gt;&#39;开始计算&#39;&lt;/span&gt;}
        &amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;button&lt;/span&gt;&amp;gt;
      &amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;div&lt;/span&gt;&amp;gt;

      {&lt;span class=&quot;hljs-comment&quot;&gt;/* 结果展示区域 */&lt;/span&gt;}
      {error &amp;amp;&amp;amp; &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;p&lt;/span&gt; style={{ &lt;span class=&quot;hljs-attribute&quot;&gt;color&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;red&#39;&lt;/span&gt; }}&amp;gt;❌ 错误: {error}&amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;p&lt;/span&gt;&amp;gt;}
      
      {result !== null &amp;amp;&amp;amp; !loading &amp;amp;&amp;amp; (
        &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;div&lt;/span&gt; style={{ marginTop: &lt;span class=&quot;hljs-string&quot;&gt;&#39;20px&#39;&lt;/span&gt;, padding: &lt;span class=&quot;hljs-string&quot;&gt;&#39;15px&#39;&lt;/span&gt;, background: &lt;span class=&quot;hljs-string&quot;&gt;&#39;#f9f9f9&#39;&lt;/span&gt; }}&amp;gt;
          &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;h3&lt;/span&gt;&amp;gt;✅ 计算完成&amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;h3&lt;/span&gt;&amp;gt;
          &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;p&lt;/span&gt;&amp;gt;&amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;strong&lt;/span&gt;&amp;gt;结果：&amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;strong&lt;/span&gt;&amp;gt; {result}&amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;p&lt;/span&gt;&amp;gt;
          &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;p&lt;/span&gt;&amp;gt;&amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;strong&lt;/span&gt;&amp;gt;耗时：&amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;strong&lt;/span&gt;&amp;gt; {duration} ms&amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;p&lt;/span&gt;&amp;gt;
          &amp;lt;&lt;span class=&quot;hljs-selector-tag&quot;&gt;p&lt;/span&gt; style={{ fontSize: &lt;span class=&quot;hljs-string&quot;&gt;&#39;12px&#39;&lt;/span&gt;, color: &lt;span class=&quot;hljs-string&quot;&gt;&#39;#666&#39;&lt;/span&gt; }}&amp;gt;
            * 注意：此时你可以随意点击页面其他按钮或滚动页面，不会卡顿。
          &amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;p&lt;/span&gt;&amp;gt;
        &amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;div&lt;/span&gt;&amp;gt;
      )}
    &amp;lt;/&lt;span class=&quot;hljs-selector-tag&quot;&gt;div&lt;/span&gt;&amp;gt;
  );
};

export default Calculator;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-12&quot;&gt;第四章：进阶思考与扩展场景&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-13&quot;&gt;4.1 为什么要这么麻烦？直接算不行吗？&lt;/h3&gt;
&lt;p&gt;如果你计算的是&amp;nbsp;&lt;code&gt;1+1&lt;/code&gt;&amp;nbsp;或者简单的列表过滤，当然不需要 Worker。但当你的任务复杂度达到&amp;nbsp;O(2n)O(2n)&amp;nbsp;或者需要处理 MB 级别的 JSON 数据时，区别就是“用户体验流畅”与“用户以为网页崩了”的区别。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-14&quot;&gt;4.2 更多应用场景&lt;/h3&gt;
&lt;p&gt;掌握了这套&amp;nbsp;&lt;code&gt;useRef + Worker&lt;/code&gt;&amp;nbsp;的模式，你可以将其应用到以下场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;前端图像处理&lt;/strong&gt;：利用 Worker 进行图片压缩、滤镜处理（配合 Canvas API 的 ImageBitmap）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;大文件解析&lt;/strong&gt;：在上传前解析巨大的 CSV 或 Excel 文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端侧 AI 推理&lt;/strong&gt;：运行 TensorFlow.js 或 ONNX Runtime Web 模型，这些模型推理非常耗时，必须放入 Worker。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;即时搜索高亮&lt;/strong&gt;：在几万条数据中进行正则匹配和高亮处理。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-id=&quot;heading-15&quot;&gt;4.3 注意事项&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据传输开销&lt;/strong&gt;：&lt;code&gt;postMessage&lt;/code&gt;&amp;nbsp;会复制数据。如果传输几兆的二进制数据，建议使用&amp;nbsp;&lt;code&gt;ArrayBuffer&lt;/code&gt;&amp;nbsp;并将其作为 Transferable Object 传递（&lt;code&gt;worker.postMessage(data, [data.buffer])&lt;/code&gt;），这样可以实现零拷贝，直接将内存所有权移交给 Worker。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兼容性&lt;/strong&gt;：现代浏览器对 Web Worker 支持良好，但在极低版本浏览器中可能需要 Polyfill。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调试&lt;/strong&gt;：Chrome DevTools 的 Sources 面板中有专门的 &quot;Threads&quot; 区域，可以切换查看 Main Thread 和 Worker Thread 的执行栈，调试非常方便。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-16&quot;&gt;结语&lt;/h2&gt;
&lt;p&gt;React 的核心在于声明式 UI，但这并不意味着我们要放弃对底层性能的掌控。通过&amp;nbsp;&lt;code&gt;useRef&lt;/code&gt;&amp;nbsp;这一看似简单的 Hook，我们巧妙地桥接了 React 的渲染世界与浏览器的多线程世界。&lt;/p&gt;
&lt;p&gt;希望这篇文章能帮你彻底搞懂 Web Worker 在 React 中的最佳实践。下次遇到卡顿，别再只会优化&amp;nbsp;&lt;code&gt;useMemo&lt;/code&gt;&amp;nbsp;了，试试把重担交给 Worker 吧！&lt;/p&gt;</description><link>https://juejin.cn/post/7671181898978181161</link><guid isPermaLink="false">https://juejin.cn/post/7671181898978181161</guid><pubDate>Fri, 07 Aug 2026 14:06:42 GMT</pubDate><author>渣波</author><category>前端</category><category>JavaScript</category></item><item><title>🔥 别再只用 useRef 拿 DOM 了！这 5 个实战场景让你彻底理解它（附源码解析 + 面试题）</title><description>&lt;h2 data-id=&quot;heading-0&quot;&gt;🔥 别再只用 useRef 拿 DOM 了！这 5 个实战场景让你彻底理解它（附源码解析 + 面试题）&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;摘要&lt;/strong&gt;：90% 的 React 开发者对 useRef 的认知停留在 &lt;code&gt;ref={inputRef}&lt;/code&gt; + &lt;code&gt;focus()&lt;/code&gt;。但 useRef 远不止于此——它是 React 给你的&quot;渲染之外的抽屉&quot;，能存 DOM、存值、存 Worker、破解闭包陷阱，甚至帮你实现子组件方法调用。这篇文章用 5 个递进场景 + 源码级原理 + 6 道面试题，帮你彻底搞懂 useRef。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-1&quot;&gt;🗺️ 文章大纲&lt;/h3&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;center&quot;&gt;序号&lt;/th&gt;&lt;th&gt;场景&lt;/th&gt;&lt;th&gt;核心知识点&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;难度&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;1&lt;/td&gt;&lt;td&gt;DOM 引用与自动聚焦&lt;/td&gt;&lt;td&gt;ref 挂载时序&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;⭐&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;2&lt;/td&gt;&lt;td&gt;不触发渲染的可变值&lt;/td&gt;&lt;td&gt;ref vs state 本质区别&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;⭐⭐&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;3&lt;/td&gt;&lt;td&gt;Web Worker 线程管理&lt;/td&gt;&lt;td&gt;持有重量级对象&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;⭐⭐&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;4&lt;/td&gt;&lt;td&gt;闭包陷阱中的逃生舱&lt;/td&gt;&lt;td&gt;ref 突破闭包捕获&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;⭐⭐⭐&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;5&lt;/td&gt;&lt;td&gt;子组件方法暴露&lt;/td&gt;&lt;td&gt;useImperativeHandle&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;⭐⭐⭐&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;读完这篇文章，你将获得：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对 useRef 从&quot;会用&quot;到&quot;懂原理&quot;的认知升级&lt;/li&gt;
&lt;li&gt;5 个可直接复用到项目中的实战代码&lt;/li&gt;
&lt;li&gt;6 道面试真题的深度解析&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-2&quot;&gt;📌 前言&lt;/h3&gt;
&lt;p&gt;这篇文章来自我的 useRef 深度学习笔记。&lt;/p&gt;
&lt;p&gt;之前我对 useRef 的认知停留在&quot;拿 DOM 用的&quot;，直到我在项目中遇到了三个&quot;意外&quot;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;code&gt;useRef&lt;/code&gt; 存一个数字，修改它居然&lt;strong&gt;不会触发重新渲染&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;useRef&lt;/code&gt; 存一个 Web Worker 实例，它居然能在渲染间&lt;strong&gt;持久存活&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;useEffect&lt;/code&gt; 的回调里，用 &lt;code&gt;useRef&lt;/code&gt; 成功&lt;strong&gt;突破了闭包陷阱&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这三个场景串起来，我才真正理解了 useRef 的设计哲学：&lt;strong&gt;它不是 DOM 工具，它是 React 的&quot;渲染之外的抽屉&quot;&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-3&quot;&gt;🎯 本文适合谁&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;学过 &lt;code&gt;useState&lt;/code&gt;、&lt;code&gt;useEffect&lt;/code&gt;，但对 &lt;code&gt;useRef&lt;/code&gt; 一知半解的 React 初学者&lt;/li&gt;
&lt;li&gt;知道 &lt;code&gt;useRef&lt;/code&gt; 能拿 DOM，但不知道它还能干嘛的同学&lt;/li&gt;
&lt;li&gt;面试前想搞清楚 &lt;code&gt;useRef&lt;/code&gt; vs &lt;code&gt;useState&lt;/code&gt; 区别的人&lt;/li&gt;
&lt;li&gt;想了解 useRef 源码级原理的进阶开发者&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-4&quot;&gt;📚 场景一：DOM 引用 —— 最经典的用法&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-5&quot;&gt;需求&lt;/h4&gt;
&lt;p&gt;页面加载后，输入框自动获取焦点，提升用户体验。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-6&quot;&gt;代码&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useRef, useEffect } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-comment&quot;&gt;// 1️⃣ 创建 ref，初始值为 null（此时 DOM 还没挂载）&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; inputRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;)

  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-comment&quot;&gt;// 3️⃣ 组件挂载后，inputRef.current 已经指向真实 DOM&lt;/span&gt;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(inputRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;)  &lt;span class=&quot;hljs-comment&quot;&gt;// &amp;lt;input type=&quot;text&quot; /&amp;gt;&lt;/span&gt;
    inputRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;focus&lt;/span&gt;()       &lt;span class=&quot;hljs-comment&quot;&gt;// 自动聚焦&lt;/span&gt;
  }, [])

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;input&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;text&quot;&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;placeholder&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;请输入用户名&quot;&lt;/span&gt;
      // &lt;span class=&quot;hljs-attr&quot;&gt;2&lt;/span&gt;️⃣ &lt;span class=&quot;hljs-attr&quot;&gt;通过&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;ref&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;属性&lt;/span&gt;，&lt;span class=&quot;hljs-attr&quot;&gt;把&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;DOM&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;节点&lt;/span&gt;&quot;&lt;span class=&quot;hljs-attr&quot;&gt;注入&lt;/span&gt;&quot;&lt;span class=&quot;hljs-attr&quot;&gt;到&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;inputRef.current&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;ref&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{inputRef}&lt;/span&gt;
    /&amp;gt;&lt;/span&gt;&lt;/span&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-7&quot;&gt;关键点：时序！&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ini&quot; lang=&quot;ini&quot;&gt;渲染阶段: &lt;span class=&quot;hljs-attr&quot;&gt;inputRef&lt;/span&gt; = { current: null }  ← DOM 还没创建
     ↓
React 创建 DOM，把 &amp;lt;input&amp;gt; 赋值给 inputRef.current
     ↓
effect 阶段: &lt;span class=&quot;hljs-attr&quot;&gt;inputRef.current&lt;/span&gt; = &amp;lt;input&amp;gt;  ← 现在可以操作了
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;一句话理解&lt;/strong&gt;：&lt;code&gt;useRef&lt;/code&gt; 就是一个&quot;盒子&quot;，渲染时是空的，挂载后 React 会把真实 DOM 放进去。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 data-id=&quot;heading-8&quot;&gt;为什么不用 useState？&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 错误示范：用 state 存 DOM&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [inputEl, setInputEl] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;)
&lt;span class=&quot;hljs-comment&quot;&gt;// setInputEl(dom) 会触发重新渲染 → 再触发 setInputEl → 死循环风险！&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 而且 DOM 不是&quot;状态&quot;，它是渲染的结果&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;对比&lt;/th&gt;&lt;th&gt;useRef&lt;/th&gt;&lt;th&gt;useState&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;修改时触发渲染&lt;/td&gt;&lt;td&gt;不触发&lt;/td&gt;&lt;td&gt;触发&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;适合存放&lt;/td&gt;&lt;td&gt;DOM 节点、不需渲染的值&lt;/td&gt;&lt;td&gt;需要驱动 UI 更新的数据&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;语义&lt;/td&gt;&lt;td&gt;&quot;引用&quot;——指向某个东西&lt;/td&gt;&lt;td&gt;&quot;状态&quot;——驱动 UI 的数据&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-9&quot;&gt;📚 场景二：可变值持久化 —— 不触发渲染的&quot;隐形变量&quot;&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-10&quot;&gt;需求&lt;/h4&gt;
&lt;p&gt;一个计数器，点击后数字增加，但&lt;strong&gt;不触发重新渲染&lt;/strong&gt;。这在性能敏感场景（如动画帧计数、事件触发频率统计）中非常有用。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-11&quot;&gt;代码&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useRef, useState, useCallback } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; numRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)       &lt;span class=&quot;hljs-comment&quot;&gt;// 引用一个值，初始为 0&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [, forceRender] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)  &lt;span class=&quot;hljs-comment&quot;&gt;// 用来手动触发渲染&lt;/span&gt;

  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;渲染了，numRef.current =&#39;&lt;/span&gt;, numRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;)

  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; handleClick = &lt;span class=&quot;hljs-title function_&quot;&gt;useCallback&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    numRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; += &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;   &lt;span class=&quot;hljs-comment&quot;&gt;// ⚡ 修改 ref.current 不会触发渲染！&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// 只有需要展示时才手动触发&lt;/span&gt;
    &lt;span class=&quot;hljs-title function_&quot;&gt;forceRender&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-params&quot;&gt;n&lt;/span&gt; =&amp;gt;&lt;/span&gt; n + &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;)
  }, [])

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{handleClick}&lt;/span&gt;&amp;gt;&lt;/span&gt;
      点击次数：{numRef.current}
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-12&quot;&gt;核心问题：为什么 &lt;code&gt;numRef.current += 1&lt;/code&gt; 不触发渲染？&lt;/h4&gt;
&lt;p&gt;这是理解 useRef 的关键。我们从 React 的调度机制来看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;useState&lt;/code&gt; 的 &lt;code&gt;setCount()&lt;/code&gt; → 调用 &lt;code&gt;dispatchAction&lt;/code&gt; → 向 Fiber 节点的 &lt;code&gt;lanes&lt;/code&gt; 注入更新优先级 → React 调度重新渲染&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useRef&lt;/code&gt; 的 &lt;code&gt;.current = xx&lt;/code&gt; → 直接修改 &lt;code&gt;{ current: xx }&lt;/code&gt; 这个普通对象的属性 → &lt;strong&gt;React 的调度系统完全不知道这件事&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用一个形象的比喻：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-makefile&quot; lang=&quot;makefile&quot;&gt;&lt;span class=&quot;hljs-section&quot;&gt;setState:  我变了！→ React 调度器: 收到，排期重新渲染！&lt;/span&gt;
&lt;span class=&quot;hljs-section&quot;&gt;ref:       我变了。→ React 调度器: ...（没收到信号，继续摸鱼）&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-13&quot;&gt;源码视角：ref 在 Fiber 中是怎么存的？&lt;/h4&gt;
&lt;p&gt;在 React 的 Fiber 架构中，每个组件对应一个 Fiber 节点。&lt;code&gt;useRef&lt;/code&gt; 创建的对象会被挂载到 &lt;code&gt;fiber.memoizedState&lt;/code&gt; 上：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 简化的 React 源码（react-reconciler）&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;initialValue&lt;/span&gt;) {
  &lt;span class=&quot;hljs-comment&quot;&gt;// 复用上一次渲染的 ref 对象（如果有的话）&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; ref = { &lt;span class=&quot;hljs-attr&quot;&gt;current&lt;/span&gt;: initialValue }
  &lt;span class=&quot;hljs-comment&quot;&gt;// 挂载到 fiber.memoizedState 链表上&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// 下次渲染时，React 会跳过 ref 的创建，直接复用同一个对象&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; ref
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键点：&lt;strong&gt;ref 对象在渲染间是同一个引用&lt;/strong&gt;。你修改 &lt;code&gt;.current&lt;/code&gt; 只是改了对象属性，React 的 reconciler 不会对比 ref 对象的内容变化，所以不会触发重渲染。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-14&quot;&gt;什么时候用 useRef 存值？&lt;/h4&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;/th&gt;&lt;th&gt;用什么&lt;/th&gt;&lt;th&gt;原因&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;需要显示在界面上的数字&lt;/td&gt;&lt;td&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/td&gt;&lt;td&gt;变更需要触发渲染&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;上一次渲染的值&lt;/td&gt;&lt;td&gt;&lt;code&gt;useRef&lt;/code&gt;&lt;/td&gt;&lt;td&gt;不需要触发渲染，只需记录&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;定时器 ID&lt;/td&gt;&lt;td&gt;&lt;code&gt;useRef&lt;/code&gt;&lt;/td&gt;&lt;td&gt;存起来以便清除，不需渲染&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;动画帧 ID&lt;/td&gt;&lt;td&gt;&lt;code&gt;useRef&lt;/code&gt;&lt;/td&gt;&lt;td&gt;同上&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;表单的&quot;脏&quot;标记&lt;/td&gt;&lt;td&gt;&lt;code&gt;useRef&lt;/code&gt;&lt;/td&gt;&lt;td&gt;标记状态，不需渲染&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;事件触发频率统计&lt;/td&gt;&lt;td&gt;&lt;code&gt;useRef&lt;/code&gt;&lt;/td&gt;&lt;td&gt;高频更新，不应触发渲染&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-15&quot;&gt;📚 场景三：Web Worker 线程 —— 持有&quot;重量级对象&quot;&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-16&quot;&gt;需求&lt;/h4&gt;
&lt;p&gt;主线程执行一个耗时计算（如大数组排序、图像处理），页面被&quot;卡死&quot;了。解决方案：用 Web Worker 把计算放到子线程。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-17&quot;&gt;代码&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === App.jsx ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useRef, useEffect } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-comment&quot;&gt;// ref 引用了 worker 线程，初始为 null&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; workerRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;)

  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-comment&quot;&gt;// 开启一个 Worker 线程（开销比较大，只创建一次）&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(
      &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;URL&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&quot;./worker.js&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;meta&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;url&lt;/span&gt;)
    )

    &lt;span class=&quot;hljs-comment&quot;&gt;// 监听 Worker 返回的结果&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
      &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;计算结果：&#39;&lt;/span&gt;, e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;)
    }

    &lt;span class=&quot;hljs-comment&quot;&gt;// 清理：组件卸载时终止 Worker&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
      workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;?.&lt;span class=&quot;hljs-title function_&quot;&gt;terminate&lt;/span&gt;()
    }
  }, [])  &lt;span class=&quot;hljs-comment&quot;&gt;// 空依赖 → 只在挂载时执行一次&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;handleCalculate&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
    &lt;span class=&quot;hljs-comment&quot;&gt;// 通过 ref 操作 Worker，发送数据到子线程&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;?.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;data&lt;/span&gt;: [&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;] })
  }

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{handleCalculate}&lt;/span&gt;&amp;gt;&lt;/span&gt;开始计算&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === worker.js ===&lt;/span&gt;
self.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { data } = e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// 模拟耗时计算&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; result = data.&lt;span class=&quot;hljs-title function_&quot;&gt;reduce&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;sum, n&lt;/span&gt;) =&amp;gt;&lt;/span&gt; sum + n * n, &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)
  self.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;(result)  &lt;span class=&quot;hljs-comment&quot;&gt;// 把结果发回主线程&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-18&quot;&gt;为什么用 useRef 而不是 useState？&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 用 useState 存 Worker&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [worker, setWorker] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;)
&lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-title function_&quot;&gt;setWorker&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(...))  &lt;span class=&quot;hljs-comment&quot;&gt;// 触发重新渲染！Worker 实例被重建！&lt;/span&gt;
}, [])

&lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 用 useRef 存 Worker&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; workerRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;)
&lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(...)  &lt;span class=&quot;hljs-comment&quot;&gt;// 不触发渲染，纯粹的&quot;保管&quot;&lt;/span&gt;
}, [])
&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;对比&lt;/th&gt;&lt;th&gt;useRef&lt;/th&gt;&lt;th&gt;useState&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;赋值触发渲染&lt;/td&gt;&lt;td&gt;不触发&lt;/td&gt;&lt;td&gt;触发&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;适合存放&lt;/td&gt;&lt;td&gt;Worker、定时器、DOM 等&quot;工具对象&quot;&lt;/td&gt;&lt;td&gt;需要展示在 UI 上的数据&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;跨渲染保持&lt;/td&gt;&lt;td&gt;同一个引用&lt;/td&gt;&lt;td&gt;同一个引用&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;核心区别&lt;/strong&gt;：Worker 是一个&quot;工具&quot;，不是&quot;数据&quot;。它不需要显示在界面上，你只是需要一个地方&quot;保管&quot;它。&lt;code&gt;useRef&lt;/code&gt; 就是那个&quot;保管箱&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-19&quot;&gt;📚 场景四：闭包陷阱中的逃生舱 —— 面试高频考点&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-20&quot;&gt;需求&lt;/h4&gt;
&lt;p&gt;一个组件中有一个 &lt;code&gt;count&lt;/code&gt; state，还有一个定时器每秒打印当前 count。你期望定时器打印最新的 count，但它永远打印初始值。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-21&quot;&gt;闭包陷阱复现&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useState, useEffect } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [count, setCount] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)

  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; timer = &lt;span class=&quot;hljs-built_in&quot;&gt;setInterval&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
      &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;count =&#39;&lt;/span&gt;, count)  &lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 永远是 0！&lt;/span&gt;
    }, &lt;span class=&quot;hljs-number&quot;&gt;1000&lt;/span&gt;)
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;clearInterval&lt;/span&gt;(timer)
  }, [])  &lt;span class=&quot;hljs-comment&quot;&gt;// 空依赖 → effect 只执行一次 → 闭包捕获了初始的 count&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{()&lt;/span&gt; =&amp;gt;&lt;/span&gt; setCount(c =&amp;gt; c + 1)}&amp;gt;
      count: {count}
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么？&lt;/strong&gt; 因为 &lt;code&gt;useEffect&lt;/code&gt; 的回调函数在组件首次渲染时创建，它&quot;闭包捕获&quot;了当时的 &lt;code&gt;count&lt;/code&gt;（值为 0）。由于依赖数组是 &lt;code&gt;[]&lt;/code&gt;，这个回调不会重建，所以永远看到 &lt;code&gt;count = 0&lt;/code&gt;。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-22&quot;&gt;用 useRef 破解闭包陷阱&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useState, useRef, useEffect } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [count, setCount] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; countRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(count)  &lt;span class=&quot;hljs-comment&quot;&gt;// 用 ref 跟踪最新值&lt;/span&gt;

  &lt;span class=&quot;hljs-comment&quot;&gt;// 每次 count 变化时，同步更新 ref&lt;/span&gt;
  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    countRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = count
  }, [count])

  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; timer = &lt;span class=&quot;hljs-built_in&quot;&gt;setInterval&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
      &lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 通过 ref 读取最新值，突破闭包捕获&lt;/span&gt;
      &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;最新 count =&#39;&lt;/span&gt;, countRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;)
    }, &lt;span class=&quot;hljs-number&quot;&gt;1000&lt;/span&gt;)
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-built_in&quot;&gt;clearInterval&lt;/span&gt;(timer)
  }, [])

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{()&lt;/span&gt; =&amp;gt;&lt;/span&gt; setCount(c =&amp;gt; c + 1)}&amp;gt;
      count: {count}
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-23&quot;&gt;原理分析&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ini&quot; lang=&quot;ini&quot;&gt;闭包捕获的是&quot;值&quot;：&lt;span class=&quot;hljs-attr&quot;&gt;count&lt;/span&gt; = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;（数字，不可变）
ref 存储的是&quot;引用&quot;：&lt;span class=&quot;hljs-attr&quot;&gt;countRef&lt;/span&gt; = { current: &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt; }（对象，可变）

每次 setCount → 触发渲染 → useEffect 同步更新 countRef.current
定时器通过 countRef.current 读取 → 永远是最新值
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;面试金句&lt;/strong&gt;：闭包捕获的是&quot;快照&quot;，ref 持有的是&quot;活引用&quot;。当闭包内的值需要保持最新时，用 ref 做&quot;逃生舱&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-24&quot;&gt;📚 场景五：子组件方法暴露 —— useImperativeHandle&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-25&quot;&gt;需求&lt;/h4&gt;
&lt;p&gt;父组件需要调用子组件内部的方法（如让子组件的 input 聚焦、让子组件的视频播放/暂停）。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-26&quot;&gt;代码&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === 子组件 ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useRef, useImperativeHandle, forwardRef } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;CustomInput&lt;/span&gt; = &lt;span class=&quot;hljs-title function_&quot;&gt;forwardRef&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;props, ref&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; inputRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;)

  &lt;span class=&quot;hljs-comment&quot;&gt;// 暴露给父组件的方法（白名单机制）&lt;/span&gt;
  &lt;span class=&quot;hljs-title function_&quot;&gt;useImperativeHandle&lt;/span&gt;(ref, &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; ({
    &lt;span class=&quot;hljs-attr&quot;&gt;focus&lt;/span&gt;: &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; inputRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;focus&lt;/span&gt;(),
    &lt;span class=&quot;hljs-attr&quot;&gt;clear&lt;/span&gt;: &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; { inputRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;value&lt;/span&gt; = &lt;span class=&quot;hljs-string&quot;&gt;&#39;&#39;&lt;/span&gt; },
    &lt;span class=&quot;hljs-attr&quot;&gt;getValue&lt;/span&gt;: &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; inputRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;value&lt;/span&gt;
  }))

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;input&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;ref&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{inputRef}&lt;/span&gt; {&lt;span class=&quot;hljs-attr&quot;&gt;...props&lt;/span&gt;} /&amp;gt;&lt;/span&gt;&lt;/span&gt;
})
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === 父组件 ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useRef } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; inputRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;)

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;CustomInput&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;ref&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{inputRef}&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;placeholder&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;请输入&quot;&lt;/span&gt; /&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{()&lt;/span&gt; =&amp;gt;&lt;/span&gt; inputRef.current.focus()}&amp;gt;聚焦&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{()&lt;/span&gt; =&amp;gt;&lt;/span&gt; inputRef.current.clear()}&amp;gt;清空&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{()&lt;/span&gt; =&amp;gt;&lt;/span&gt; alert(inputRef.current.getValue())}&amp;gt;获取值&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-27&quot;&gt;为什么需要 useImperativeHandle？&lt;/h4&gt;
&lt;p&gt;直接把 ref 传给子组件也能拿到 DOM，但这样做有两个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;破坏封装&lt;/strong&gt;：父组件可以调用 DOM 的任何方法，子组件失去对自身 API 的控制&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不利于重构&lt;/strong&gt;：子组件内部实现变了（比如 input 换成了 contentEditable），父组件代码也要跟着改&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;useImperativeHandle&lt;/code&gt; 的白名单机制，让子组件&lt;strong&gt;只暴露必要的方法&lt;/strong&gt;，实现了解耦。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-28&quot;&gt;🧠 五个场景总结&lt;/h3&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;/th&gt;&lt;th&gt;useRef 存的是&lt;/th&gt;&lt;th&gt;为什么不用 useState&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;DOM 引用&lt;/td&gt;&lt;td&gt;真实 DOM 节点&lt;/td&gt;&lt;td&gt;DOM 不是状态，且 setState 会触发无意义渲染&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;可变值&lt;/td&gt;&lt;td&gt;不需要渲染的计数/标记&lt;/td&gt;&lt;td&gt;ref 变更对 React 不可见，避免不必要的渲染&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Worker 线程&lt;/td&gt;&lt;td&gt;重量级的&quot;工具对象&quot;&lt;/td&gt;&lt;td&gt;工具对象不应参与渲染循环&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;闭包陷阱&lt;/td&gt;&lt;td&gt;最新值的&quot;活引用&quot;&lt;/td&gt;&lt;td&gt;闭包捕获的是快照，ref 是可变引用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;方法暴露&lt;/td&gt;&lt;td&gt;子组件 API 的桥接&lt;/td&gt;&lt;td&gt;需要配合 forwardRef 实现跨组件引用传递&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h4 data-id=&quot;heading-29&quot;&gt;一句话理解 useRef&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;useRef&lt;/code&gt; 是 React 给你的一个&quot;渲染之外的抽屉&quot;。你可以把任何东西放进去（DOM、数字、Worker、定时器 ID...），修改它不会触发渲染，但它会在组件的整个生命周期内保持不变。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-30&quot;&gt;❓ 面试官可能会问（6 道高频题）&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-31&quot;&gt;Q1：useRef 和直接声明一个变量 &lt;code&gt;let count = 0&lt;/code&gt; 有什么区别？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;：函数组件每次渲染都会重新执行函数体，&lt;code&gt;let count = 0&lt;/code&gt; 每次都会重新声明并重置为 0。而 &lt;code&gt;useRef&lt;/code&gt; 返回的对象在渲染间始终是同一个引用，&lt;code&gt;.current&lt;/code&gt; 的值会跨渲染保持。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; count = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;           &lt;span class=&quot;hljs-comment&quot;&gt;// 每次渲染都是 0（重新声明）&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; ref = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)   &lt;span class=&quot;hljs-comment&quot;&gt;// 只有第一次是 0，之后保持上次的值&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{()&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
    count += 1            // 点击后 count = 1，但下次渲染又变回 0
    ref.current += 1      // 点击后 ref.current = 1，下次渲染还是 1
  }}&amp;gt;点击&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-32&quot;&gt;Q2：useRef 可以替代 useState 吗？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;：不能。ref 变更不触发渲染，所以如果你需要界面上显示最新值，必须用 useState。但你可以用 ref 来&quot;辅助&quot; state，比如记录上一次的值（&lt;code&gt;usePrevious&lt;/code&gt; 自定义 Hook）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 自定义 Hook：获取上一次的值&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;usePrevious&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;value&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; ref = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;()
  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    ref.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = value  &lt;span class=&quot;hljs-comment&quot;&gt;// 每次渲染后更新 ref&lt;/span&gt;
  })
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; ref.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;     &lt;span class=&quot;hljs-comment&quot;&gt;// 返回上一次的值&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-33&quot;&gt;Q3：useRef 如何帮助解决闭包陷阱？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;：闭包捕获的是变量的&quot;快照值&quot;，而 ref 存储的是一个&quot;可变引用对象&quot;。通过在 effect 中同步更新 &lt;code&gt;ref.current&lt;/code&gt;，可以让闭包内的回调函数通过 &lt;code&gt;ref.current&lt;/code&gt; 始终读到最新值，从而突破闭包的捕获限制。详见本文场景四。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-34&quot;&gt;Q4：useRef 在 Fiber 架构中是怎么存储的？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;：&lt;code&gt;useRef&lt;/code&gt; 创建的 &lt;code&gt;{ current: initialValue }&lt;/code&gt; 对象会被挂载到 Fiber 节点的 &lt;code&gt;memoizedState&lt;/code&gt; 链表上。首次渲染时创建对象，后续渲染时 React 复用同一个引用。修改 &lt;code&gt;.current&lt;/code&gt; 不会触发调度器的 &lt;code&gt;scheduleUpdate&lt;/code&gt;，因为它不经过 &lt;code&gt;dispatchAction&lt;/code&gt; 流程。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-35&quot;&gt;Q5：useImperativeHandle 的作用是什么？为什么需要它？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;：&lt;code&gt;useImperativeHandle&lt;/code&gt; 配合 &lt;code&gt;forwardRef&lt;/code&gt; 使用，让子组件可以&lt;strong&gt;自定义暴露给父组件的 ref 内容&lt;/strong&gt;。它实现了&quot;白名单&quot;机制——父组件只能调用子组件明确暴露的方法，而不是直接访问整个 DOM。这提高了组件的封装性和可维护性。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-36&quot;&gt;Q6：在 React 19 中，forwardRef 还需要吗？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;A&lt;/strong&gt;：React 19 中，&lt;code&gt;ref&lt;/code&gt; 已经作为普通 prop 直接传递给函数组件，不再需要 &lt;code&gt;forwardRef&lt;/code&gt; 包裹。但 &lt;code&gt;useImperativeHandle&lt;/code&gt; 仍然有用，因为它控制的是&quot;暴露什么&quot;，而不是&quot;如何传递 ref&quot;。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-37&quot;&gt;⚠️ 常见踩坑提醒&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-38&quot;&gt;踩坑 1：ref.current 变化不会触发 useEffect&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; countRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)

&lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;countRef.current 变了&#39;&lt;/span&gt;, countRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;)
  &lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 这个 effect 永远不会因为 countRef.current 变化而重新执行&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// 因为 React 不追踪 ref 的变更&lt;/span&gt;
}, [countRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;])  &lt;span class=&quot;hljs-comment&quot;&gt;// 注意：这个依赖不会生效！&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-39&quot;&gt;踩坑 2：不能在渲染期间读写 ref（Strict Mode 下会报错）&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; countRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)

  &lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 渲染期间修改 ref（React 18 Strict Mode 会警告）&lt;/span&gt;
  countRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; += &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;

  &lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 应该在 effect 或事件处理函数中修改&lt;/span&gt;
  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    countRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; += &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;
  }, [])
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-40&quot;&gt;🔗 参考资料&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Freact.dev%2Freference%2Freact%2FuseRef&quot; target=&quot;_blank&quot; title=&quot;https://react.dev/reference/react/useRef&quot; ref=&quot;nofollow noopener noreferrer&quot;&gt;React 官方文档 - useRef&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Freact.dev%2Flearn%2Freferencing-values-with-refs&quot; target=&quot;_blank&quot; title=&quot;https://react.dev/learn/referencing-values-with-refs&quot; ref=&quot;nofollow noopener noreferrer&quot;&gt;React 官方文档 - Referencing Values with Refs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://link.juejin.cn/?target=https%3A%2F%2Freact.dev%2Freference%2Freact%2FuseImperativeHandle&quot; target=&quot;_blank&quot; title=&quot;https://react.dev/reference/react/useImperativeHandle&quot; ref=&quot;nofollow noopener noreferrer&quot;&gt;React 官方文档 - useImperativeHandle&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-41&quot;&gt;📣 写在最后&lt;/h3&gt;
&lt;p&gt;useRef 是 React 中最容易被低估的 Hook。大多数人只知道它能拿 DOM，但它真正的价值在于：&lt;strong&gt;提供了一个渲染周期之外的、持久的、可变的存储空间&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果你觉得这篇文章帮你理清了 useRef 的脉络，麻烦点个 &lt;strong&gt;赞&lt;/strong&gt; 👍 收个 &lt;strong&gt;藏&lt;/strong&gt; ⭐，你的支持是我持续输出的最大动力！&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;你在项目中用 useRef 做过什么&quot;骚操作&quot;？欢迎在评论区分享，我们一起交流！&lt;/strong&gt; 💬&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🔔 &lt;strong&gt;下期预告&lt;/strong&gt;：&lt;code&gt;useMemo&lt;/code&gt; 和 &lt;code&gt;useCallback&lt;/code&gt; 到底什么时候该用？90% 的人都用错了！关注我，不迷路。&lt;/p&gt;
&lt;/blockquote&gt;</description><link>https://juejin.cn/post/7671120692242612230</link><guid isPermaLink="false">https://juejin.cn/post/7671120692242612230</guid><pubDate>Fri, 07 Aug 2026 13:53:19 GMT</pubDate><author>ReBound</author><category>前端</category><category>JavaScript</category><category>React.js</category></item><item><title>🍉 我把《天龙八部》喂给了 AI，问它&quot;段誉会什么武功？&quot;答案让我惊呆了</title><description>&lt;blockquote&gt;
&lt;p&gt;写在前面：今天这节课太有趣了！之前我们做的 RAG 都是小打小闹——手动写几篇&quot;光光和东东&quot;的故事。今天老师直接把《天龙八部》的 EPUB 电子书扔进了 Milvus 向量数据库！Loader 加载 → Splitter 分块 → Embedding 向量化 → Milvus 存储 → 语义检索 → LLM 生成回答——一整条 RAG 流水线完整跑通了。然后我问：&quot;段誉会什么武功？&quot; AI 答上来了。我再问：&quot;鸠摩智会什么武功？&quot; AI 又答上来了。那一刻我感觉自己拥有了一个&quot;AI 版的金庸百科&quot;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-0&quot;&gt;一、RAG 完整链路：从文件到回答&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-1&quot;&gt;1.1 天龙八部 RAG 的&quot;五步法&quot;&lt;/h3&gt;
&lt;p&gt;老师说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;我们学习了 Loader、Splitter、Milvus、RAG 流程完整跑通了。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ini&quot; lang=&quot;ini&quot;&gt;天龙八部.epub（电子书文件）
  ↓ ① Loader（EPubLoader）
Document 对象数组（按章节分）
  ↓ ② Splitter（RecursiveCharacterTextSplitter）
小的 Document 切片（&lt;span class=&quot;hljs-attr&quot;&gt;chunkSize&lt;/span&gt;=&lt;span class=&quot;hljs-number&quot;&gt;500&lt;/span&gt;）
  ↓ ③ Embedding（text-embedding-v3）
1024 维向量
  ↓ ④ Milvus 向量数据库（存储 + 索引）
持久化存储，IVF_FLAT 索引加速
  ↓ ⑤ RAG 检索 + 生成
问题 → 向量化 → 搜索 → 增强 Prompt → LLM 回答
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这五步串起来，就是一个完整的生产级 RAG 流程。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-2&quot;&gt;二、第一步：EPubLoader 加载电子书&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-3&quot;&gt;2.1 EPUB 是什么？&lt;/h3&gt;
&lt;p&gt;EPUB 是电子书的标准格式，金庸全集、网络小说大多是这个格式。&lt;/p&gt;
&lt;p&gt;LangChain 提供了 &lt;code&gt;EPubLoader&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;EPubLoader&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;@langchain/community/document_loaders/fs/epub&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; loader = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;EPubLoader&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;./天龙八部.epub&#39;&lt;/span&gt;, {
    &lt;span class=&quot;hljs-attr&quot;&gt;splitChapters&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;,  &lt;span class=&quot;hljs-comment&quot;&gt;// 按章节拆分&lt;/span&gt;
});
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; documents = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; loader.&lt;span class=&quot;hljs-title function_&quot;&gt;load&lt;/span&gt;();
&lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;`加载完成，共&lt;span class=&quot;hljs-subst&quot;&gt;${documents.length}&lt;/span&gt;个章节`&lt;/span&gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;splitChapters: true&lt;/code&gt; 表示按照电子书的章节自动拆分成多个 Document。&lt;/strong&gt; 每章一个 Document。&lt;/p&gt;
&lt;p&gt;老师说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Loader 从各种来源加载文档。&lt;code&gt;EPUB、CSV……&lt;/code&gt;，相应的 Loader 加载器？LangChain 有 180 多种。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-4&quot;&gt;三、第二步：文本切分&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-5&quot;&gt;3.1 RecursiveCharacterTextSplitter&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;RecursiveCharacterTextSplitter&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@langchain/textsplitters&quot;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; textSplitter = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;RecursiveCharacterTextSplitter&lt;/span&gt;({
    &lt;span class=&quot;hljs-attr&quot;&gt;chunkSize&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;500&lt;/span&gt;,      &lt;span class=&quot;hljs-comment&quot;&gt;// 每块约 500 个字符&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;chunkOverlap&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;50&lt;/span&gt;,    &lt;span class=&quot;hljs-comment&quot;&gt;// 重叠 50 个字符，保持语义连贯&lt;/span&gt;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;每章可能有几千字，不能整章向量化——太大了检索不精准。需要切成语义完整的块。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;老师说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;切割符号——&lt;code&gt;。！？&lt;/code&gt;，&lt;code&gt;chunk_size&lt;/code&gt; 大小，&lt;code&gt;overlap&lt;/code&gt; 重叠。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-6&quot;&gt;四、第三步：流式处理，一边切一边向量化&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-7&quot;&gt;4.1 按章节分批处理&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; chapterIndex = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; chapterIndex &amp;lt; documentLen; chapterIndex++) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; chapter = documents[chapterIndex];
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; chunks = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; textSplitter.&lt;span class=&quot;hljs-title function_&quot;&gt;splitText&lt;/span&gt;(chapter.&lt;span class=&quot;hljs-property&quot;&gt;pageContent&lt;/span&gt;);
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;`拆分为&lt;span class=&quot;hljs-subst&quot;&gt;${chunks.length}&lt;/span&gt;个数据切片`&lt;/span&gt;);
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; insertedCount = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;insertChunksBatch&lt;/span&gt;(chunks, bookId, chapterIndex + &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;);
    totalInserted += insertedCount;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;老师设计了一个&quot;流式处理&quot;方案——不一次把所有内容加载到内存，而是一章一章处理。&lt;/strong&gt; 每章切完立即向量化、插入数据库。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-8&quot;&gt;4.2 向量化并插入&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;insertChunksBatch&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;chunks, bookId, chapterNum&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; insertData = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Promise&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;all&lt;/span&gt;(
        chunks.&lt;span class=&quot;hljs-title function_&quot;&gt;map&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;async&lt;/span&gt; (chunk, chunkIndex) =&amp;gt; {
            &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; vector = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;getEmbedding&lt;/span&gt;(chunk);
            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; {
                &lt;span class=&quot;hljs-attr&quot;&gt;id&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;`&lt;span class=&quot;hljs-subst&quot;&gt;${bookId}&lt;/span&gt;_&lt;span class=&quot;hljs-subst&quot;&gt;${chapterNum}&lt;/span&gt;_&lt;span class=&quot;hljs-subst&quot;&gt;${chunkIndex}&lt;/span&gt;`&lt;/span&gt;,
                &lt;span class=&quot;hljs-attr&quot;&gt;book_id&lt;/span&gt;: bookId,
                &lt;span class=&quot;hljs-attr&quot;&gt;book_name&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;天龙八部&#39;&lt;/span&gt;,
                &lt;span class=&quot;hljs-attr&quot;&gt;chapter_num&lt;/span&gt;: chapterNum,
                &lt;span class=&quot;hljs-attr&quot;&gt;index&lt;/span&gt;: chunkIndex,
                &lt;span class=&quot;hljs-attr&quot;&gt;content&lt;/span&gt;: chunk,
                &lt;span class=&quot;hljs-attr&quot;&gt;vector&lt;/span&gt;: vector,
            };
        })
    );

    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; insertResult = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; client.&lt;span class=&quot;hljs-title function_&quot;&gt;insert&lt;/span&gt;({
        &lt;span class=&quot;hljs-attr&quot;&gt;collection_name&lt;/span&gt;: &lt;span class=&quot;hljs-variable constant_&quot;&gt;COLLECTION_NAME&lt;/span&gt;,
        &lt;span class=&quot;hljs-attr&quot;&gt;data&lt;/span&gt;: insertData,
    });

    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Number&lt;/span&gt;(insertResult.&lt;span class=&quot;hljs-property&quot;&gt;insert_cnt&lt;/span&gt;) || &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;每条切片在插入前都生成 1024 维向量&lt;/strong&gt;，和切片内容一起存入 Milvus。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-9&quot;&gt;五、Milvus 集合设计&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-10&quot;&gt;5.1 创建集合&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; client.&lt;span class=&quot;hljs-title function_&quot;&gt;createCollection&lt;/span&gt;({
    &lt;span class=&quot;hljs-attr&quot;&gt;collection_name&lt;/span&gt;: &lt;span class=&quot;hljs-variable constant_&quot;&gt;COLLECTION_NAME&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;fields&lt;/span&gt;: [
        { &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;id&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;data_type&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;DataType&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;VarChar&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;max_length&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;100&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;is_primary_key&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; },
        { &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;book_id&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;data_type&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;DataType&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;VarChar&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;max_length&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;100&lt;/span&gt; },
        { &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;book_name&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;data_type&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;DataType&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;VarChar&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;max_length&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;200&lt;/span&gt; },
        { &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;chapter_num&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;data_type&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;DataType&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;Int32&lt;/span&gt; },
        { &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;index&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;data_type&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;DataType&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;Int32&lt;/span&gt; },
        { &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;content&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;data_type&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;DataType&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;VarChar&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;max_length&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;10000&lt;/span&gt; },
        { &lt;span class=&quot;hljs-attr&quot;&gt;name&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;vector&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;data_type&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;DataType&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;FloatVector&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;dim&lt;/span&gt;: &lt;span class=&quot;hljs-variable constant_&quot;&gt;VECTOR_DIM&lt;/span&gt; },
    ]
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这个集合设计包含了所有需要的信息：&lt;/strong&gt;&lt;/p&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;字段&lt;/th&gt;&lt;th&gt;用途&lt;/th&gt;&lt;th&gt;举例&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;id&lt;/code&gt;&lt;/td&gt;&lt;td&gt;唯一标识&lt;/td&gt;&lt;td&gt;&lt;code&gt;1_1_0&lt;/code&gt;（第 1 章第 0 块）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;book_id&lt;/code&gt;&lt;/td&gt;&lt;td&gt;书的编号&lt;/td&gt;&lt;td&gt;支持多本书&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;book_name&lt;/code&gt;&lt;/td&gt;&lt;td&gt;书名&lt;/td&gt;&lt;td&gt;天龙八部&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;chapter_num&lt;/code&gt;&lt;/td&gt;&lt;td&gt;章节号&lt;/td&gt;&lt;td&gt;第 3 章&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;index&lt;/code&gt;&lt;/td&gt;&lt;td&gt;切片序号&lt;/td&gt;&lt;td&gt;第 5 块&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;content&lt;/code&gt;&lt;/td&gt;&lt;td&gt;原文内容&lt;/td&gt;&lt;td&gt;&quot;段誉跟随钟灵……&quot;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;vector&lt;/code&gt;&lt;/td&gt;&lt;td&gt;1024 维语义向量&lt;/td&gt;&lt;td&gt;用于相似度搜索&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 data-id=&quot;heading-11&quot;&gt;5.2 创建索引&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; client.&lt;span class=&quot;hljs-title function_&quot;&gt;createIndex&lt;/span&gt;({
    &lt;span class=&quot;hljs-attr&quot;&gt;collection_name&lt;/span&gt;: &lt;span class=&quot;hljs-variable constant_&quot;&gt;COLLECTION_NAME&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;field_name&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&#39;vector&#39;&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;index_type&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;IndexType&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;IVF_FLAT&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;metric_type&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;MetricType&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;COSINE&lt;/span&gt;,
    &lt;span class=&quot;hljs-attr&quot;&gt;params&lt;/span&gt;: { &lt;span class=&quot;hljs-attr&quot;&gt;nlist&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;1024&lt;/span&gt; }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;老师说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;&lt;code&gt;IVF_FLAT&lt;/code&gt;——聚簇索引，毫秒级。&lt;code&gt;COSINE&lt;/code&gt;——高维相似度，数据量大了也不慢。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-12&quot;&gt;六、RAG 检索与生成&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-13&quot;&gt;6.1 搜索相关片段&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;retrieveRelevantContent&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;question, k = &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; queryVector = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;getEmbeddings&lt;/span&gt;(question);
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; searchResult = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; client.&lt;span class=&quot;hljs-title function_&quot;&gt;search&lt;/span&gt;({
        &lt;span class=&quot;hljs-attr&quot;&gt;collection_name&lt;/span&gt;: &lt;span class=&quot;hljs-variable constant_&quot;&gt;COLLECTION_NAME&lt;/span&gt;,
        &lt;span class=&quot;hljs-attr&quot;&gt;vector&lt;/span&gt;: queryVector,
        &lt;span class=&quot;hljs-attr&quot;&gt;limit&lt;/span&gt;: k,
        &lt;span class=&quot;hljs-attr&quot;&gt;metric_type&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;MetricType&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;COSINE&lt;/span&gt;,
        &lt;span class=&quot;hljs-attr&quot;&gt;output_fields&lt;/span&gt;: [&lt;span class=&quot;hljs-string&quot;&gt;&#39;id&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&#39;book_id&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&#39;chapter_num&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&#39;index&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&#39;content&#39;&lt;/span&gt;],
    });
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; searchResult.&lt;span class=&quot;hljs-property&quot;&gt;results&lt;/span&gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-14&quot;&gt;6.2 增强 Prompt + 生成&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; prompt = &lt;span class=&quot;hljs-string&quot;&gt;`你是一个专业的《天龙八部》小说助手。
基于小说回答问题，用准确、详细的语言。
请根据以下小说片段内容回答问题：
&lt;span class=&quot;hljs-subst&quot;&gt;${context}&lt;/span&gt;

用户问题：&lt;span class=&quot;hljs-subst&quot;&gt;${question}&lt;/span&gt;

回答要求：
1. 如片段中有相关信息，请结合小说内容给出详细准确的回答。
2. 可以综合多个片段的内容，提供完整的答案。
3. 如果片段中没有相关信息，请如实告诉用户。
4. 回答要准确，符合小说的情节和人物设定。
5. 可以引用原文内容来支持你的回答。
AI 助手的回答：`&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-15&quot;&gt;6.3 实际问答效果&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;用户问：&quot;段誉会什么武功？&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;搜索到的相关片段（余弦相似度排序）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-css&quot; lang=&quot;css&quot;&gt;&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;. &lt;span class=&quot;hljs-selector-attr&quot;&gt;[Score: 0.8921]&lt;/span&gt; 章节：第&lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;章
   段誉按照图中所示，开始修炼&quot;北冥神功&quot;……
&lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;. &lt;span class=&quot;hljs-selector-attr&quot;&gt;[Score: 0.8745]&lt;/span&gt; 章节：第&lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;章
   段誉使出&quot;凌波微步&quot;，脚步飘忽不定……
&lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;. &lt;span class=&quot;hljs-selector-attr&quot;&gt;[Score: 0.8312]&lt;/span&gt; 章节：第&lt;span class=&quot;hljs-number&quot;&gt;7&lt;/span&gt;章
   段誉心中焦急，六脉神剑便不由自主地……
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;AI 的回答：&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;段誉身怀多项绝世武功。他从无量山玉璧中习得了&lt;strong&gt;北冥神功&lt;/strong&gt;，能够吸收他人内力为己用；后来又在天龙寺学得了&lt;strong&gt;六脉神剑&lt;/strong&gt;，以指代剑，威力无穷；此外他还精通&lt;strong&gt;凌波微步&lt;/strong&gt;，这是一门极其精妙的轻功身法，脚步飘忽、变化莫测……&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;这就是 RAG 的威力——AI 不是凭空&quot;背&quot;知识，而是从向量库里检索到相关内容后，基于原文生成回答。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-16&quot;&gt;七、老师的&quot;天龙八部 RAG&quot;总结&lt;/h2&gt;
&lt;p&gt;老师说：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Loader 从各种来源加载文档——EPUB、CSV……相应的 Loader 加载器。Splitter 分块——Separator 切割符号 &lt;code&gt;。！？&lt;/code&gt;，chunk_size，overlap。Embedding——1024 维→百万字。Milvus 数据库。RAG——Cosine，top_k。&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;这五个模块串起来，就是一个完整的、可扩展的 RAG 知识库系统。&lt;/strong&gt; 换一本书？把文件名改了就行。换一个知识库类别？换 Loader 就行。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-17&quot;&gt;八、总结：RAG 完整链路一览&lt;/h2&gt;















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;步骤&lt;/th&gt;&lt;th&gt;技术&lt;/th&gt;&lt;th&gt;核心参数&lt;/th&gt;&lt;th&gt;做了什么&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;① 加载&lt;/td&gt;&lt;td&gt;&lt;code&gt;EPubLoader&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;splitChapters: true&lt;/code&gt;&lt;/td&gt;&lt;td&gt;读取电子书，按章拆分&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;② 切分&lt;/td&gt;&lt;td&gt;&lt;code&gt;RecursiveCharacterTextSplitter&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;chunkSize: 500&lt;/code&gt;, &lt;code&gt;overlap: 50&lt;/code&gt;&lt;/td&gt;&lt;td&gt;切成语义完整的块&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;③ 向量化&lt;/td&gt;&lt;td&gt;&lt;code&gt;OpenAIEmbeddings&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;dimensions: 1024&lt;/code&gt;&lt;/td&gt;&lt;td&gt;文本 → 1024 维向量&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;④ 存储&lt;/td&gt;&lt;td&gt;&lt;code&gt;MilvusClient.insert&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;IVF_FLAT&lt;/code&gt; 索引, &lt;code&gt;COSINE&lt;/code&gt; 相似度&lt;/td&gt;&lt;td&gt;存入向量数据库&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;⑤ 检索&lt;/td&gt;&lt;td&gt;&lt;code&gt;client.search&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;limit: 3&lt;/code&gt;, &lt;code&gt;COSINE&lt;/code&gt;&lt;/td&gt;&lt;td&gt;语义搜索最相关片段&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;⑥ 生成&lt;/td&gt;&lt;td&gt;&lt;code&gt;ChatOpenAI.invoke&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;temperature: 0.1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;基于原文生成回答&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;从文件到回答，六步走完。这就是现代 AI 应用的知识库技术栈。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-18&quot;&gt;写在最后&lt;/h2&gt;
&lt;p&gt;今天这节课真的太有趣了！亲手把《天龙八部》喂给 AI，然后问它武功、人物、情节，它都能答上来。更让我兴奋的是，这一整套流程——Loader → Splitter → Embedding → Milvus → RAG——是通用的。&lt;/p&gt;
&lt;p&gt;换个 Loader 就能处理 PDF，换个文件就能处理金庸全集。这套&quot;知识库流水线&quot;，可以应用到任何需要&quot;AI + 私有知识&quot;的场景。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;下次面试官问你：&quot;生产环境的 RAG 完整流程是什么？&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;你可以淡定地说：&lt;/p&gt;
&lt;p&gt;&quot;生产环境 RAG 的完整流程是六步。① &lt;strong&gt;Loader&lt;/strong&gt;——用 &lt;code&gt;EPubLoader&lt;/code&gt;、&lt;code&gt;PDFLoader&lt;/code&gt; 等从文件加载内容，转为标准 Document；② &lt;strong&gt;Splitter&lt;/strong&gt;——用 &lt;code&gt;RecursiveCharacterTextSplitter&lt;/code&gt; 按句号等分隔符切分成语义完整的块，设置 &lt;code&gt;chunkSize&lt;/code&gt; 和 &lt;code&gt;chunkOverlap&lt;/code&gt;；③ &lt;strong&gt;Embedding&lt;/strong&gt;——用 Embedding 模型将每个块转成高维向量（如 1024 维）；④ &lt;strong&gt;存储&lt;/strong&gt;——将向量和原文存入 Milvus 等向量数据库，建 &lt;code&gt;IVF_FLAT&lt;/code&gt; 索引和 &lt;code&gt;COSINE&lt;/code&gt; 相似度度量；⑤ &lt;strong&gt;检索&lt;/strong&gt;——用户提问时，将问题向量化，通过 &lt;code&gt;client.search&lt;/code&gt; 在 Milvus 中搜索最相关的 K 个片段；⑥ &lt;strong&gt;生成&lt;/strong&gt;——将检索到的片段组装成增强 Prompt，交给 LLM 生成最终回答。六步串起来就是一个完整的 RAG 知识库系统。&quot;&lt;/p&gt;
&lt;p&gt;然后看着面试官满意的表情，心里默念：&lt;strong&gt;这波，又稳了。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;本文所有代码示例均来自课堂学习资料，真实可运行。&lt;/em&gt;&lt;/p&gt;</description><link>https://juejin.cn/post/7671078538116546575</link><guid isPermaLink="false">https://juejin.cn/post/7671078538116546575</guid><pubDate>Fri, 07 Aug 2026 13:27:35 GMT</pubDate><author>默_笙</author><category>人工智能</category><category>前端</category><category>JavaScript</category></item><item><title>赋予 AI “灵魂”：LangChain.js 中临时与长期记忆的终极实战指南</title><description>&lt;p&gt;在构建 RAG（检索增强生成）应用时，我们往往过于关注“检索”的准确性，而忽略了“对话”的连贯性。一个没有记忆的 AI，就像是一个患有严重顺行性遗忘症的患者——它博学多才，却记不住你上一秒说了什么。&lt;/p&gt;
&lt;p&gt;在 LangChain.js 的生态中，Memory（记忆）模块是连接 LLM 与用户历史交互的桥梁。它不仅仅是简单的文本拼接，更是一场关于&lt;strong&gt;上下文窗口管理&lt;/strong&gt;、&lt;strong&gt;信息压缩&lt;/strong&gt;与&lt;strong&gt;持久化存储&lt;/strong&gt;的艺术。&lt;/p&gt;
&lt;p&gt;本文将带你从底层的&amp;nbsp;&lt;code&gt;BaseChatMessageHistory&lt;/code&gt;&amp;nbsp;到上层的&amp;nbsp;&lt;code&gt;RunnableWithMessageHistory&lt;/code&gt;，再到复杂的长期记忆架构，全方位拆解如何在 Node.js 环境中构建具备“灵魂”的 AI 应用。&lt;/p&gt;
&lt;h2 data-id=&quot;heading-0&quot;&gt;第一章：记忆的基石——LangChain.js 消息体系&lt;/h2&gt;
&lt;p&gt;在深入 Memory 之前，我们必须先理解 LangChain.js 是如何定义“记忆”的。与 Python 版本类似，JS 版本的核心在于&amp;nbsp;&lt;strong&gt;Message（消息）&lt;/strong&gt; &amp;nbsp;对象。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-1&quot;&gt;1.1 消息的类型&lt;/h3&gt;
&lt;p&gt;在代码中，记忆本质上是一个&amp;nbsp;&lt;code&gt;BaseMessage&lt;/code&gt;&amp;nbsp;数组。理解这些类型对于后续处理至关重要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HumanMessage&lt;/strong&gt;: 用户的输入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AIMessage&lt;/strong&gt;: AI 的输出。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SystemMessage&lt;/strong&gt;: 系统提示词（通常作为对话的基调）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FunctionMessage / ToolMessage&lt;/strong&gt;: 函数调用的结果（在 Agent 开发中常作为记忆的一部分）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-id=&quot;heading-2&quot;&gt;1.2 为什么不能直接把所有历史塞给 LLM？&lt;/h3&gt;
&lt;p&gt;这是新手最容易犯的错误。LLM 的 Context Window（上下文窗口）是有限的（例如 4k, 8k, 32k tokens）。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Token 爆炸&lt;/strong&gt;：随着对话进行，历史消息会迅速耗尽 Token 配额。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lost in the Middle&lt;/strong&gt;：研究表明，LLM 对长文本中间部分的信息关注度较低，过多的无关历史会干扰当前的回答。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成本与延迟&lt;/strong&gt;：更多的 Token 意味着更高的 API 费用和更长的等待时间。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，Memory 的核心价值在于：&lt;strong&gt;在有限的窗口内，保留最有价值的信息。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 data-id=&quot;heading-3&quot;&gt;第二章：临时会话记忆——让对话“连贯”起来&lt;/h2&gt;
&lt;p&gt;临时记忆（Short-term Memory），通常指当前会话窗口内的上下文管理。它的生命周期通常绑定于一次会话（Session）。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-4&quot;&gt;2.1 核心组件：ChatMessageHistory&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ChatMessageHistory&lt;/code&gt;&amp;nbsp;是 LangChain.js 中用于存储和检索消息列表的标准接口。它本身不决定“如何压缩”，只负责“存”和“取”。&lt;/p&gt;
&lt;p&gt;最简单的实现是内存存储（仅用于测试）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;InMemoryChatMessageHistory&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@langchain/core/chat_history&quot;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; history = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;InMemoryChatMessageHistory&lt;/span&gt;();
&lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; history.&lt;span class=&quot;hljs-title function_&quot;&gt;addUserMessage&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&quot;你好，我叫小明&quot;&lt;/span&gt;);
&lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; history.&lt;span class=&quot;hljs-title function_&quot;&gt;addAIChatMessage&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&quot;你好小明！有什么我可以帮你的吗？&quot;&lt;/span&gt;);

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; messages = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; history.&lt;span class=&quot;hljs-title function_&quot;&gt;getMessages&lt;/span&gt;();
&lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(messages); 
&lt;span class=&quot;hljs-comment&quot;&gt;// [HumanMessage, AIMessage]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-5&quot;&gt;2.2 生产级封装：RunnableWithMessageHistory&lt;/h3&gt;
&lt;p&gt;在实际的 LangChain.js (v0.1+) 开发中，我们不再手动去&amp;nbsp;&lt;code&gt;history.getMessages()&lt;/code&gt;&amp;nbsp;然后拼接到 Prompt 里。官方推荐的做法是使用&amp;nbsp;&lt;strong&gt;&lt;code&gt;RunnableWithMessageHistory&lt;/code&gt;&lt;/strong&gt;。这是一个高阶封装，它自动处理了“读取历史 -&amp;gt; 拼接到输入 -&amp;gt; 调用 LLM -&amp;gt; 保存新消息”的全流程。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-6&quot;&gt;场景：构建一个有状态的客服机器人&lt;/h4&gt;
&lt;p&gt;假设我们需要为每个&amp;nbsp;&lt;code&gt;session_id&lt;/code&gt;&amp;nbsp;维护独立的对话历史。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;ChatOpenAI&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@langchain/openai&quot;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;ChatPromptTemplate&lt;/span&gt;, &lt;span class=&quot;hljs-title class_&quot;&gt;MessagesPlaceholder&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@langchain/core/prompts&quot;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;RunnableWithMessageHistory&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@langchain/core/runnables&quot;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;UpstashRedisChatMessageHistory&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@langchain/community/stores/message/upstash_redis&quot;&lt;/span&gt;;

&lt;span class=&quot;hljs-comment&quot;&gt;// 1. 定义 Prompt，注意这里必须包含 MessagesPlaceholder&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 它是告诉 LLM &quot;这里将插入历史消息&quot; 的占位符&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; prompt = &lt;span class=&quot;hljs-title class_&quot;&gt;ChatPromptTemplate&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;fromMessages&lt;/span&gt;([
  [&lt;span class=&quot;hljs-string&quot;&gt;&quot;system&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;你是一个乐于助人的客服助手。&quot;&lt;/span&gt;],
  &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;MessagesPlaceholder&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&quot;history&quot;&lt;/span&gt;), &lt;span class=&quot;hljs-comment&quot;&gt;// 关键占位符&lt;/span&gt;
  [&lt;span class=&quot;hljs-string&quot;&gt;&quot;human&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&quot;{input}&quot;&lt;/span&gt;],
]);

&lt;span class=&quot;hljs-comment&quot;&gt;// 2. 定义 LLM&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; model = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;ChatOpenAI&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;temperature&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;0.7&lt;/span&gt; });

&lt;span class=&quot;hljs-comment&quot;&gt;// 3. 组装链&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; chain = prompt.&lt;span class=&quot;hljs-title function_&quot;&gt;pipe&lt;/span&gt;(model);

&lt;span class=&quot;hljs-comment&quot;&gt;// 4. 包装进 RunnableWithMessageHistory&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; chainWithHistory = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;RunnableWithMessageHistory&lt;/span&gt;({
  &lt;span class=&quot;hljs-attr&quot;&gt;runnable&lt;/span&gt;: chain,
  
  &lt;span class=&quot;hljs-comment&quot;&gt;// 获取历史记录的工厂函数&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// 每次调用时，根据 sessionId 返回对应的 History 实例&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;getMessageHistory&lt;/span&gt;: &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;sessionId&lt;/span&gt;) =&amp;gt;&lt;/span&gt; 
    &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;UpstashRedisChatMessageHistory&lt;/span&gt;({
      sessionId,
      &lt;span class=&quot;hljs-attr&quot;&gt;config&lt;/span&gt;: {
        &lt;span class=&quot;hljs-attr&quot;&gt;url&lt;/span&gt;: process.&lt;span class=&quot;hljs-property&quot;&gt;env&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;UPSTASH_REDIS_REST_URL&lt;/span&gt;!,
        &lt;span class=&quot;hljs-attr&quot;&gt;token&lt;/span&gt;: process.&lt;span class=&quot;hljs-property&quot;&gt;env&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;UPSTASH_REDIS_REST_TOKEN&lt;/span&gt;!,
      },
    }),
    
  &lt;span class=&quot;hljs-comment&quot;&gt;// 指定输入字段和输出字段&lt;/span&gt;
  &lt;span class=&quot;hljs-attr&quot;&gt;inputMessagesKey&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&quot;input&quot;&lt;/span&gt;,
  &lt;span class=&quot;hljs-attr&quot;&gt;historyMessagesKey&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&quot;history&quot;&lt;/span&gt;,
});

&lt;span class=&quot;hljs-comment&quot;&gt;// 5. 调用&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; response = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; chainWithHistory.&lt;span class=&quot;hljs-title function_&quot;&gt;invoke&lt;/span&gt;(
  { &lt;span class=&quot;hljs-attr&quot;&gt;input&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&quot;我刚才提到的订单号是多少？&quot;&lt;/span&gt; },
  { &lt;span class=&quot;hljs-attr&quot;&gt;configurable&lt;/span&gt;: { &lt;span class=&quot;hljs-attr&quot;&gt;sessionId&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&quot;user_001_session_A&quot;&lt;/span&gt; } }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;技巧点拨：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解耦存储&lt;/strong&gt;：注意上面的&amp;nbsp;&lt;code&gt;getMessageHistory&lt;/code&gt;。我们将具体的存储实现（Redis）与业务逻辑解耦了。你可以轻松切换成 MongoDB 或 PostgreSQL 实现，只需替换这个类。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置传递&lt;/strong&gt;：&lt;code&gt;sessionId&lt;/code&gt;&amp;nbsp;是通过&amp;nbsp;&lt;code&gt;invoke&lt;/code&gt;&amp;nbsp;的第二个参数&amp;nbsp;&lt;code&gt;configurable&lt;/code&gt;&amp;nbsp;传递的，这是一种非常优雅的运行时配置方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-id=&quot;heading-7&quot;&gt;2.3 进阶技巧：滑动窗口与截断&lt;/h3&gt;
&lt;p&gt;默认的&amp;nbsp;&lt;code&gt;InMemoryChatMessageHistory&lt;/code&gt;&amp;nbsp;或 Redis 实现通常会返回&lt;strong&gt;所有&lt;/strong&gt;历史。当对话很长时，这会撑爆 Token。我们需要引入&lt;strong&gt;缓冲窗口&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;虽然 LangChain.js 目前没有像 Python 那样内置极其丰富的&amp;nbsp;&lt;code&gt;BufferWindowMemory&lt;/code&gt;&amp;nbsp;类，但我们可以通过自定义&amp;nbsp;&lt;code&gt;getMessageHistory&lt;/code&gt;&amp;nbsp;逻辑来实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 自定义逻辑：只取最后 N 条消息&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;getLastNMessages&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;async&lt;/span&gt; (&lt;span class=&quot;hljs-params&quot;&gt;historyInstance, n = &lt;span class=&quot;hljs-number&quot;&gt;10&lt;/span&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; allMessages = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; historyInstance.&lt;span class=&quot;hljs-title function_&quot;&gt;getMessages&lt;/span&gt;();
  &lt;span class=&quot;hljs-comment&quot;&gt;// 简单的切片操作，只保留最后 N 条&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; allMessages.&lt;span class=&quot;hljs-title function_&quot;&gt;slice&lt;/span&gt;(-n); 
};

&lt;span class=&quot;hljs-comment&quot;&gt;// 在 RunnableWithMessageHistory 中应用&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 注意：这需要稍微修改 RunnableWithMessageHistory 的配置或者手动实现 Wrapper&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 目前 JS 版更推荐直接在 Prompt 层面或自定义 Runnable 中处理截断&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;更优解：&lt;/strong&gt; &amp;nbsp;使用 Token 计数器进行动态截断。在将 messages 传入 LLM 之前，计算 Token 数，如果超过阈值（如 3000 tokens），则从最早的消息开始丢弃，直到符合限制。这比固定条数更安全。&lt;/p&gt;
&lt;h2 data-id=&quot;heading-8&quot;&gt;第三章：长期会话记忆——打造“懂你”的 AI&lt;/h2&gt;
&lt;p&gt;临时记忆解决了“刚才说了什么”，而长期记忆（Long-term Memory）解决的是“你是谁”、“我们之前的共识是什么”。在 RAG 系统中，这通常通过&amp;nbsp;&lt;strong&gt;Vector Store（向量数据库）&lt;/strong&gt; &amp;nbsp;来实现。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-9&quot;&gt;3.1 核心概念：实体提取与知识图谱&lt;/h3&gt;
&lt;p&gt;长期记忆不是把每一句话都存下来，而是&lt;strong&gt;提炼&lt;/strong&gt;。&lt;br&gt;
流程通常是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;观察&lt;/strong&gt;：捕获当前的对话内容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提取&lt;/strong&gt;：使用 LLM 从对话中提取关键事实（如用户的喜好、名字、重要事件）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储&lt;/strong&gt;：将这些事实向量化并存入数据库。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检索&lt;/strong&gt;：在新对话开始时，根据当前问题检索相关的长期记忆。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-id=&quot;heading-10&quot;&gt;3.2 实战：基于 VectorStore 的长期记忆&lt;/h3&gt;
&lt;p&gt;我们可以利用 LangChain.js 的&amp;nbsp;&lt;code&gt;VectorStoreRetrieverMemory&lt;/code&gt;&amp;nbsp;模式。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-11&quot;&gt;步骤一：初始化向量存储&lt;/h4&gt;
&lt;p&gt;这里我们以 ChromaDB 为例（也可以使用 Pinecone, Milvus 等）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;Chroma&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@langchain/community/vectorstores/chroma&quot;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;OpenAIEmbeddings&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@langchain/openai&quot;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; vectorStore = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Chroma&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;fromExistingCollection&lt;/span&gt;(
  &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;OpenAIEmbeddings&lt;/span&gt;(),
  { &lt;span class=&quot;hljs-attr&quot;&gt;collectionName&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&quot;long_term_memory&quot;&lt;/span&gt; }
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-12&quot;&gt;步骤二：构建记忆检索链&lt;/h4&gt;
&lt;p&gt;我们需要创建一个机制，在每次对话前，先去向量库里搜一下有没有关于这个用户的旧闻。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { createStuffDocumentsChain } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;langchain/chains/combine_documents&quot;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;ChatPromptTemplate&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@langchain/core/prompts&quot;&lt;/span&gt;;

&lt;span class=&quot;hljs-comment&quot;&gt;// 定义一个专门用于注入背景知识的 Prompt&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; memoryPrompt = &lt;span class=&quot;hljs-title class_&quot;&gt;ChatPromptTemplate&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;fromTemplate&lt;/span&gt;(
  &lt;span class=&quot;hljs-string&quot;&gt;`以下是关于用户的长期记忆信息，如果与当前问题相关，请参考回答：
  &amp;lt;context&amp;gt;
  {context}
  &amp;lt;/context&amp;gt;
  
  当前用户问题: {input}`&lt;/span&gt;
);

&lt;span class=&quot;hljs-comment&quot;&gt;// 创建检索器&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; retriever = vectorStore.&lt;span class=&quot;hljs-title function_&quot;&gt;asRetriever&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;k&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt; }); &lt;span class=&quot;hljs-comment&quot;&gt;// 只取最相关的3条&lt;/span&gt;

&lt;span class=&quot;hljs-comment&quot;&gt;// 创建文档处理链（将检索到的记忆片段合并成字符串）&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; combineDocsChain = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;createStuffDocumentsChain&lt;/span&gt;({
  &lt;span class=&quot;hljs-attr&quot;&gt;llm&lt;/span&gt;: &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;ChatOpenAI&lt;/span&gt;(),
  &lt;span class=&quot;hljs-attr&quot;&gt;prompt&lt;/span&gt;: memoryPrompt,
});

&lt;span class=&quot;hljs-comment&quot;&gt;// 创建检索链&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; retrievalChain = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;createRetrievalChain&lt;/span&gt;({
  combineDocsChain,
  retriever,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-13&quot;&gt;步骤三：写入长期记忆（后台任务）&lt;/h4&gt;
&lt;p&gt;你不能在用户等待回复的时候同步做这件事，太慢了。通常的做法是异步执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;Document&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@langchain/core/documents&quot;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;saveToLongTermMemory&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;sessionId, userMessage, aiResponse&lt;/span&gt;) {
  &lt;span class=&quot;hljs-comment&quot;&gt;// 1. 使用 LLM 提取关键信息&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; extractionPrompt = &lt;span class=&quot;hljs-string&quot;&gt;`从以下对话中提取关于用户的关键事实（偏好、个人信息、重要事件）。
  如果没有重要事实，返回空。格式为 JSON 数组。
  User: &lt;span class=&quot;hljs-subst&quot;&gt;${userMessage}&lt;/span&gt;
  AI: &lt;span class=&quot;hljs-subst&quot;&gt;${aiResponse}&lt;/span&gt;`&lt;/span&gt;;
  
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; extractionResult = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; llm.&lt;span class=&quot;hljs-title function_&quot;&gt;invoke&lt;/span&gt;(extractionPrompt);
  &lt;span class=&quot;hljs-comment&quot;&gt;// 假设解析出了 facts = [&quot;用户喜欢喝拿铁&quot;, &quot;用户住在上海&quot;]&lt;/span&gt;
  
  &lt;span class=&quot;hljs-comment&quot;&gt;// 2. 存入向量库&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (facts.&lt;span class=&quot;hljs-property&quot;&gt;length&lt;/span&gt; &amp;gt; &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; docs = facts.&lt;span class=&quot;hljs-title function_&quot;&gt;map&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-params&quot;&gt;fact&lt;/span&gt; =&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Document&lt;/span&gt;({
      &lt;span class=&quot;hljs-attr&quot;&gt;pageContent&lt;/span&gt;: fact,
      &lt;span class=&quot;hljs-attr&quot;&gt;metadata&lt;/span&gt;: { sessionId, &lt;span class=&quot;hljs-attr&quot;&gt;type&lt;/span&gt;: &lt;span class=&quot;hljs-string&quot;&gt;&quot;user_preference&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;timestamp&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;Date&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;now&lt;/span&gt;() }
    }));
    &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; vectorStore.&lt;span class=&quot;hljs-title function_&quot;&gt;addDocuments&lt;/span&gt;(docs);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-14&quot;&gt;3.3 高级技巧：Zep 与 Mem0&lt;/h3&gt;
&lt;p&gt;自己写提取逻辑很麻烦。在 JS 生态中，强烈建议使用专门的记忆管理服务，如&amp;nbsp;&lt;strong&gt;Zep&lt;/strong&gt;&amp;nbsp;或&amp;nbsp;&lt;strong&gt;Mem0&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;以 Zep 为例（它有优秀的 JS SDK）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-csharp&quot; lang=&quot;csharp&quot;&gt;import { ZepClient } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@getzep/zep-js&quot;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; client = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; ZepClient({ apiKey: &lt;span class=&quot;hljs-string&quot;&gt;&quot;YOUR_API_KEY&quot;&lt;/span&gt; });

&lt;span class=&quot;hljs-comment&quot;&gt;// 添加消息（自动处理摘要、实体提取、向量化）&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; client.memory.&lt;span class=&quot;hljs-keyword&quot;&gt;add&lt;/span&gt;(sessionId, {
  messages: [{ role: &lt;span class=&quot;hljs-string&quot;&gt;&quot;human&quot;&lt;/span&gt;, content: &lt;span class=&quot;hljs-string&quot;&gt;&quot;我喜欢科幻小说&quot;&lt;/span&gt; }]
});

&lt;span class=&quot;hljs-comment&quot;&gt;// 获取记忆（自动总结 + 向量检索）&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; memory = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; client.memory.&lt;span class=&quot;hljs-keyword&quot;&gt;get&lt;/span&gt;(sessionId, { 
  lastn: &lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;, &lt;span class=&quot;hljs-comment&quot;&gt;// 最近的5轮&lt;/span&gt;
  searchType: &lt;span class=&quot;hljs-string&quot;&gt;&quot;similarity&quot;&lt;/span&gt; &lt;span class=&quot;hljs-comment&quot;&gt;// 开启语义搜索&lt;/span&gt;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式将“长期记忆”变成了一个黑盒服务，极大地降低了开发复杂度。&lt;/p&gt;
&lt;h2 data-id=&quot;heading-15&quot;&gt;第四章：RAG 中的记忆融合策略&lt;/h2&gt;
&lt;p&gt;在 RAG 系统中，我们面临双重检索：&lt;strong&gt;知识库检索&lt;/strong&gt;（外部数据）+&amp;nbsp;&lt;strong&gt;记忆检索&lt;/strong&gt;（内部数据）。如何融合是关键。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-16&quot;&gt;4.1 策略一：并行检索（Parallel Retrieval）&lt;/h3&gt;
&lt;p&gt;将“用户问题”同时发给“知识库 Retriever”和“记忆 Retriever”。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 伪代码结构&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; finalChain = &lt;span class=&quot;hljs-title class_&quot;&gt;RunnableBranch&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;from&lt;/span&gt;([
  &lt;span class=&quot;hljs-comment&quot;&gt;// 分支逻辑...&lt;/span&gt;
]).&lt;span class=&quot;hljs-title function_&quot;&gt;pipe&lt;/span&gt;({
  &lt;span class=&quot;hljs-attr&quot;&gt;context&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;RunnableMap&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;from&lt;/span&gt;({
    &lt;span class=&quot;hljs-attr&quot;&gt;knowledge&lt;/span&gt;: knowledgeRetriever, &lt;span class=&quot;hljs-comment&quot;&gt;// 查文档&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;memory&lt;/span&gt;: memoryRetriever        &lt;span class=&quot;hljs-comment&quot;&gt;// 查历史&lt;/span&gt;
  }).&lt;span class=&quot;hljs-title function_&quot;&gt;pipe&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;inputs&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;formatContext&lt;/span&gt;(inputs.&lt;span class=&quot;hljs-property&quot;&gt;knowledge&lt;/span&gt;, inputs.&lt;span class=&quot;hljs-property&quot;&gt;memory&lt;/span&gt;)),
  &lt;span class=&quot;hljs-attr&quot;&gt;question&lt;/span&gt;: &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;input&lt;/span&gt;) =&amp;gt;&lt;/span&gt; input.&lt;span class=&quot;hljs-property&quot;&gt;question&lt;/span&gt;
}).&lt;span class=&quot;hljs-title function_&quot;&gt;pipe&lt;/span&gt;(llm);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-17&quot;&gt;4.2 策略二：记忆作为元数据过滤&lt;/h3&gt;
&lt;p&gt;如果用户的长期记忆中包含了“我只关注金融领域的新闻”，那么在检索外部知识库时，应该将这个偏好作为 Metadata Filter 传给向量数据库，从而缩小检索范围。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-ini&quot; lang=&quot;ini&quot;&gt;// 假设从记忆中提取到了 filter criteria
const &lt;span class=&quot;hljs-attr&quot;&gt;memoryFilter&lt;/span&gt; = { category: &lt;span class=&quot;hljs-string&quot;&gt;&quot;finance&quot;&lt;/span&gt; }&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;

const &lt;span class=&quot;hljs-attr&quot;&gt;results&lt;/span&gt; = await vectorStore.similaritySearch(query, &lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;, memoryFilter)&lt;span class=&quot;hljs-comment&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-18&quot;&gt;4.3 策略三：HyDE (Hypothetical Document Embeddings) 结合记忆&lt;/h3&gt;
&lt;p&gt;有时候用户的问题很模糊（例如：“那个怎么样？”）。此时直接检索效果很差。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先查短期记忆，确认“那个”指代什么。&lt;/li&gt;
&lt;li&gt;利用 LLM 生成一个假设性的完整问题。&lt;/li&gt;
&lt;li&gt;用生成的完整问题去检索 RAG 知识库。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-id=&quot;heading-19&quot;&gt;第五章：避坑指南与性能优化&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-20&quot;&gt;5.1 避免“幽灵上下文”&lt;/h3&gt;
&lt;p&gt;在使用&amp;nbsp;&lt;code&gt;RunnableWithMessageHistory&lt;/code&gt;&amp;nbsp;时，务必确保&amp;nbsp;&lt;code&gt;MessagesPlaceholder&lt;/code&gt;&amp;nbsp;的变量名（如&amp;nbsp;&lt;code&gt;history&lt;/code&gt;）与&amp;nbsp;&lt;code&gt;historyMessagesKey&lt;/code&gt;&amp;nbsp;严格一致。否则 LLM 会收到空的上下文，或者报错。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-21&quot;&gt;5.2 序列化陷阱&lt;/h3&gt;
&lt;p&gt;在 Node.js 中，如果你使用 Redis 存储消息，要注意&amp;nbsp;&lt;code&gt;AIMessage&lt;/code&gt;&amp;nbsp;等对象包含复杂的方法。不要直接&amp;nbsp;&lt;code&gt;JSON.stringify&lt;/code&gt;&amp;nbsp;整个对象。&lt;br&gt;
&lt;strong&gt;正确做法&lt;/strong&gt;：使用 LangChain 提供的&amp;nbsp;&lt;code&gt;mapChatMessagesToStoredMessages&lt;/code&gt;&amp;nbsp;和&amp;nbsp;&lt;code&gt;mapStoredMessagesToChatMessages&lt;/code&gt;&amp;nbsp;工具函数进行转换。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { mapChatMessagesToStoredMessages } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&quot;@langchain/core/messages&quot;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; stored = &lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;mapChatMessagesToStoredMessages&lt;/span&gt;(messages);
&lt;span class=&quot;hljs-keyword&quot;&gt;await&lt;/span&gt; redis.&lt;span class=&quot;hljs-title function_&quot;&gt;set&lt;/span&gt;(key, &lt;span class=&quot;hljs-title class_&quot;&gt;JSON&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;stringify&lt;/span&gt;(stored));
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-22&quot;&gt;5.3 隐私与安全&lt;/h3&gt;
&lt;p&gt;长期记忆是隐私泄露的重灾区。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PII 移除&lt;/strong&gt;：在存入长期记忆前，必须经过一层 PII（个人身份信息）检测。不要明文存储身份证号、密码等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTL (Time To Live)&lt;/strong&gt; ：给 Redis 中的短期记忆设置过期时间（如 24 小时）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;遗忘权&lt;/strong&gt;：提供 API 允许用户删除特定的记忆片段或清空所有历史。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-id=&quot;heading-23&quot;&gt;5.4 调试技巧&lt;/h3&gt;
&lt;p&gt;记忆系统是黑盒，调试困难。&lt;br&gt;
&lt;strong&gt;建议&lt;/strong&gt;：在开发阶段，使用 LangSmith 或简单的 Console Log 打印出最终发送给 LLM 的完整 Prompt。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 调试中间件&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; debuggableChain = chain.&lt;span class=&quot;hljs-title function_&quot;&gt;pipe&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;output&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&quot;=== Final Prompt Sent to LLM ===&quot;&lt;/span&gt;);
  &lt;span class=&quot;hljs-comment&quot;&gt;// 这里需要拦截 Input 才能看到，通常在 RunnableMap 中做&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; output;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到完整的 Prompt 后，你就能判断是“记忆没取出来”还是“记忆太多导致 LLM 晕了”。&lt;/p&gt;
&lt;h2 data-id=&quot;heading-24&quot;&gt;第六章：未来展望——自适应记忆&lt;/h2&gt;
&lt;p&gt;目前的记忆大多是被动式的。未来的方向是&lt;strong&gt;主动式记忆&lt;/strong&gt;。&lt;br&gt;
AI 应该能够意识到：“我现在掌握的信息不足以回答这个问题，我需要询问用户以更新我的长期记忆。”&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;User: &quot;帮我订机票。&quot;&lt;br&gt;
AI (Internal Thought): 记忆里只有他喜欢靠窗，但不知道目的地。&lt;br&gt;
AI (Response): &quot;好的，还是老规矩帮您选靠窗的位置吗？另外，这次您打算去哪里？&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这种能力需要结合&amp;nbsp;&lt;strong&gt;Agent（智能体）&lt;/strong&gt; &amp;nbsp;框架，将 Memory 作为一个 Tool 供 AI 自主调用（&lt;code&gt;read_memory&lt;/code&gt;,&amp;nbsp;&lt;code&gt;write_memory&lt;/code&gt;），而不仅仅是作为 Prompt 的背景板。&lt;/p&gt;
&lt;h2 data-id=&quot;heading-25&quot;&gt;结语&lt;/h2&gt;
&lt;p&gt;在 JavaScript 生态中构建 RAG 记忆系统，既充满了挑战也充满乐趣。从&amp;nbsp;&lt;code&gt;InMemoryChatMessageHistory&lt;/code&gt;&amp;nbsp;的简单起步，到&amp;nbsp;&lt;code&gt;RunnableWithMessageHistory&lt;/code&gt;&amp;nbsp;的工程化封装，再到结合 Vector Store 的长期记忆网络，每一步都是为了让 AI 更像一个人。&lt;/p&gt;
&lt;p&gt;记住，&lt;strong&gt;Memory 不仅仅是技术实现，更是产品设计。&lt;/strong&gt; &amp;nbsp;你需要根据业务场景决定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这个应用需要记住多久的事？（Session 级 vs User 级）&lt;/li&gt;
&lt;li&gt;需要记住多细的事？（原文 vs 摘要 vs 实体）&lt;/li&gt;
&lt;li&gt;如何平衡成本与智能度？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;希望这篇指南能为你的 LangChain.js 开发之路点亮一盏灯。现在，打开你的 IDE，去赋予你的 AI 真正的“灵魂”吧！&lt;/p&gt;</description><link>https://juejin.cn/post/7671120692242579462</link><guid isPermaLink="false">https://juejin.cn/post/7671120692242579462</guid><pubDate>Fri, 07 Aug 2026 13:24:20 GMT</pubDate><author>渣波</author><category>前端</category><category>JavaScript</category></item><item><title>不要把 Context 当万能状态库：主题共享和鼠标 Hook 应该这样拆</title><description>&lt;h2 data-id=&quot;heading-0&quot;&gt;不要把 Context 当万能状态库：主题共享和鼠标 Hook 应该这样拆&lt;/h2&gt;
&lt;p&gt;摘要：一个 React 学习 Demo 同时包含主题 Context 与 &lt;code&gt;useMouse&lt;/code&gt;。它们解决的不是同一类问题：Context 用于跨层传递共享数据，自定义 Hook 用于封装可复用的状态和副作用。本文从源码调用链出发，给出 Provider、&lt;code&gt;useContext&lt;/code&gt;、事件清理和职责划分的自检方法。代码运行未验证。&lt;/p&gt;
&lt;p&gt;深层组件树需要主题时，很多人会把 &lt;code&gt;theme&lt;/code&gt; 从 App 一路传到按钮。多个页面都需要鼠标位置时，又会复制 &lt;code&gt;useState + useEffect + addEventListener&lt;/code&gt;。前者是跨层共享问题，后者是逻辑复用问题。关键不是“都用 Hook”，而是选对职责：&lt;strong&gt;Context 管共享数据，自定义 Hook 管可复用逻辑。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 data-id=&quot;heading-1&quot;&gt;从主题消费链路看 Context&lt;/h3&gt;
&lt;p&gt;Demo 先创建主题通道：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { createContext } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt; = &lt;span class=&quot;hljs-title function_&quot;&gt;createContext&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;light&#39;&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;light&lt;/code&gt; 不是当前主题的强制值，而是兜底值。消费者没有被对应 Provider 包裹时，&lt;code&gt;useContext(ThemeContext)&lt;/code&gt; 才会得到它。&lt;/p&gt;
&lt;p&gt;Provider 负责限定共享数据生效的组件树：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;Provider&lt;/span&gt; value=&lt;span class=&quot;hljs-string&quot;&gt;&quot;dark&quot;&lt;/span&gt;&amp;gt;
  &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Page&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;
&amp;lt;/&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;Provider&lt;/span&gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它包裹的 &lt;code&gt;Page&lt;/code&gt; 和后代可以读取 &lt;code&gt;dark&lt;/code&gt;。如果嵌套了同一种 Provider，消费者读取最近的 Provider 值，因此内部 Provider 可以覆盖外部 Provider。&lt;/p&gt;
&lt;p&gt;Demo 没有让组件到处直接写 &lt;code&gt;useContext&lt;/code&gt;，而是封装为 &lt;code&gt;useTheme&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useContext } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;../ThemeContext&#39;&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useTheme&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useContext&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;ThemeContext&lt;/span&gt;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Page&lt;/code&gt; 和 &lt;code&gt;Child&lt;/code&gt; 调用 &lt;code&gt;useTheme()&lt;/code&gt;，其中 &lt;code&gt;Child&lt;/code&gt; 将读取到的主题写入按钮的 &lt;code&gt;className&lt;/code&gt; 和文本。这说明业务组件只关心“拿主题”，不必关心 Context 定义在哪个文件。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-2&quot;&gt;useMouse 复用的是逻辑，不是同一份坐标&lt;/h3&gt;
&lt;p&gt;鼠标 Hook 的实现包含 state 和副作用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useMouse&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [x, setX] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [y, setY] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;)

  &lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;handleMouseMove&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) {
      &lt;span class=&quot;hljs-title function_&quot;&gt;setX&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;clientX&lt;/span&gt;)
      &lt;span class=&quot;hljs-title function_&quot;&gt;setY&lt;/span&gt;(e.&lt;span class=&quot;hljs-property&quot;&gt;clientY&lt;/span&gt;)
    }

    &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;addEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove)

    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
      &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;removeEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove)
    }
  }, [])

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; { x, y }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用端只需要：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { x, y } = &lt;span class=&quot;hljs-title function_&quot;&gt;useMouse&lt;/span&gt;()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段封装把“坐标 state、监听注册、卸载清理”从页面组件移走。&lt;code&gt;clientX&lt;/code&gt;、&lt;code&gt;clientY&lt;/code&gt; 是鼠标相对浏览器可视区域的坐标。&lt;/p&gt;
&lt;p&gt;但要注意：两个组件分别调用 &lt;code&gt;useMouse()&lt;/code&gt; 时，它们拥有各自的 state 和 Effect。它们只是复用同一段代码，并不会天然共享一份 &lt;code&gt;x&lt;/code&gt;、&lt;code&gt;y&lt;/code&gt;。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-3&quot;&gt;事件监听的关键不是添加，而是成对清理&lt;/h3&gt;
&lt;p&gt;Effect 中的清理函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;removeEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保证组件卸载后停止监听。更重要的是，添加和移除时传入的必须是同一个函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;addEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove)
&lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;removeEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;mousemove&#39;&lt;/span&gt;, handleMouseMove)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果每次都传一个新的匿名函数，浏览器无法定位已经注册的监听器。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-4&quot;&gt;选择工具时只问一个问题&lt;/h3&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;你的问题&lt;/th&gt;&lt;th&gt;选择&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;多个深层后代需要同一份主题、用户、语言数据&lt;/td&gt;&lt;td&gt;Context&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;多个组件需要复用请求、监听、定时器等逻辑&lt;/td&gt;&lt;td&gt;自定义 Hook&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;只有直接父子组件使用的数据&lt;/td&gt;&lt;td&gt;Props&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Context 不是全局变量，也不应替代所有局部 state。自定义 Hook 也不是共享状态容器，它主要用来抽离重复的响应式和副作用逻辑。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-5&quot;&gt;一次可执行的自检&lt;/h3&gt;
&lt;ul class=&quot;contains-task-list&quot;&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled=&quot;&quot;&gt; Provider 是否包裹了需要读取数据的组件树？&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled=&quot;&quot;&gt; Consumer 读取的数据结构是否与 Provider 的 &lt;code&gt;value&lt;/code&gt; 一致？&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled=&quot;&quot;&gt; Hook 是否以 &lt;code&gt;use&lt;/code&gt; 开头，并在组件或 Hook 顶层调用？&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled=&quot;&quot;&gt; &lt;code&gt;useEffect&lt;/code&gt; 是否释放了事件监听、定时器、Worker 或连接？&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled=&quot;&quot;&gt; 添加和移除事件时是否复用了同一个函数引用？&lt;/li&gt;
&lt;li class=&quot;task-list-item&quot;&gt;&lt;input type=&quot;checkbox&quot; disabled=&quot;&quot;&gt; 复用需求是共享数据，还是共享逻辑？&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-id=&quot;heading-6&quot;&gt;本次源码的未验证项&lt;/h3&gt;
&lt;p&gt;静态阅读发现 &lt;code&gt;App.jsx&lt;/code&gt; 已通过 &lt;code&gt;useMouse()&lt;/code&gt; 获得坐标，但仍导入了未使用的 &lt;code&gt;useState&lt;/code&gt; 与 &lt;code&gt;useEffect&lt;/code&gt;。本次未执行 lint，因此不把它写成已确认错误。&lt;/p&gt;
&lt;p&gt;已读的 &lt;code&gt;App.jsx&lt;/code&gt; 也没有展示主题 Provider 的实际挂载位置，所以主题链路的运行状态未知。本文基于本地源码整理，代码运行未验证。&lt;/p&gt;
&lt;p&gt;最终判断很简单：数据需要跨层抵达多个消费者时用 Context；逻辑需要被多个组件复用时写自定义 Hook。先按这个边界拆分，再决定是否需要更复杂的状态管理方案。&lt;/p&gt;</description><link>https://juejin.cn/post/7671100900817092617</link><guid isPermaLink="false">https://juejin.cn/post/7671100900817092617</guid><pubDate>Fri, 07 Aug 2026 13:14:44 GMT</pubDate><author>BreezeJiang</author><category>前端</category><category>JavaScript</category><category>React.js</category></item><item><title>从“迷路”到“瞬移”：彻底搞懂 React Router 的 `useLocation` 与 `useNavigate`</title><description>&lt;p&gt;在构建单页应用（SPA）时，前端路由是核心骨架。我们习惯了用&amp;nbsp;&lt;code&gt;&amp;lt;Link&amp;gt;&lt;/code&gt;&amp;nbsp;跳转页面，用&amp;nbsp;&lt;code&gt;&amp;lt;Route&amp;gt;&lt;/code&gt;&amp;nbsp;定义路径。但在复杂的业务场景中，仅仅会配置路由是远远不够的。&lt;/p&gt;
&lt;p&gt;你是否遇到过这样的场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户未登录时点击“个人中心”，跳转登录页，登录成功后却只能回到首页，而不是刚才想去的“个人中心”？&lt;/li&gt;
&lt;li&gt;表单提交后，用户刷新页面导致数据重复提交？&lt;/li&gt;
&lt;li&gt;在复杂的嵌套路由中，搞不清当前的 URL 参数到底在哪里？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;解决这些问题的关键，就在于深入理解 React Router 提供的两个核心 Hooks：&lt;code&gt;useLocation&lt;/code&gt;&amp;nbsp;和&amp;nbsp;&lt;code&gt;useNavigate&lt;/code&gt;。它们不仅是跳转和获取参数的工具，更是掌控用户浏览历史、实现丝滑鉴权体验的“核武器”。&lt;/p&gt;
&lt;p&gt;本文将结合实战代码，带你彻底吃透这两个 API，从原理到最佳实践，让你的路由控制能力上一个台阶。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-0&quot;&gt;📍 一、&amp;nbsp;&lt;code&gt;useLocation&lt;/code&gt;：不仅仅是获取 URL 参数&lt;/h3&gt;
&lt;p&gt;很多开发者对&amp;nbsp;&lt;code&gt;useLocation&lt;/code&gt;&amp;nbsp;的理解仅停留在“获取查询参数（Query Params）”上。实际上，它是当前激活路由的&lt;strong&gt;状态快照&lt;/strong&gt;。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-1&quot;&gt;1.&amp;nbsp;&lt;code&gt;location&lt;/code&gt;&amp;nbsp;对象的全貌&lt;/h4&gt;
&lt;p&gt;当你在组件中调用&amp;nbsp;&lt;code&gt;const location = useLocation()&lt;/code&gt;&amp;nbsp;时，你拿到的是一个包含以下属性的对象：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-perl&quot; lang=&quot;perl&quot;&gt;// 假设当前 URL 为: http:&lt;span class=&quot;hljs-regexp&quot;&gt;//&lt;/span&gt;&lt;span class=&quot;hljs-number&quot;&gt;127.0&lt;/span&gt;.&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;.&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;:&lt;span class=&quot;hljs-number&quot;&gt;5500&lt;/span&gt;/&lt;span class=&quot;hljs-comment&quot;&gt;#/login?redirect=%2Fuser%2F123&lt;/span&gt;

{
  pathname: &lt;span class=&quot;hljs-string&quot;&gt;&quot;/login&quot;&lt;/span&gt;,                &lt;span class=&quot;hljs-regexp&quot;&gt;//&lt;/span&gt; 路由路径（不含查询参数）
  search: &lt;span class=&quot;hljs-string&quot;&gt;&quot;?redirect=%2Fuser%2F123&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-regexp&quot;&gt;//&lt;/span&gt; 查询字符串
  hash: &lt;span class=&quot;hljs-string&quot;&gt;&quot;&quot;&lt;/span&gt;,                          &lt;span class=&quot;hljs-regexp&quot;&gt;//&lt;/span&gt; hash 值（在 HashRouter 中通常为空，因为路径已被解析）
  &lt;span class=&quot;hljs-keyword&quot;&gt;state&lt;/span&gt;: null,                       &lt;span class=&quot;hljs-regexp&quot;&gt;//&lt;/span&gt; 关键！这是通过 navigate 传递的“隐式”数据
  key: &lt;span class=&quot;hljs-string&quot;&gt;&quot;xs63g7&quot;&lt;/span&gt;                      // 唯一标识，用于跟踪历史记录条目
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-2&quot;&gt;2. 实战：解析 URL 参数&lt;/h4&gt;
&lt;p&gt;最基础的用法是解析&amp;nbsp;&lt;code&gt;search&lt;/code&gt;&amp;nbsp;字段。虽然我们可以手写正则，但现代浏览器原生提供了&amp;nbsp;&lt;code&gt;URLSearchParams&lt;/code&gt;，结合&amp;nbsp;&lt;code&gt;useLocation&lt;/code&gt;&amp;nbsp;使用非常优雅：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useLocation } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react-router-dom&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;SearchPage&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { search } = &lt;span class=&quot;hljs-title function_&quot;&gt;useLocation&lt;/span&gt;();
  
  &lt;span class=&quot;hljs-comment&quot;&gt;// 自动处理 ? 符号，解析为键值对&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; params = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;URLSearchParams&lt;/span&gt;(search);
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; query = params.&lt;span class=&quot;hljs-title function_&quot;&gt;get&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;q&#39;&lt;/span&gt;); &lt;span class=&quot;hljs-comment&quot;&gt;// 获取 ?q=xxx 中的值&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;搜索关键词：{query}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-3&quot;&gt;🚀 二、&amp;nbsp;&lt;code&gt;useNavigate&lt;/code&gt;：掌控跳转的“方向盘”&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useNavigate&lt;/code&gt;&amp;nbsp;取代了 v5 版本的&amp;nbsp;&lt;code&gt;history.push&lt;/code&gt;，它更加灵活，支持命令式导航。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-4&quot;&gt;1. 基础跳转与参数传递&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useNavigate } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react-router-dom&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;MyComponent&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; navigate = &lt;span class=&quot;hljs-title function_&quot;&gt;useNavigate&lt;/span&gt;();

  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;goToDetail&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
    &lt;span class=&quot;hljs-comment&quot;&gt;// 1. 字符串跳转&lt;/span&gt;
    &lt;span class=&quot;hljs-title function_&quot;&gt;navigate&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;/detail/123&#39;&lt;/span&gt;);
    
    &lt;span class=&quot;hljs-comment&quot;&gt;// 2. 数字跳转（类似 history.go）&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// navigate(-1); // 后退一页&lt;/span&gt;
  };

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onClick&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{goToDetail}&lt;/span&gt;&amp;gt;&lt;/span&gt;查看详情&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-5&quot;&gt;2. 核心秘籍：&lt;code&gt;replace&lt;/code&gt;&amp;nbsp;模式&lt;/h4&gt;
&lt;p&gt;这是很多新手容易忽略，但在实际开发中&lt;strong&gt;至关重要&lt;/strong&gt;的配置。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;场景：&lt;/strong&gt; &amp;nbsp;用户登录。&lt;br&gt;
如果用户访问&amp;nbsp;&lt;code&gt;/pay&lt;/code&gt;（支付页），被拦截跳转到&amp;nbsp;&lt;code&gt;/login&lt;/code&gt;。登录成功后，如果使用默认的&amp;nbsp;&lt;code&gt;navigate(&#39;/pay&#39;)&lt;/code&gt;，浏览器的历史记录栈会变成：&lt;code&gt;[/pay, /login, /pay]&lt;/code&gt;。&lt;br&gt;
此时，用户在支付页点击“后退”，会回到登录页！这显然是不合理的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案：&lt;/strong&gt; &amp;nbsp;使用&amp;nbsp;&lt;code&gt;replace: true&lt;/code&gt;。&lt;br&gt;
它会用新页面&lt;strong&gt;替换&lt;/strong&gt;当前页面在历史记录中的位置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// Login.jsx&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;handleLogin&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-comment&quot;&gt;// 模拟登录成功&lt;/span&gt;
  &lt;span class=&quot;hljs-variable language_&quot;&gt;localStorage&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;setItem&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;isLogin&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&#39;true&#39;&lt;/span&gt;);
  
  &lt;span class=&quot;hljs-comment&quot;&gt;// 第二个参数配置 replace: true&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// 这会将 /login 的记录替换为 /pay，栈变为 [/pay, /pay] -&amp;gt; 实际上看起来像直接跳转&lt;/span&gt;
  &lt;span class=&quot;hljs-title function_&quot;&gt;navigate&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;/pay&#39;&lt;/span&gt;, { &lt;span class=&quot;hljs-attr&quot;&gt;replace&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对比图解：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;表格&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;模式&lt;/th&gt;&lt;th&gt;历史栈变化 (假设原栈:&lt;code&gt;[A]&lt;/code&gt;)&lt;/th&gt;&lt;th&gt;用户点击“后退”的行为&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;默认 (Push)&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;[A, B, C]&lt;/code&gt;&lt;/td&gt;&lt;td&gt;C -&amp;gt; B -&amp;gt; A&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Replace&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;[A, C]&lt;/code&gt;&amp;nbsp;(B被C替换)&lt;/td&gt;&lt;td&gt;C -&amp;gt; A&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-6&quot;&gt;🛡️ 三、 终极实战：实现“登录后重定向回原页面”&lt;/h3&gt;
&lt;p&gt;这是前端鉴权中最经典的需求。用户未登录访问&amp;nbsp;&lt;code&gt;/user/123&lt;/code&gt;，跳转登录，登录后自动回到&amp;nbsp;&lt;code&gt;/user/123&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这需要&amp;nbsp;&lt;code&gt;useLocation&lt;/code&gt;&amp;nbsp;的&amp;nbsp;&lt;code&gt;state&lt;/code&gt;&amp;nbsp;和&amp;nbsp;&lt;code&gt;useNavigate&lt;/code&gt;&amp;nbsp;的配合。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-7&quot;&gt;1. 第一步：受保护路由 (&lt;code&gt;ProtectedRoute&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;我们需要一个高阶组件或包装组件，在渲染子组件前检查登录状态。如果未登录，跳转至登录页，并&lt;strong&gt;携带当前路径信息&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ProtectedRoute.jsx&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;Navigate&lt;/span&gt;, useLocation } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react-router-dom&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;ProtectedRoute&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;{ children }&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; isLogin = &lt;span class=&quot;hljs-variable language_&quot;&gt;localStorage&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;getItem&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;isLogin&#39;&lt;/span&gt;) === &lt;span class=&quot;hljs-string&quot;&gt;&#39;true&#39;&lt;/span&gt;;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; location = &lt;span class=&quot;hljs-title function_&quot;&gt;useLocation&lt;/span&gt;(); &lt;span class=&quot;hljs-comment&quot;&gt;// 获取当前试图访问的位置&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (!isLogin) {
    &lt;span class=&quot;hljs-comment&quot;&gt;// 未登录，重定向到登录页&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// 关键点：通过 state 传递 from 信息&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
      &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Navigate&lt;/span&gt; 
        &lt;span class=&quot;hljs-attr&quot;&gt;to&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;/login&quot;&lt;/span&gt; 
        &lt;span class=&quot;hljs-attr&quot;&gt;state&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{{&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;from:&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;location&lt;/span&gt; }} // &lt;span class=&quot;hljs-attr&quot;&gt;把&lt;/span&gt;“&lt;span class=&quot;hljs-attr&quot;&gt;我从哪里来&lt;/span&gt;”&lt;span class=&quot;hljs-attr&quot;&gt;告诉登录页&lt;/span&gt;
        &lt;span class=&quot;hljs-attr&quot;&gt;replace&lt;/span&gt; // &lt;span class=&quot;hljs-attr&quot;&gt;替换掉当前的访问记录&lt;/span&gt;，&lt;span class=&quot;hljs-attr&quot;&gt;避免死循环&lt;/span&gt;
      /&amp;gt;&lt;/span&gt;&lt;/span&gt;
    );
  }

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; children;
};

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;ProtectedRoute&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-8&quot;&gt;2. 第二步：登录页 (&lt;code&gt;Login&lt;/code&gt;)&lt;/h4&gt;
&lt;p&gt;登录页需要读取&amp;nbsp;&lt;code&gt;state&lt;/code&gt;，知道用户原本想去哪。如果&amp;nbsp;&lt;code&gt;state&lt;/code&gt;&amp;nbsp;为空（比如用户直接访问登录页），则默认跳回首页。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// Login.jsx&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useNavigate, useLocation } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react-router-dom&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;Login&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; navigate = &lt;span class=&quot;hljs-title function_&quot;&gt;useNavigate&lt;/span&gt;();
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; location = &lt;span class=&quot;hljs-title function_&quot;&gt;useLocation&lt;/span&gt;();
  
  &lt;span class=&quot;hljs-comment&quot;&gt;// 核心逻辑：获取来源路径，如果没有则默认为 &#39;/&#39;&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// location.state?.from?.pathname 获取原本想去的地址&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; = location.&lt;span class=&quot;hljs-property&quot;&gt;state&lt;/span&gt;?.&lt;span class=&quot;hljs-property&quot;&gt;from&lt;/span&gt;?.&lt;span class=&quot;hljs-property&quot;&gt;pathname&lt;/span&gt; || &lt;span class=&quot;hljs-string&quot;&gt;&#39;/&#39;&lt;/span&gt;;

  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;handleSubmit&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt; {
    e.&lt;span class=&quot;hljs-title function_&quot;&gt;preventDefault&lt;/span&gt;();
    &lt;span class=&quot;hljs-comment&quot;&gt;// ... 模拟登录请求 ...&lt;/span&gt;
    
    &lt;span class=&quot;hljs-comment&quot;&gt;// 登录成功&lt;/span&gt;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;localStorage&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;setItem&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;isLogin&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&#39;true&#39;&lt;/span&gt;);
    
    &lt;span class=&quot;hljs-comment&quot;&gt;// 跳转回来源页面，并使用 replace 清除登录页的历史记录&lt;/span&gt;
    &lt;span class=&quot;hljs-title function_&quot;&gt;navigate&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt;, { &lt;span class=&quot;hljs-attr&quot;&gt;replace&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; });
  };

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;form&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;onSubmit&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{handleSubmit}&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;h1&lt;/span&gt;&amp;gt;&lt;/span&gt;请登录&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;h1&lt;/span&gt;&amp;gt;&lt;/span&gt;
      {/* 表单内容 */}
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;type&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;submit&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;登录&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;form&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  );
};

&lt;span class=&quot;hljs-keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Login&lt;/span&gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-id=&quot;heading-9&quot;&gt;3. 第三步：路由配置&lt;/h4&gt;
&lt;p&gt;在&amp;nbsp;&lt;code&gt;App&lt;/code&gt;&amp;nbsp;组件中，将需要鉴权的页面包裹在&amp;nbsp;&lt;code&gt;ProtectedRoute&lt;/code&gt;&amp;nbsp;中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// App.jsx&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { &lt;span class=&quot;hljs-title class_&quot;&gt;Routes&lt;/span&gt;, &lt;span class=&quot;hljs-title class_&quot;&gt;Route&lt;/span&gt; } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react-router-dom&#39;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;ProtectedRoute&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;./ProtectedRoute&#39;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Pay&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;./pages/Pay&#39;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Login&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;./pages/Login&#39;&lt;/span&gt;;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;App&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; (
    &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Routes&lt;/span&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Route&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;path&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;/login&quot;&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;element&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{&lt;/span&gt;&amp;lt;&lt;span class=&quot;hljs-attr&quot;&gt;Login&lt;/span&gt; /&amp;gt;&lt;/span&gt;} /&amp;gt;
      
      {/* 需要鉴权的路由 */}
      &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Route&lt;/span&gt; 
        &lt;span class=&quot;hljs-attr&quot;&gt;path&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;/pay&quot;&lt;/span&gt; 
        &lt;span class=&quot;hljs-attr&quot;&gt;element&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;{&lt;/span&gt;
          &amp;lt;&lt;span class=&quot;hljs-attr&quot;&gt;ProtectedRoute&lt;/span&gt;&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;Pay&lt;/span&gt; /&amp;gt;&lt;/span&gt;
          &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;ProtectedRoute&lt;/span&gt;&amp;gt;&lt;/span&gt;
        } 
      /&amp;gt;
    &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;Routes&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-10&quot;&gt;🧠 四、 深度解析：为什么&amp;nbsp;&lt;code&gt;state&lt;/code&gt;&amp;nbsp;比&amp;nbsp;&lt;code&gt;search&lt;/code&gt;&amp;nbsp;参数更好？&lt;/h3&gt;
&lt;p&gt;你可能会问： &lt;em&gt;“我能不能把来源路径拼在 URL 后面，比如&amp;nbsp;&lt;code&gt;/login?redirect=/pay&lt;/code&gt;？”&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;答案是可以，但使用&amp;nbsp;&lt;code&gt;location.state&lt;/code&gt;&amp;nbsp;有以下优势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;URL 整洁&lt;/strong&gt;：&lt;code&gt;state&lt;/code&gt;&amp;nbsp;是隐式传递的，不会污染地址栏，用户刷新页面时（如果是编程式导航），参数依然存在于 history state 中（但在强刷时会丢失，需注意）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;类型安全&lt;/strong&gt;：&lt;code&gt;state&lt;/code&gt;&amp;nbsp;可以传递对象、布尔值等复杂数据，而&amp;nbsp;&lt;code&gt;search&lt;/code&gt;&amp;nbsp;只能是字符串。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语义清晰&lt;/strong&gt;：&lt;code&gt;state&lt;/code&gt;&amp;nbsp;专用于页面间的数据传递，&lt;code&gt;search&lt;/code&gt;&amp;nbsp;专用于资源过滤和排序。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; &amp;nbsp;&lt;code&gt;location.state&lt;/code&gt;&amp;nbsp;的数据在用户&lt;strong&gt;强制刷新页面&lt;/strong&gt;（F5）后会丢失。因此，对于极其重要的数据（如支付订单号），建议还是通过 URL 参数或后端接口获取，但对于“从哪来回哪去”这种导航辅助信息，&lt;code&gt;state&lt;/code&gt;&amp;nbsp;是最佳选择。&lt;/p&gt;
&lt;hr&gt;
&lt;h3 data-id=&quot;heading-11&quot;&gt;📌 五、 总结与最佳实践&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;鉴权跳转必用&amp;nbsp;&lt;code&gt;replace&lt;/code&gt;&lt;/strong&gt;：无论是从&amp;nbsp;&lt;code&gt;ProtectedRoute&lt;/code&gt;&amp;nbsp;跳往&amp;nbsp;&lt;code&gt;Login&lt;/code&gt;，还是&amp;nbsp;&lt;code&gt;Login&lt;/code&gt;&amp;nbsp;跳回原页面，都建议使用&amp;nbsp;&lt;code&gt;{ replace: true }&lt;/code&gt;，避免用户点击后退时陷入“登录页死循环”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;利用&amp;nbsp;&lt;code&gt;state&lt;/code&gt;&amp;nbsp;传递上下文&lt;/strong&gt;：不要把&amp;nbsp;&lt;code&gt;useLocation&lt;/code&gt;&amp;nbsp;仅仅当作获取&amp;nbsp;&lt;code&gt;?id=1&lt;/code&gt;&amp;nbsp;的工具，利用&amp;nbsp;&lt;code&gt;state&lt;/code&gt;&amp;nbsp;在页面间传递来源信息，能极大提升用户体验。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认回退路径&lt;/strong&gt;：在处理&amp;nbsp;&lt;code&gt;location.state?.from&lt;/code&gt;&amp;nbsp;时，永远要准备一个兜底路径（通常是&amp;nbsp;&lt;code&gt;/&lt;/code&gt;&amp;nbsp;或&amp;nbsp;&lt;code&gt;/dashboard&lt;/code&gt;），防止用户直接访问登录页导致跳转错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;区分&amp;nbsp;&lt;code&gt;HashRouter&lt;/code&gt;&amp;nbsp;与&amp;nbsp;&lt;code&gt;BrowserRouter&lt;/code&gt;&lt;/strong&gt;：虽然 API 一致，但在&amp;nbsp;&lt;code&gt;HashRouter&lt;/code&gt;&amp;nbsp;中，&lt;code&gt;location.pathname&lt;/code&gt;&amp;nbsp;不包含&amp;nbsp;&lt;code&gt;#&lt;/code&gt;&amp;nbsp;后的部分，且&amp;nbsp;&lt;code&gt;location.hash&lt;/code&gt;&amp;nbsp;通常为空（被 React Router 内部消费了），这点在打印日志时要注意。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;掌握&amp;nbsp;&lt;code&gt;useLocation&lt;/code&gt;&amp;nbsp;和&amp;nbsp;&lt;code&gt;useNavigate&lt;/code&gt;，你就掌握了 React 应用的“导航系统”。希望这篇文章能帮你写出更健壮、体验更流畅的路由逻辑！&lt;/p&gt;</description><link>https://juejin.cn/post/7671107889684316166</link><guid isPermaLink="false">https://juejin.cn/post/7671107889684316166</guid><pubDate>Fri, 07 Aug 2026 12:17:19 GMT</pubDate><author>渣波</author><category>前端</category><category>JavaScript</category></item><item><title>JavaScript 闭包</title><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;摘要&lt;/strong&gt;：闭包（Closure）是&lt;strong&gt;函数与其词法环境（Lexical Environment）的组合&lt;/strong&gt;——一个函数可以&quot;记住&quot;并访问它被创建时所处的外部作用域中的变量，即使它在该作用域之外被执行。一句话记忆：&lt;strong&gt;函数打包了出生地的变量，走到哪都能用&lt;/strong&gt;。核心要点：① 内部函数引用外部变量即形成闭包；② 闭包实现变量私有化/数据封装/柯里化；③ 潜在风险是内存泄漏和循环引用；④ 避免泄漏需手动解除引用和及时移除监听器。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-id=&quot;heading-0&quot;&gt;一、什么是闭包&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-1&quot;&gt;1.1 定义&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;一个函数可以&quot;记住&quot;并访问它被创建时所处的&lt;strong&gt;外部词法作用域&lt;/strong&gt;，即使它在该作用域之外被执行，这种现象就叫做闭包。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === 最简单的闭包示例 ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;test&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; name = &lt;span class=&quot;hljs-string&quot;&gt;&#39;nana&#39;&lt;/span&gt;;        &lt;span class=&quot;hljs-comment&quot;&gt;// 外部变量&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;t2&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {             &lt;span class=&quot;hljs-comment&quot;&gt;// 内部函数 —— 这就是闭包！&lt;/span&gt;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(name);         &lt;span class=&quot;hljs-comment&quot;&gt;// 访问了外部变量 name&lt;/span&gt;
  }

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; t2;                  &lt;span class=&quot;hljs-comment&quot;&gt;// 把内部函数返回到外部&lt;/span&gt;
}

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; fn = &lt;span class=&quot;hljs-title function_&quot;&gt;test&lt;/span&gt;();            &lt;span class=&quot;hljs-comment&quot;&gt;// test() 执行完毕，它的作用域本该销毁&lt;/span&gt;
&lt;span class=&quot;hljs-title function_&quot;&gt;fn&lt;/span&gt;();                         &lt;span class=&quot;hljs-comment&quot;&gt;// 但 t2 仍然能访问到 name → &quot;heyi&quot;&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 为什么？因为 t2 形成了闭包，把 name &quot;打包带走&quot;了&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-2&quot;&gt;1.2 本质：词法作用域 + 函数作为一等公民&lt;/h3&gt;
&lt;p&gt;闭包的产生需要两个条件同时满足：&lt;/p&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;条件&lt;/th&gt;&lt;th&gt;说明&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;① 内部函数引用了外部变量&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;必须有变量被&quot;捕获&quot;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;② 内部函数在外部被调用&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;超出了定义时的作用域范围&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 不是闭包的情况：&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;foo&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; x = &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;;
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(x);   &lt;span class=&quot;hljs-comment&quot;&gt;// 只是普通的作用域查找，没有形成闭包&lt;/span&gt;
}
&lt;span class=&quot;hljs-title function_&quot;&gt;foo&lt;/span&gt;();

&lt;span class=&quot;hljs-comment&quot;&gt;// 是闭包的情况：&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;bar&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; x = &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {   &lt;span class=&quot;hljs-comment&quot;&gt;// 内部函数引用了 x&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; x;           &lt;span class=&quot;hljs-comment&quot;&gt;// 并且被返回到外部使用&lt;/span&gt;
  };
}
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; fn = &lt;span class=&quot;hljs-title function_&quot;&gt;bar&lt;/span&gt;();       &lt;span class=&quot;hljs-comment&quot;&gt;// fn 持有对 x 的引用 → 闭包形成&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-3&quot;&gt;二、闭包的核心应用场景&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-4&quot;&gt;2.1 变量私有化（数据封装）&lt;/h3&gt;
&lt;p&gt;这是闭包最经典的应用——通过闭包创建&quot;私有变量&quot;，外部无法直接修改：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === 计数器：变量私有化 ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;createCounter&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; count = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;                    &lt;span class=&quot;hljs-comment&quot;&gt;// 私有变量，外部无法直接访问！&lt;/span&gt;

  &lt;span class=&quot;hljs-comment&quot;&gt;// 返回一个访问器函数（闭包）&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    count++;
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; count;
  };
}

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; counter1 = &lt;span class=&quot;hljs-title function_&quot;&gt;createCounter&lt;/span&gt;();   &lt;span class=&quot;hljs-comment&quot;&gt;// 创建第一个计数器实例&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; c1 = &lt;span class=&quot;hljs-title function_&quot;&gt;counter1&lt;/span&gt;();              &lt;span class=&quot;hljs-comment&quot;&gt;// 1&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; c2 = &lt;span class=&quot;hljs-title function_&quot;&gt;counter1&lt;/span&gt;();              &lt;span class=&quot;hljs-comment&quot;&gt;// 2&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; c3 = &lt;span class=&quot;hljs-title function_&quot;&gt;counter1&lt;/span&gt;();              &lt;span class=&quot;hljs-comment&quot;&gt;// 3&lt;/span&gt;

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; counter2 = &lt;span class=&quot;hljs-title function_&quot;&gt;createCounter&lt;/span&gt;();   &lt;span class=&quot;hljs-comment&quot;&gt;// 创建第二个计数器实例（独立！）&lt;/span&gt;
&lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-title function_&quot;&gt;counter2&lt;/span&gt;());            &lt;span class=&quot;hljs-comment&quot;&gt;// 1（从 0 开始，不受 counter1 影响）&lt;/span&gt;

&lt;span class=&quot;hljs-comment&quot;&gt;// 关键点：&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 1. count 是私有的，外部无法直接改写 counter1.count = 999 ❌&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 2. 每次 createCounter() 都创建新的闭包，count 互不影响&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 3. 只有通过返回的函数才能间接操作 count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-5&quot;&gt;2.2 模块模式（Module Pattern）&lt;/h3&gt;
&lt;p&gt;在 ES6 Module 普及之前，闭包是实现模块化的核心手段：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === 用闭包实现模块 ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;UserModule&lt;/span&gt; = (&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-comment&quot;&gt;// 私有变量&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; _users = [];
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; _idCounter = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;

  &lt;span class=&quot;hljs-comment&quot;&gt;// 私有函数&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;_generateId&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; ++_idCounter;
  }

  &lt;span class=&quot;hljs-comment&quot;&gt;// 公共 API（通过闭包暴露）&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; {
    &lt;span class=&quot;hljs-title function_&quot;&gt;add&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;name&lt;/span&gt;) {
      &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; user = { &lt;span class=&quot;hljs-attr&quot;&gt;id&lt;/span&gt;: &lt;span class=&quot;hljs-title function_&quot;&gt;_generateId&lt;/span&gt;(), name };
      _users.&lt;span class=&quot;hljs-title function_&quot;&gt;push&lt;/span&gt;(user);
      &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; user;
    },
    &lt;span class=&quot;hljs-title function_&quot;&gt;getById&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;id&lt;/span&gt;) {
      &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; _users.&lt;span class=&quot;hljs-title function_&quot;&gt;find&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-params&quot;&gt;u&lt;/span&gt; =&amp;gt;&lt;/span&gt; u.&lt;span class=&quot;hljs-property&quot;&gt;id&lt;/span&gt; === id);
    },
    &lt;span class=&quot;hljs-title function_&quot;&gt;getAll&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
      &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; [..._users];          &lt;span class=&quot;hljs-comment&quot;&gt;// 返回副本，防止外部篡改&lt;/span&gt;
    },
    &lt;span class=&quot;hljs-keyword&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;count&lt;/span&gt;() {                  &lt;span class=&quot;hljs-comment&quot;&gt;// getter&lt;/span&gt;
      &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; _users.&lt;span class=&quot;hljs-property&quot;&gt;length&lt;/span&gt;;
    }
  };
})();

&lt;span class=&quot;hljs-title class_&quot;&gt;UserModule&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;add&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;Alice&#39;&lt;/span&gt;);
&lt;span class=&quot;hljs-title class_&quot;&gt;UserModule&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;add&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;Bob&#39;&lt;/span&gt;);
&lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;UserModule&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;count&lt;/span&gt;);     &lt;span class=&quot;hljs-comment&quot;&gt;// 2&lt;/span&gt;
&lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;UserModule&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;_users&lt;/span&gt;);    &lt;span class=&quot;hljs-comment&quot;&gt;// undefined（私有！）&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-6&quot;&gt;2.3 函数柯里化（Currying）&lt;/h3&gt;
&lt;p&gt;柯里化是闭包的经典应用——将多参数函数转化为一系列单参数嵌套函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === 普通多参数函数 ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;add&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;a, b, c&lt;/span&gt;) {           &lt;span class=&quot;hljs-comment&quot;&gt;// Identifier &#39;add&#39; has already been declared&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; a + b + c;
}
&lt;span class=&quot;hljs-title function_&quot;&gt;add&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;, &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;);                     &lt;span class=&quot;hljs-comment&quot;&gt;// 6&lt;/span&gt;

&lt;span class=&quot;hljs-comment&quot;&gt;// === 柯里化版本：add(1)(2)(3) ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;add&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;a&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;b&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;c&lt;/span&gt;) {
      &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; a + b + c;           &lt;span class=&quot;hljs-comment&quot;&gt;// a 和 b 通过闭包被&quot;记住&quot;&lt;/span&gt;
    };
  };
}

&lt;span class=&quot;hljs-title function_&quot;&gt;add&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;)(&lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;)(&lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;);                     &lt;span class=&quot;hljs-comment&quot;&gt;// 6&lt;/span&gt;

&lt;span class=&quot;hljs-comment&quot;&gt;// 实际应用：预设参数&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; add10 = &lt;span class=&quot;hljs-title function_&quot;&gt;add&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;10&lt;/span&gt;);            &lt;span class=&quot;hljs-comment&quot;&gt;// 预设 a = 10&lt;/span&gt;
&lt;span class=&quot;hljs-title function_&quot;&gt;add10&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;)(&lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;);                      &lt;span class=&quot;hljs-comment&quot;&gt;// 18&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; add10And20 = &lt;span class=&quot;hljs-title function_&quot;&gt;add10&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;20&lt;/span&gt;);     &lt;span class=&quot;hljs-comment&quot;&gt;// 预设 a=10, b=20&lt;/span&gt;
&lt;span class=&quot;hljs-title function_&quot;&gt;add10And20&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;);                    &lt;span class=&quot;hljs-comment&quot;&gt;// 35&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-7&quot;&gt;2.4 其他常见场景&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === 场景 1：防抖 / 节流 ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;debounce&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;fn, delay&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; timer = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;               &lt;span class=&quot;hljs-comment&quot;&gt;// 闭包保存 timer 状态&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;...args&lt;/span&gt;) {
    &lt;span class=&quot;hljs-built_in&quot;&gt;clearTimeout&lt;/span&gt;(timer);
    timer = &lt;span class=&quot;hljs-built_in&quot;&gt;setTimeout&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; fn.&lt;span class=&quot;hljs-title function_&quot;&gt;apply&lt;/span&gt;(&lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;, args), delay);
  };
}

&lt;span class=&quot;hljs-comment&quot;&gt;// === 场景 2：React Hooks 的本质就是闭包 ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;initialValue&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; state = initialValue;        &lt;span class=&quot;hljs-comment&quot;&gt;// 闭包持有状态&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;setState&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;newValue&lt;/span&gt;) {
    state = &lt;span class=&quot;hljs-keyword&quot;&gt;typeof&lt;/span&gt; newValue === &lt;span class=&quot;hljs-string&quot;&gt;&#39;function&#39;&lt;/span&gt; ? &lt;span class=&quot;hljs-title function_&quot;&gt;newValue&lt;/span&gt;(state) : newValue;
    &lt;span class=&quot;hljs-title function_&quot;&gt;render&lt;/span&gt;();                     &lt;span class=&quot;hljs-comment&quot;&gt;// 触发重新渲染&lt;/span&gt;
  }
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; [state, setState];       &lt;span class=&quot;hljs-comment&quot;&gt;// 返回状态和更新函数&lt;/span&gt;
}

&lt;span class=&quot;hljs-comment&quot;&gt;// === 场景 3：迭代器 / 生成器模拟 ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;createIterator&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;arr&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; index = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;                  &lt;span class=&quot;hljs-comment&quot;&gt;// 闭包保存游标位置&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; {
    &lt;span class=&quot;hljs-title function_&quot;&gt;next&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
      &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; index &amp;lt; arr.&lt;span class=&quot;hljs-property&quot;&gt;length&lt;/span&gt;
        ? { &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;: arr[index++], &lt;span class=&quot;hljs-attr&quot;&gt;done&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt; }
        : { &lt;span class=&quot;hljs-attr&quot;&gt;value&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;undefined&lt;/span&gt;, &lt;span class=&quot;hljs-attr&quot;&gt;done&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; };
    },
    &lt;span class=&quot;hljs-title function_&quot;&gt;reset&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) { index = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; }
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-8&quot;&gt;三、闭包的底层原理&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-9&quot;&gt;3.1 执行上下文与作用域链&lt;/h3&gt;
&lt;p&gt;理解闭包需要先理解 JS 的执行模型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-css&quot; lang=&quot;css&quot;&gt;┌─────────────────────────────────────────────┐
│  全局执行上下文 (Global EC)                   │
│  ┌───────────────────────────────────────┐  │
│  │  函数 createCounter 的执行上下文        │  │
│  │  ┌─────────────────────────────────┐  │  │
│  │  │  匿名返回函数的执行上下文 (闭包)   │  │  │
│  │  │                                 │  │  │
│  │  │  变量查找顺序:                   │  │  │
│  │  │  自身 → 外层(createCounter) → 全局│  │  │
│  │  └─────────────────────────────────┘  │  │
│  │                                       │  │
│  │  Variable Environment: { count }       │  │
│  └───────────────────────────────────────┘  │
│                                             │
│  Variable Environment: { createCounter, ...}│
└─────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-10&quot;&gt;3.2 闭包的本质：[[Environment]] 引用&lt;/h3&gt;
&lt;p&gt;每个 JS 函数对象内部都有一个隐藏属性 &lt;code&gt;[[Environment]]&lt;/code&gt;，指向其定义时的&lt;strong&gt;词法环境&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 伪代码表示闭包的内部结构&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;createCounter&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; count = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;

  &lt;span class=&quot;hljs-comment&quot;&gt;// 返回的匿名函数内部大致长这样：&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// {&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;//   code: function() { count++; return count; },&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;//   [[Environment]]: {           // ← 词法环境引用&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;//     count: 0,                 // ← 被捕获的变量&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;//     outer: &amp;lt;GlobalEnv&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;//   }&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// }&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    count++;
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; count;
  };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关键点&lt;/strong&gt;：只要存在对闭包函数的引用，&lt;code&gt;[[Environment]]&lt;/code&gt; 就不会被垃圾回收，里面的变量就一直存活。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-11&quot;&gt;四、闭包可能导致的问题&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-12&quot;&gt;4.1 内存泄漏（最常见问题）&lt;/h3&gt;
&lt;p&gt;闭包会阻止外部变量的垃圾回收——如果不再需要的闭包没有被释放，内存就会持续增长：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === 泄漏示例 1：超大闭包 ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;leakyHandler&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; hugeData = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Array&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;100000&lt;/span&gt;).&lt;span class=&quot;hljs-title function_&quot;&gt;fill&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;x&#39;&lt;/span&gt;);  &lt;span class=&quot;hljs-comment&quot;&gt;// ~400KB 数据&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// hugeData 被闭包捕获，只要 handler 存在就不会被回收&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;handler&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;clicked&#39;&lt;/span&gt;);   &lt;span class=&quot;hljs-comment&quot;&gt;// 根本没用 hugeData，但它还是被闭包持有！&lt;/span&gt;
  };
}

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; handler = &lt;span class=&quot;hljs-title function_&quot;&gt;leakyHandler&lt;/span&gt;();
&lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;getElementById&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;btn&#39;&lt;/span&gt;).&lt;span class=&quot;hljs-title function_&quot;&gt;addEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;click&#39;&lt;/span&gt;, handler);
&lt;span class=&quot;hljs-comment&quot;&gt;// 结果：hugeData (~400KB) 在页面生命周期内永远无法释放&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === 泄漏示例 2：循环中的闭包（经典面试题）===&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 错误：所有回调共享同一个 i（var 的作用域问题）&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;; i++) {
  &lt;span class=&quot;hljs-built_in&quot;&gt;setTimeout&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(i);    &lt;span class=&quot;hljs-comment&quot;&gt;// 输出：3, 3, 3（不是 0, 1, 2！）&lt;/span&gt;
  }, &lt;span class=&quot;hljs-number&quot;&gt;100&lt;/span&gt;);
}
&lt;span class=&quot;hljs-comment&quot;&gt;// 原因：var i 是函数作用域，三个闭包引用的是同一个 i，循环结束后 i=3&lt;/span&gt;

&lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 修复方式 1：let 块级作用域（每次循环创建新绑定）&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;; i++) {
  &lt;span class=&quot;hljs-built_in&quot;&gt;setTimeout&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(i);    &lt;span class=&quot;hljs-comment&quot;&gt;// 输出：0, 1, 2 ✅&lt;/span&gt;
  }, &lt;span class=&quot;hljs-number&quot;&gt;100&lt;/span&gt;);
}

&lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 修复方式 2：IIFE 创建独立作用域&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;var&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;; i++) {
  (&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;j&lt;/span&gt;) {
    &lt;span class=&quot;hljs-built_in&quot;&gt;setTimeout&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
      &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(j);  &lt;span class=&quot;hljs-comment&quot;&gt;// 输出：0, 1, 2 ✅&lt;/span&gt;
    }, &lt;span class=&quot;hljs-number&quot;&gt;100&lt;/span&gt;);
  })(i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-13&quot;&gt;4.2 循环引用（IE 旧时代的噩梦）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === 循环引用导致 DOM 无法回收 ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;setup&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; element = &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;getElementById&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;myDiv&#39;&lt;/span&gt;);

  element.&lt;span class=&quot;hljs-property&quot;&gt;onclick&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-comment&quot;&gt;// 这个闭包引用了 element&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// element 又通过 onclick 引用了这个函数&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// → 循环引用！&lt;/span&gt;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(element.&lt;span class=&quot;hljs-property&quot;&gt;id&lt;/span&gt;);
  };
}

&lt;span class=&quot;hljs-comment&quot;&gt;// 在现代 V8/SpiderMonkey 中，GC 能处理这种循环引用&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 但在旧版 IE（&amp;lt;9）中会导致 DOM 元素永远不被回收&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-14&quot;&gt;4.3 性能开销&lt;/h3&gt;
&lt;p&gt;闭包不是免费的：&lt;/p&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;开销项&lt;/th&gt;&lt;th&gt;说明&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;额外内存&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;每个闭包都携带 &lt;code&gt;[[Environment]]&lt;/code&gt; 引用链&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;查找耗时&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;变量访问需要沿作用域链逐层查找（V8 有优化但非零成本）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;优化限制&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;V8 对闭包变量的某些优化（如内联缓存）受限&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 性能敏感场景下避免不必要的闭包&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 循环中反复创建闭包&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;slow&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;arr&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; result = [];
  &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; arr.&lt;span class=&quot;hljs-property&quot;&gt;length&lt;/span&gt;; i++) {
    result.&lt;span class=&quot;hljs-title function_&quot;&gt;push&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; arr[i] * &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;);   &lt;span class=&quot;hljs-comment&quot;&gt;// 每次迭代创建新闭包&lt;/span&gt;
  }
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; result;
}

&lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 如果不需要闭包，就不要用&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;fast&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;arr&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; arr.&lt;span class=&quot;hljs-title function_&quot;&gt;map&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-params&quot;&gt;x&lt;/span&gt; =&amp;gt;&lt;/span&gt; x * &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;);        &lt;span class=&quot;hljs-comment&quot;&gt;// 无闭包，更轻量&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-15&quot;&gt;五、如何避免闭包导致的内存泄漏&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-16&quot;&gt;5.1 手动解除引用（= null）&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;setupLeakFree&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; element = &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;getElementById&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;myDiv&#39;&lt;/span&gt;);
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; hugeData = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Array&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;100000&lt;/span&gt;).&lt;span class=&quot;hljs-title function_&quot;&gt;fill&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;x&#39;&lt;/span&gt;);

  element.&lt;span class=&quot;hljs-property&quot;&gt;onclick&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;handleClick&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(element.&lt;span class=&quot;hljs-property&quot;&gt;id&lt;/span&gt;);         &lt;span class=&quot;hljs-comment&quot;&gt;// 只用到 element，不用 hugeData&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// hugeData 不小心被闭包捕获了&lt;/span&gt;
  };

  &lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 解决：手动切断不需要的外部变量引用&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// 方法 A：赋值 null&lt;/span&gt;
  hugeData = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;                   &lt;span class=&quot;hljs-comment&quot;&gt;// 释放大对象的引用&lt;/span&gt;

  &lt;span class=&quot;hljs-comment&quot;&gt;// 方法 B：用局部变量缩小闭包范围&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// const id = element.id;           // 只提取需要的值&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// element.onclick = () =&amp;gt; console.log(id);&lt;/span&gt;
}

&lt;span class=&quot;hljs-comment&quot;&gt;// 更彻底的方式：用完后主动置空&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; handler = &lt;span class=&quot;hljs-title function_&quot;&gt;setupLeakFree&lt;/span&gt;();
&lt;span class=&quot;hljs-comment&quot;&gt;// ... 页面卸载或组件销毁时&lt;/span&gt;
handler = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;                      &lt;span class=&quot;hljs-comment&quot;&gt;// 断开引用 → GC 可以回收&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-17&quot;&gt;5.2 及时移除事件监听器&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === 最佳实践：成对出现（添加 ↔ 移除）===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Component&lt;/span&gt; {
  &lt;span class=&quot;hljs-title function_&quot;&gt;constructor&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;el&lt;/span&gt;) {
    &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;el&lt;/span&gt; = el;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;handleClick&lt;/span&gt; = &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;onClick&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;bind&lt;/span&gt;(&lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;);

    el.&lt;span class=&quot;hljs-title function_&quot;&gt;addEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;click&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;handleClick&lt;/span&gt;);
  }

  &lt;span class=&quot;hljs-title function_&quot;&gt;onClick&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;clicked&#39;&lt;/span&gt;);
  }

  &lt;span class=&quot;hljs-title function_&quot;&gt;destroy&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-comment&quot;&gt;// ⚠️ 关键：移除监听器！否则 this.el 和 this 永远不释放&lt;/span&gt;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;el&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;removeEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;click&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;handleClick&lt;/span&gt;);
    &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;el&lt;/span&gt; = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;                  &lt;span class=&quot;hljs-comment&quot;&gt;// 断开 DOM 引用&lt;/span&gt;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;handleClick&lt;/span&gt; = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;         &lt;span class=&quot;hljs-comment&quot;&gt;// 断开函数引用&lt;/span&gt;
  }
}

&lt;span class=&quot;hljs-comment&quot;&gt;// React 中对应的是 useEffect cleanup&lt;/span&gt;
&lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;handler&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; { &lt;span class=&quot;hljs-comment&quot;&gt;/* ... */&lt;/span&gt; };
  &lt;span class=&quot;hljs-variable language_&quot;&gt;window&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;addEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;resize&#39;&lt;/span&gt;, handler);
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; &lt;span class=&quot;hljs-variable language_&quot;&gt;window&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;removeEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;resize&#39;&lt;/span&gt;, handler);  &lt;span class=&quot;hljs-comment&quot;&gt;// 清理！&lt;/span&gt;
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-18&quot;&gt;5.3 避免不必要的闭包&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === 原则：不需要访问外部变量就不要放在闭包位置 ===&lt;/span&gt;

&lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 不必要的闭包&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;processData&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;items&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; config = { &lt;span class=&quot;hljs-attr&quot;&gt;debug&lt;/span&gt;: &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt; };    &lt;span class=&quot;hljs-comment&quot;&gt;// 被闭包捕获但不一定需要&lt;/span&gt;

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; items.&lt;span class=&quot;hljs-title function_&quot;&gt;map&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;item&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (config.&lt;span class=&quot;hljs-property&quot;&gt;debug&lt;/span&gt;) {              &lt;span class=&quot;hljs-comment&quot;&gt;// 闭包引用了 config&lt;/span&gt;
      &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(item);
    }
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; item * &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;;
  });
}

&lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 把不变量提到参数或常量&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-variable constant_&quot;&gt;DEBUG&lt;/span&gt; = &lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;;                  &lt;span class=&quot;hljs-comment&quot;&gt;// 模块级常量，不在闭包链上&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;processData&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;items&lt;/span&gt;) {
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; items.&lt;span class=&quot;hljs-title function_&quot;&gt;map&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;&lt;span class=&quot;hljs-params&quot;&gt;item&lt;/span&gt; =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (&lt;span class=&quot;hljs-variable constant_&quot;&gt;DEBUG&lt;/span&gt;) &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(item);
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; item * &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;;
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-19&quot;&gt;5.4 WeakMap 解决强引用问题&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// === WeakMap：弱引用，不阻止 GC ===&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; privateData = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;WeakMap&lt;/span&gt;();

&lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;User&lt;/span&gt; {
  &lt;span class=&quot;hljs-title function_&quot;&gt;constructor&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;name&lt;/span&gt;) {
    &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;name&lt;/span&gt; = name;
    privateData.&lt;span class=&quot;hljs-title function_&quot;&gt;set&lt;/span&gt;(&lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;, {         &lt;span class=&quot;hljs-comment&quot;&gt;// 以实例为 key（弱引用）&lt;/span&gt;
      &lt;span class=&quot;hljs-attr&quot;&gt;createdAt&lt;/span&gt;: &lt;span class=&quot;hljs-title class_&quot;&gt;Date&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;now&lt;/span&gt;(),
      &lt;span class=&quot;hljs-attr&quot;&gt;token&lt;/span&gt;: &lt;span class=&quot;hljs-title function_&quot;&gt;generateToken&lt;/span&gt;()
    });
  }

  &lt;span class=&quot;hljs-title function_&quot;&gt;getToken&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; privateData.&lt;span class=&quot;hljs-title function_&quot;&gt;get&lt;/span&gt;(&lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;)?.&lt;span class=&quot;hljs-property&quot;&gt;token&lt;/span&gt;;
  }
}

&lt;span class=&quot;hljs-comment&quot;&gt;// 当 User 实例被 GC 回收时，privateData 中对应的条目自动清除&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// 这是闭包 + 私有数据的现代化替代方案&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-20&quot;&gt;六、方案对比&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-21&quot;&gt;6.1 闭包 vs 其他实现私有变量的方式&lt;/h3&gt;















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方案&lt;/th&gt;&lt;th&gt;私有性&lt;/th&gt;&lt;th&gt;继承共享&lt;/th&gt;&lt;th&gt;内存效率&lt;/th&gt;&lt;th&gt;复杂度&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;闭包（工厂函数）&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;✅ 真正私有&lt;/td&gt;&lt;td&gt;❌ 每实例独立副本&lt;/td&gt;&lt;td&gt;一般（每实例一套闭包）&lt;/td&gt;&lt;td&gt;低&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;# 私有字段（ES2022）&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;✅ 真正私有&lt;/td&gt;&lt;td&gt;✅ 原型共享&lt;/td&gt;&lt;td&gt;高（最优）&lt;/td&gt;&lt;td&gt;极低&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;_ 约定（下划线）&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;❌ 假私有（可访问）&lt;/td&gt;&lt;td&gt;✅ 原型共享&lt;/td&gt;&lt;td&gt;最高&lt;/td&gt;&lt;td&gt;最低&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;WeakMap&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;✅ 真正私有&lt;/td&gt;&lt;td&gt;✅ 外部 Map 管理&lt;/td&gt;&lt;td&gt;高&lt;/td&gt;&lt;td&gt;中&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Symbol&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;❌ 可通过 Reflect.ownKeys 发现&lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;td&gt;高&lt;/td&gt;&lt;td&gt;中&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ES2022 # 私有字段（推荐现代项目使用）&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Counter&lt;/span&gt; {
  #count = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;                       &lt;span class=&quot;hljs-comment&quot;&gt;// 真正私有，外部完全不可访问&lt;/span&gt;

  &lt;span class=&quot;hljs-title function_&quot;&gt;increment&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) { &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; ++&lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.#count; }
  &lt;span class=&quot;hljs-keyword&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;value&lt;/span&gt;() { &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-variable language_&quot;&gt;this&lt;/span&gt;.#count; }
}

&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; c = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Counter&lt;/span&gt;();
c.#count;                          &lt;span class=&quot;hljs-comment&quot;&gt;// SyntaxError! 私有字段&lt;/span&gt;
c.&lt;span class=&quot;hljs-title function_&quot;&gt;increment&lt;/span&gt;();                     &lt;span class=&quot;hljs-comment&quot;&gt;// 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-22&quot;&gt;6.2 闭包的优缺点总结&lt;/h3&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;维度&lt;/th&gt;&lt;th&gt;详情&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;优点&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;① 变量私有化 / 数据封装 ② 实现柯里化 / 偏函数 ③ 模块模式 ④ 防抖节流 / 状态保持 ⑤ React Hooks 的基础&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;缺点&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;① 内存泄漏风险（外部变量无法被回收）② 循环引用（旧 IE 问题）③ 变量查找性能开销 ④ 调试困难（闭包变量不易观察）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;适用场景&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;需要保持状态的工厂函数、事件回调、异步回调、模块封装&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;避免场景&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;简单工具函数、性能热点代码、不需要外部变量的纯函数&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-23&quot;&gt;七、完整流程串联&lt;/h2&gt;
&lt;p&gt;下面用一个完整的例子走通「闭包创建 → 使用 → 泄漏 → 修复」的全过程：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-javascript&quot; lang=&quot;javascript&quot;&gt;【步骤 &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;】定义外部函数
  &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;factory&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {
    &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; secret = &lt;span class=&quot;hljs-string&quot;&gt;&#39;xxx&#39;&lt;/span&gt;;            &lt;span class=&quot;hljs-comment&quot;&gt;// 将被闭包捕获的变量&lt;/span&gt;

    &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; {
      &lt;span class=&quot;hljs-title function_&quot;&gt;get&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) { &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; secret; },    &lt;span class=&quot;hljs-comment&quot;&gt;// 闭包 A：读取 secret&lt;/span&gt;
      &lt;span class=&quot;hljs-title function_&quot;&gt;set&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;v&lt;/span&gt;) { secret = v; },      &lt;span class=&quot;hljs-comment&quot;&gt;// 闭包 B：写入 secret&lt;/span&gt;
      &lt;span class=&quot;hljs-title function_&quot;&gt;reset&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) { secret = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;; }   &lt;span class=&quot;hljs-comment&quot;&gt;// 闭包 C：清空 secret&lt;/span&gt;
    };
  }

【步骤 &lt;span class=&quot;hljs-number&quot;&gt;2&lt;/span&gt;】调用工厂，获得闭包实例
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; instance = &lt;span class=&quot;hljs-title function_&quot;&gt;factory&lt;/span&gt;();
  &lt;span class=&quot;hljs-comment&quot;&gt;// factory() 执行完毕，但其内部的 secret 被 3 个闭包引用&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// → secret 不会随 factory 的执行上下文销毁而消失&lt;/span&gt;

【步骤 &lt;span class=&quot;hljs-number&quot;&gt;3&lt;/span&gt;】通过闭包操作私有数据
  instance.&lt;span class=&quot;hljs-title function_&quot;&gt;get&lt;/span&gt;();                  &lt;span class=&quot;hljs-comment&quot;&gt;// &#39;xxx&#39;&lt;/span&gt;
  instance.&lt;span class=&quot;hljs-title function_&quot;&gt;set&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;new value&#39;&lt;/span&gt;);       &lt;span class=&quot;hljs-comment&quot;&gt;// 修改成功&lt;/span&gt;
  instance.&lt;span class=&quot;hljs-title function_&quot;&gt;get&lt;/span&gt;();                  &lt;span class=&quot;hljs-comment&quot;&gt;// &#39;new value&#39;&lt;/span&gt;

【步骤 &lt;span class=&quot;hljs-number&quot;&gt;4&lt;/span&gt;】不再需要时，手动释放
  instance = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;                 &lt;span class=&quot;hljs-comment&quot;&gt;// 3 个闭包函数全部断开引用&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// → secret 现在没有任何引用可达&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// → GC 下一次运行时回收 secret&lt;/span&gt;

【步骤 &lt;span class=&quot;hljs-number&quot;&gt;5&lt;/span&gt;】如果忘记释放（泄漏！）
  &lt;span class=&quot;hljs-comment&quot;&gt;// instance 被全局变量 / DOM 事件 / 定时器长期持有&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// → secret 永远不释放&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// → 如果 secret 是大对象 → 内存持续增长&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-24&quot;&gt;最小可运行的完整 Demo&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-html&quot; lang=&quot;html&quot;&gt;&lt;span class=&quot;hljs-meta&quot;&gt;&amp;lt;!DOCTYPE &lt;span class=&quot;hljs-keyword&quot;&gt;html&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;html&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;body&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;id&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;btn&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;点击计数&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;span&lt;/span&gt; &lt;span class=&quot;hljs-attr&quot;&gt;id&lt;/span&gt;=&lt;span class=&quot;hljs-string&quot;&gt;&quot;display&quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;0&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;span&lt;/span&gt;&amp;gt;&lt;/span&gt;

  &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;script&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;javascript&quot;&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// 1. 用闭包创建一个安全的计数器&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;createSafeCounter&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;displayEl&lt;/span&gt;) {
      &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; count = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;                &lt;span class=&quot;hljs-comment&quot;&gt;// 私有变量&lt;/span&gt;

      &lt;span class=&quot;hljs-keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;increment&lt;/span&gt;(&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) {        &lt;span class=&quot;hljs-comment&quot;&gt;// 闭包函数&lt;/span&gt;
        count++;
        displayEl.&lt;span class=&quot;hljs-property&quot;&gt;textContent&lt;/span&gt; = count;
        &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;`count = &lt;span class=&quot;hljs-subst&quot;&gt;${count}&lt;/span&gt; (私有变量，外部无法直接改)`&lt;/span&gt;);
      }

      &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; increment;             &lt;span class=&quot;hljs-comment&quot;&gt;// 返回闭包&lt;/span&gt;
    }

    &lt;span class=&quot;hljs-comment&quot;&gt;// 2. 绑定到按钮&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; btn = &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;getElementById&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;btn&#39;&lt;/span&gt;);
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; display = &lt;span class=&quot;hljs-variable language_&quot;&gt;document&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;getElementById&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;display&#39;&lt;/span&gt;);
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; counter = &lt;span class=&quot;hljs-title function_&quot;&gt;createSafeCounter&lt;/span&gt;(display);   &lt;span class=&quot;hljs-comment&quot;&gt;// 创建闭包实例&lt;/span&gt;

    btn.&lt;span class=&quot;hljs-title function_&quot;&gt;addEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;click&#39;&lt;/span&gt;, counter);

    &lt;span class=&quot;hljs-comment&quot;&gt;// 3. 演示安全性和独立性&lt;/span&gt;
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; counter2 = &lt;span class=&quot;hljs-title function_&quot;&gt;createSafeCounter&lt;/span&gt;(&lt;span class=&quot;hljs-title class_&quot;&gt;Object&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;assign&lt;/span&gt;({}, display));
    &lt;span class=&quot;hljs-comment&quot;&gt;// counter 和 counter2 各自拥有独立的 count&lt;/span&gt;

    &lt;span class=&quot;hljs-comment&quot;&gt;// 4. 页面卸载时清理（防泄漏）&lt;/span&gt;
    &lt;span class=&quot;hljs-variable language_&quot;&gt;window&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;addEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;unload&#39;&lt;/span&gt;, &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
      btn.&lt;span class=&quot;hljs-title function_&quot;&gt;removeEventListener&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;click&#39;&lt;/span&gt;, counter);
      &lt;span class=&quot;hljs-comment&quot;&gt;// counter 的引用在这里断开 → GC 可回收&lt;/span&gt;
    });

    &lt;span class=&quot;hljs-comment&quot;&gt;// 验证私有性：&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// console.log(count);          // ReferenceError! 外部无法访问&lt;/span&gt;
    &lt;span class=&quot;hljs-comment&quot;&gt;// counter.count = 999;         // 无效！count 不在 counter 上&lt;/span&gt;
  &lt;/span&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;script&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;body&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;html&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-25&quot;&gt;八、面试高频 Q&amp;amp;A&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Q1：什么是闭包？用一句话解释。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A：闭包是&lt;strong&gt;函数 + 它被定义时的词法环境&lt;/strong&gt;的组合。当一个函数能够&quot;记住&quot;并访问它所在作用域的变量，即使在其定义的作用域之外执行，就形成了闭包。本质是函数内部的 &lt;code&gt;[[Environment]]&lt;/code&gt; 指向了外部词法环境。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q2：闭包有哪些实际用途？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A：五大用途——① &lt;strong&gt;变量私有化&lt;/strong&gt;（createCounter 工厂函数）；② &lt;strong&gt;模块模式&lt;/strong&gt;（IIFE 封装私有 API）；③ &lt;strong&gt;函数柯里化&lt;/strong&gt;（add(a)(b)(c)）；④ &lt;strong&gt;状态保持&lt;/strong&gt;（防抖/节流/React setState）；⑤ &lt;strong&gt;回调函数&lt;/strong&gt;（事件处理/异步操作中访问外层变量）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q3：闭包为什么会导致内存泄漏？如何解决？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A：因为闭包保持了对外部变量的引用，使得这些变量&lt;strong&gt;无法被垃圾回收&lt;/strong&gt;。解决方案：(1) &lt;strong&gt;手动置空&lt;/strong&gt; &lt;code&gt;variable = null&lt;/code&gt;；(2) &lt;strong&gt;及时移除事件监听器&lt;/strong&gt; &lt;code&gt;removeEventListener&lt;/code&gt;；(3) &lt;strong&gt;组件销毁时清理&lt;/strong&gt;（React useEffect return 清理函数）；(4) &lt;strong&gt;避免不必要的闭包&lt;/strong&gt;（不需要外部变量就不放在闭包位置）；(5) &lt;strong&gt;用 WeakMap 替代强引用&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q4：循环中的闭包为什么输出全是一样的值？（经典题）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A：&lt;code&gt;var&lt;/code&gt; 是&lt;strong&gt;函数作用域&lt;/strong&gt;，循环中的 &lt;code&gt;var i&lt;/code&gt; 只有一个，所有 setTimeout 回调共享同一个 &lt;code&gt;i&lt;/code&gt;，当回调执行时循环已结束，&lt;code&gt;i&lt;/code&gt; 已是最终值。修复方法：(1) &lt;strong&gt;用 &lt;code&gt;let&lt;/code&gt; 替代 &lt;code&gt;var&lt;/code&gt;&lt;/strong&gt;（let 是块级作用域，每次迭代创建新绑定）；(2) &lt;strong&gt;用 IIFE&lt;/strong&gt; &lt;code&gt;(function(j){...})(i)&lt;/code&gt; 为每次迭代创建独立作用域。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q5：ES2022 的 &lt;code&gt;#&lt;/code&gt; 私有字段和闭包实现的私有变量有什么区别？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A：&lt;code&gt;#&lt;/code&gt; 私有字段是语言级别的真私有（语法层面禁止外部访问），且存储在原型上（所有实例共享一份方法），内存效率更高；闭包私有变量是运行时层面的（通过作用域隐藏），每实例各自持有一份闭包副本。新项目优先用 &lt;code&gt;#&lt;/code&gt; 字段，需要动态私有数据或兼容旧环境时用闭包。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-26&quot;&gt;九、记忆口诀&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;函数打包出生地&lt;/strong&gt;：闭包 = 函数 + 出生时的词法环境，走到哪都能用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;私有变量靠它藏&lt;/strong&gt;：createCounter 让 count 成私有，外部改不了&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;循环 var 要注意&lt;/strong&gt;：var 共享同一个 i，改 let 或 IIFE 就行&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不用赶紧断引用&lt;/strong&gt;：&lt;code&gt;removeEventListener&lt;/code&gt; + &lt;code&gt;= null&lt;/code&gt;，别让大对象赖着不走&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能用 # 号就用 #&lt;/strong&gt;：ES2022 私有字段更省内存，闭包留给需要动态封装的场景&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;本笔记由「前端面试学习笔记」skill 生成，配套 SVG 图解见正文。&lt;/em&gt;&lt;/p&gt;</description><link>https://juejin.cn/post/7671100905954000932</link><guid isPermaLink="false">https://juejin.cn/post/7671100905954000932</guid><pubDate>Fri, 07 Aug 2026 11:29:17 GMT</pubDate><author>做前端的娜娜子</author><category>前端</category><category>JavaScript</category><category>掘金·金石计划</category></item><item><title>🧵 浏览器里的第二大脑：Web Worker</title><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;当计算量大到 Event Loop 也撑不住时，是时候请出真正的&quot;多线程&quot;了&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-0&quot;&gt;📌 导语&lt;/h2&gt;
&lt;p&gt;如果你写过前端，一定遇到过这样的场景：页面里某个按钮一点，整个界面就像被冻住了一样——滚动卡顿、点击无响应、动画定格……用户体验瞬间归零。&lt;/p&gt;
&lt;p&gt;这个问题的根源，叫做 &lt;strong&gt;&quot;JS 单线程&quot;&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;JS 生来就是单线程的，这决定了它一次只能做一件事。遇到耗时任务，要么用 Event Loop 异步&quot;挂起&quot;，要么……就只能硬扛着让页面卡死。&lt;/p&gt;
&lt;p&gt;那有没有第三种方案？&lt;strong&gt;既不用阻塞页面，又能真正并行计算？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;答案是 &lt;strong&gt;Web Worker&lt;/strong&gt; —— 浏览器给 JS 开辟的一条独立后台线程，专门用来承接那些 Event Loop 也扛不住的重活儿。&lt;/p&gt;
&lt;p&gt;这篇文章会从 JS 单线程的底层原理讲起，逐步深入到 Web Worker 的完整用法、实战 Demo、踩坑经验，以及一个有趣的主线程对比实验。&lt;strong&gt;全文 5000+ 字，建议先收藏再阅读。&lt;/strong&gt; 📖&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-1&quot;&gt;一、JS 为什么是单线程？🤔&lt;/h2&gt;
&lt;p&gt;JS 诞生之初是给浏览器做前端交互和脚本工作的，核心任务是&lt;strong&gt;操作页面（DOM）&lt;/strong&gt;、&lt;strong&gt;响应用户点击/滚动&lt;/strong&gt;等。&lt;/p&gt;
&lt;p&gt;如果 JS 是多线程，就会出现&lt;strong&gt;多个线程同时改同一个 DOM 元素&lt;/strong&gt;的情况——线程 A 想把这个按钮改成红色，线程 B 想把它删掉，结果不可预测，页面一致性无从保证。所以 JS 设计为单线程，一次只做一件事，简单可靠。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-2&quot;&gt;二、Event Loop（事件循环）—— JS 的异步之道 🌊&lt;/h2&gt;
&lt;p&gt;页面越来越复杂之后，单线程就面临一个问题：&lt;strong&gt;遇到耗时任务怎么办？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;JS 的解决方案是 &lt;strong&gt;Event Loop（事件循环）&lt;/strong&gt;：&lt;/p&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;代码类型&lt;/th&gt;&lt;th&gt;执行方式&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;同步代码&lt;/td&gt;&lt;td&gt;立刻执行，做完才往后走&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;异步代码&lt;/td&gt;&lt;td&gt;先挂起（跳过），等结果好了再回来处理&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;用餐厅比喻 🍽️：&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;角色&lt;/th&gt;&lt;th&gt;对应&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;主厨（主线程）&lt;/td&gt;&lt;td&gt;炒菜 + 接单&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Event Loop&lt;/td&gt;&lt;td&gt;服务员，&quot;菜单先放着，你先炒别的&quot;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;同步代码&lt;/td&gt;&lt;td&gt;&quot;立刻炒，炒完才接下一单&quot;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;异步代码（setTimeout/fetch）&lt;/td&gt;&lt;td&gt;&quot;这个要炖半小时，挂单，到时间了服务员叫你&quot;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;核心思想：&lt;strong&gt;异步无阻塞&lt;/strong&gt;——不要卡在这，跳过执行后面的任务，前端要尽快响应用户交互（屏幕滚动、鼠标点击）。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-3&quot;&gt;三、Event Loop 不够用时：Web Worker 登场 🚀&lt;/h2&gt;
&lt;p&gt;LLM（大模型推理）、游戏引擎计算、加密等场景，&lt;strong&gt;计算量巨大&lt;/strong&gt;，Event Loop 异步也无能为力——因为异步只是&quot;换个时间执行&quot;，最终还是&lt;strong&gt;同一个线程在算&lt;/strong&gt;，CPU 被占满时页面照样卡死。&lt;/p&gt;
&lt;p&gt;于是 HTML5 提供了 &lt;strong&gt;Web Worker&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器给 JS 开辟一条&lt;strong&gt;独立后台线程&lt;/strong&gt;，拥有独立内存空间&lt;/li&gt;
&lt;li&gt;专门承接纯计算任务，和主线程&lt;strong&gt;并行执行&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;算完后通过&lt;strong&gt;消息机制&lt;/strong&gt;告知主线程&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;简单理解：Event Loop 是&quot;同一个厨师换个时间做&quot;，Web Worker 是&quot;隔壁厨房再雇一个厨师同时做&quot;。&lt;/strong&gt; 👨‍🍳👨‍🍳&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-4&quot;&gt;四、浏览器架构 —— 为什么 JS 单线程但 Worker 能多线程？🏗️&lt;/h2&gt;
&lt;p&gt;JS 本身运行在 &lt;strong&gt;V8 引擎&lt;/strong&gt;上，V8 确实是单线程执行 JS 代码的。但&lt;strong&gt;浏览器（Chrome/Edge）是一个用 C++ 写的多进程、多线程软件&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/14e8df44ad4e4c869d97f05a99e8cb32~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oOz6KaB5oiQ5Li657OV57OV5omL:q75.awebp?rk3s=f64ab15b&amp;amp;x-expires=1786705199&amp;amp;x-signature=3HDYBR3M6MJztjMZHx5VOUS4JFA%3D&quot; alt=&quot;deepseek_mermaid_20260807_ea803a.png&quot; loading=&quot;lazy&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;JS 的单线程机制并没有改变。只是浏览器这个&quot;房东&quot;开了多个 V8 实例各跑各的。Worker 是浏览器给的辅助能力，JS 语言本身还是单线程。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-5&quot;&gt;五、Web Worker 的类型 📂&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;类型&lt;/th&gt;&lt;th&gt;说明&lt;/th&gt;&lt;th&gt;适用场景&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Dedicated Worker（专用）&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;只能被创建它的页面使用，一对一&lt;/td&gt;&lt;td&gt;最常见，本 Demo 用的就是这个&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Shared Worker（共享）&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;同源多个页面/iframe 共享一个 Worker&lt;/td&gt;&lt;td&gt;多标签页数据同步&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Service Worker&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;网络代理层，拦截请求&lt;/td&gt;&lt;td&gt;PWA 离线缓存、推送通知&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;本文主要围绕 &lt;strong&gt;Dedicated Worker&lt;/strong&gt; 展开，这是日常开发中使用最广泛的一种。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-6&quot;&gt;六、Worker 能做什么，不能做什么？✅❌&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-7&quot;&gt;✅ 能做的&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;console.log&lt;/code&gt; —— 打印日志&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setTimeout / setInterval&lt;/code&gt; —— 定时器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fetch / XMLHttpRequest&lt;/code&gt; —— 发网络请求&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WebSocket&lt;/code&gt; —— 长连接&lt;/li&gt;
&lt;li&gt;&lt;code&gt;JSON / Math&lt;/code&gt; —— 标准 JS 计算&lt;/li&gt;
&lt;li&gt;&lt;code&gt;importScripts()&lt;/code&gt; —— 引入其他 JS 文件（相当于主线程的 &lt;code&gt;&amp;lt;script src=&quot;...&quot;&amp;gt;&lt;/code&gt;，会在 Worker 里同步加载并执行）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IndexedDB&lt;/code&gt; —— 本地数据库&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// worker.js 里引入外部工具库&lt;/span&gt;
importScripts(&lt;span class=&quot;hljs-string&quot;&gt;&#39;./utils.js&#39;&lt;/span&gt;);  &lt;span class=&quot;hljs-comment&quot;&gt;// 加载后，utils.js 里的函数就可以直接用了&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-8&quot;&gt;❌ 不能做的（关键限制！）&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;不能操作 DOM&lt;/strong&gt; —— Worker 没有 &lt;code&gt;document&lt;/code&gt;、&lt;code&gt;window&lt;/code&gt; 对象&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不能访问 localStorage&lt;/strong&gt; —— 同上&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不能直接操作父页面的变量&lt;/strong&gt; —— 内存隔离&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;记忆口诀：&lt;strong&gt;Worker = 只能算数，不能碰页面。&lt;/strong&gt; 想更新 UI？算完发消息回主线程，让主线程去改。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-9&quot;&gt;七、消息通信机制 —— Worker 核心 📨&lt;/h2&gt;
&lt;p&gt;主线程和 Worker 线程的&lt;strong&gt;内存空间是隔离的&lt;/strong&gt;，不能直接共享变量或调函数，只能通过&quot;发消息&quot;通信：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/4b8062b6e6274120a7d477d54580a569~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oOz6KaB5oiQ5Li657OV57OV5omL:q75.awebp?rk3s=f64ab15b&amp;amp;x-expires=1786705199&amp;amp;x-signature=cftoNYGdR6pPuCX9H0rTiXgvKMw%3D&quot; alt=&quot;deepseek_mermaid_20260807_77d996.png&quot; loading=&quot;lazy&quot; referrerpolicy=&quot;no-referrer&quot;&gt;&lt;/p&gt;
&lt;h3 data-id=&quot;heading-10&quot;&gt;7.1 数据传递方式：结构化克隆（Structured Clone）🧬&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;postMessage&lt;/code&gt; 传递数据时，浏览器会把数据&lt;strong&gt;完整复制一份&lt;/strong&gt;（克隆体），Worker 改它的副本，主线程的原件不受影响。就像复印一份文件给别人，他在复印件上乱画，你的原件纹丝不动。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-11&quot;&gt;7.2 主线程 → Worker 📤&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 主线程发消息&lt;/span&gt;
workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;num&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;88&lt;/span&gt; });
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-12&quot;&gt;7.3 Worker → 主线程 📥&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// worker.js 收消息&lt;/span&gt;
self.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { num } = e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;;   &lt;span class=&quot;hljs-comment&quot;&gt;// 解构取出参数&lt;/span&gt;
  &lt;span class=&quot;hljs-comment&quot;&gt;// 干重活...&lt;/span&gt;
  self.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({ &lt;span class=&quot;hljs-attr&quot;&gt;result&lt;/span&gt;: sum });    &lt;span class=&quot;hljs-comment&quot;&gt;// 发回结果&lt;/span&gt;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-13&quot;&gt;7.4 主线程收结果 🎯&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 主线程收消息&lt;/span&gt;
workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { result } = e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;;
  &lt;span class=&quot;hljs-title function_&quot;&gt;setResult&lt;/span&gt;(result);  &lt;span class=&quot;hljs-comment&quot;&gt;// 更新页面&lt;/span&gt;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-14&quot;&gt;7.5 self 是什么？🧐&lt;/h3&gt;
&lt;p&gt;在 Worker 线程内部，&lt;code&gt;self&lt;/code&gt; 就是 Worker 的&lt;strong&gt;全局对象&lt;/strong&gt;，相当于主线程里的 &lt;code&gt;window&lt;/code&gt;。Worker 里没有 &lt;code&gt;window&lt;/code&gt;，只有 &lt;code&gt;self&lt;/code&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-15&quot;&gt;八、Web Worker 适合的场景 🎯&lt;/h2&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;场景&lt;/th&gt;&lt;th&gt;例子&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;大量数据计算&lt;/td&gt;&lt;td&gt;找质数、排序百万条数据、图片处理&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;游戏引擎&lt;/td&gt;&lt;td&gt;物理碰撞检测、寻路算法&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LLM / AI&lt;/td&gt;&lt;td&gt;浏览器端跑模型推理&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;加密密集计算&lt;/td&gt;&lt;td&gt;文件 MD5、AES 加密大文件、bcrypt&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;大文件处理&lt;/td&gt;&lt;td&gt;前端解析 Excel/CSV、压缩图片&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;实时数据流&lt;/td&gt;&lt;td&gt;WebSocket 收到的数据流解析&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-16&quot;&gt;九、实战 Demo：useRef + Web Worker 耗时运算 💻&lt;/h2&gt;
&lt;p&gt;下面用实际代码完整演示：用 Worker 执行 500 亿次循环计算，主线程照常响应用户。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-17&quot;&gt;9.1 整体架构 🏛️&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;①② 是页面加载时的&lt;strong&gt;初始化阶段&lt;/strong&gt;（useEffect 执行），③④ 是用户点击后的&lt;strong&gt;交互阶段&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;img src=&quot;https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/6c1cad2ccaee4de6b2ac813d758c5ca2~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5oOz6KaB5oiQ5Li657OV57OV5omL:q75.awebp?rk3s=f64ab15b&amp;amp;x-expires=1786705199&amp;amp;x-signature=KnSZGRwr71j9NwPfyUpxBqFGroo%3D&quot; alt=&quot;deepseek_mermaid_20260807_639951.png&quot; width=&quot;85%&quot; loading=&quot;lazy&quot; referrerpolicy=&quot;no-referrer&quot;&gt;
&lt;h3 data-id=&quot;heading-18&quot;&gt;9.2 App.jsx 关键代码拆解 🔍&lt;/h3&gt;
&lt;h4 data-id=&quot;heading-19&quot;&gt;🧩 为什么用 useRef 存 Worker？&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; workerRef = &lt;span class=&quot;hljs-title function_&quot;&gt;useRef&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;);  &lt;span class=&quot;hljs-comment&quot;&gt;// 持久化的可变对象&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React 中存数据有三种方式：&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方式&lt;/th&gt;&lt;th&gt;值变了会不会重新渲染？&lt;/th&gt;&lt;th&gt;适合存什么？&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;会&lt;/strong&gt;，组件重新跑一遍&lt;/td&gt;&lt;td&gt;要显示在页面上的数据&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;普通变量 &lt;code&gt;let x&lt;/code&gt;&lt;/td&gt;&lt;td&gt;不会，但每次渲染都重置&lt;/td&gt;&lt;td&gt;❌ 不持久，没用&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;useRef&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;不会&lt;/strong&gt;，且值跨渲染持久&lt;/td&gt;&lt;td&gt;Worker 实例、定时器 ID&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Worker 实例不需要触发渲染（你不是要在页面上显示这个 Worker 对象），所以 &lt;code&gt;useRef&lt;/code&gt; 是唯一正确选择。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-20&quot;&gt;🧩 useEffect：组件挂载时初始化 Worker&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-comment&quot;&gt;// 创建 Worker 线程&lt;/span&gt;
  workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(
    &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;URL&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&quot;./worker.js&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;meta&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;url&lt;/span&gt;)
  );

  &lt;span class=&quot;hljs-comment&quot;&gt;// 设置&quot;耳朵&quot;：Worker 算完发消息回来，这里收&lt;/span&gt;
  workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
    &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { result } = e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;;
    &lt;span class=&quot;hljs-title function_&quot;&gt;setResult&lt;/span&gt;(result);    &lt;span class=&quot;hljs-comment&quot;&gt;// 更新页面显示结果&lt;/span&gt;
    &lt;span class=&quot;hljs-title function_&quot;&gt;setLoading&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;);    &lt;span class=&quot;hljs-comment&quot;&gt;// 按钮恢复可点击&lt;/span&gt;
  };

  &lt;span class=&quot;hljs-comment&quot;&gt;// 清理函数：组件卸载时，关掉 Worker 释放内存&lt;/span&gt;
  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;?.&lt;span class=&quot;hljs-title function_&quot;&gt;terminate&lt;/span&gt;();  &lt;span class=&quot;hljs-comment&quot;&gt;// 杀死 Worker 线程&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;        &lt;span class=&quot;hljs-comment&quot;&gt;// 清空引用，帮助垃圾回收&lt;/span&gt;
  };
}, []);  &lt;span class=&quot;hljs-comment&quot;&gt;// 空依赖 = 只在组件第一次挂载时执行&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意事项 ⚠️：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;new URL(&quot;./worker.js&quot;, import.meta.url)&lt;/code&gt;：Vite 的写法，正确找到打包后的 worker.js&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?.&lt;/code&gt;（可选链）：如果 Worker 还没创建好就被卸载，不会报错&lt;/li&gt;
&lt;li&gt;&lt;code&gt;terminate()&lt;/code&gt;：Worker 实例自带的方法，立刻杀死线程&lt;/li&gt;
&lt;li&gt;空依赖 &lt;code&gt;[]&lt;/code&gt;：确保只创建一个 Worker，不会每次渲染都新建&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Worker 线程创建成本很高&lt;/strong&gt;（浏览器要开新线程、初始化 V8 实例），应该一次创建、反复使用。不要每次任务都 &lt;code&gt;new Worker()&lt;/code&gt;，否则反而比单线程还慢&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-id=&quot;heading-21&quot;&gt;🧩 按钮点击：给 Worker 派活&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;startHeavyCalc&lt;/span&gt; = (&lt;span class=&quot;hljs-params&quot;&gt;&lt;/span&gt;) =&amp;gt; {
  &lt;span class=&quot;hljs-title function_&quot;&gt;setLoading&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;true&lt;/span&gt;);               &lt;span class=&quot;hljs-comment&quot;&gt;// 按钮变灰 + 文字变为&quot;正在后台计算...&quot;&lt;/span&gt;
  workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({
    &lt;span class=&quot;hljs-attr&quot;&gt;num&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;88&lt;/span&gt;                       &lt;span class=&quot;hljs-comment&quot;&gt;// 传给 Worker 的参数&lt;/span&gt;
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;postMessage&lt;/code&gt; 是异步的，消息发出后主线程立刻继续干别的，不会等 Worker 算完。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-22&quot;&gt;🧩 条件渲染显示结果&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;{result &amp;amp;&amp;amp; &lt;span class=&quot;xml&quot;&gt;&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-name&quot;&gt;h3&lt;/span&gt;&amp;gt;&lt;/span&gt;计算结果：{result}&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-name&quot;&gt;h3&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React 的条件渲染简写。&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; 短路运算：如果 &lt;code&gt;result&lt;/code&gt; 有值（真值），就渲染后面的 JSX；如果是空字符串（假值），什么都不显示。&lt;/p&gt;
&lt;h4 data-id=&quot;heading-23&quot;&gt;🧩 disabled 防重复点击&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-jsx&quot; lang=&quot;jsx&quot;&gt;&amp;lt;button onClick={startHeavyCalc} disabled={loading}&amp;gt;
  {loading ? &lt;span class=&quot;hljs-string&quot;&gt;&quot;正在后台计算...&quot;&lt;/span&gt; : &lt;span class=&quot;hljs-string&quot;&gt;&quot;启动繁重计算&quot;&lt;/span&gt;}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;disabled={loading}&lt;/code&gt;：loading 为 true 时按钮变灰，防止用户疯狂点击&lt;/li&gt;
&lt;li&gt;三元表达式切换文字：给用户明确的状态反馈&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-id=&quot;heading-24&quot;&gt;9.3 worker.js 关键代码拆解 🔍&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// self：Worker 线程的全局对象（相当于主线程的 window）&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// self.onmessage：主线程发消息过来时，这个函数就会被调用&lt;/span&gt;
self.&lt;span class=&quot;hljs-property&quot;&gt;onmessage&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; { num } = e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;;      &lt;span class=&quot;hljs-comment&quot;&gt;// 解构取出主线程传来的参数&lt;/span&gt;
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;log&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&quot;worker收到主线程任务，参数为：&quot;&lt;/span&gt;, e.&lt;span class=&quot;hljs-property&quot;&gt;data&lt;/span&gt;);

  &lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; sum = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;
  &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;hljs-number&quot;&gt;50000000000&lt;/span&gt;; i++) {
    sum += num * i;            &lt;span class=&quot;hljs-comment&quot;&gt;// 500 亿次循环（纯计算，会耗时！）&lt;/span&gt;
  }

  self.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;({           &lt;span class=&quot;hljs-comment&quot;&gt;// 算完把结果发回主线程&lt;/span&gt;
    &lt;span class=&quot;hljs-attr&quot;&gt;result&lt;/span&gt;: sum
  });
};
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Worker 是纯 JS 环境，不是 React 环境，不能用任何 React Hook。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-25&quot;&gt;十、拓展：对比实验 —— 为什么需要 Worker？🧪&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ 以下为建议拓展方向，当前 Demo 代码中只有 Worker 执行按钮，未包含主线程对比按钮和计时器。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;光靠理论说不清楚 Worker 的价值，&lt;strong&gt;亲眼看到对比才是最好的学习方式&lt;/strong&gt;：&lt;/p&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;按钮&lt;/th&gt;&lt;th&gt;执行方式&lt;/th&gt;&lt;th&gt;效果&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&quot;主线程执行&quot;&lt;/td&gt;&lt;td&gt;在 App.jsx 里直接写 for 循环&lt;/td&gt;&lt;td&gt;页面卡死，滚动不了，计时器停止&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&quot;Worker 执行&quot;&lt;/td&gt;&lt;td&gt;postMessage 发给 Worker 算&lt;/td&gt;&lt;td&gt;页面丝滑，计时器照跳&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;建议在 Demo 中同时放两个按钮 + 一个实时跳动的计时器（&lt;code&gt;setInterval&lt;/code&gt; 每秒 +1）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;点&quot;主线程执行&quot; → 计时器停了 → &lt;strong&gt;证明主线程被阻塞&lt;/strong&gt; 🧊&lt;/li&gt;
&lt;li&gt;点&quot;Worker 执行&quot; → 计时器照跳 → &lt;strong&gt;证明 Worker 不阻塞主线程&lt;/strong&gt; ✨&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-26&quot;&gt;十一、Worker 生命周期管理 🔄&lt;/h2&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;方法&lt;/th&gt;&lt;th&gt;调用方&lt;/th&gt;&lt;th&gt;效果&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;new Worker(url)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;主线程&lt;/td&gt;&lt;td&gt;创建 Worker，浏览器开新线程&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;.postMessage(data)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;主线程 / Worker&lt;/td&gt;&lt;td&gt;发送消息&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;.onmessage&lt;/code&gt;&lt;/td&gt;&lt;td&gt;主线程 / Worker&lt;/td&gt;&lt;td&gt;接收消息的回调&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;.onerror&lt;/code&gt;&lt;/td&gt;&lt;td&gt;主线程&lt;/td&gt;&lt;td&gt;Worker 报错时触发&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;.terminate()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;主线程&lt;/td&gt;&lt;td&gt;立刻杀死 Worker（不给收尾机会）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;self.close()&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Worker 自己&lt;/td&gt;&lt;td&gt;Worker 主动退出&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 data-id=&quot;heading-27&quot;&gt;11.1 清理的最佳实践 🧹&lt;/h3&gt;
&lt;p&gt;组件卸载时必须关闭 Worker，否则会变成&quot;僵尸线程&quot;浪费内存：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(...);

  &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;?.&lt;span class=&quot;hljs-title function_&quot;&gt;terminate&lt;/span&gt;();  &lt;span class=&quot;hljs-comment&quot;&gt;// ① 可选链防崩溃，杀死线程&lt;/span&gt;
    workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-literal&quot;&gt;null&lt;/span&gt;;        &lt;span class=&quot;hljs-comment&quot;&gt;// ② 清空引用，帮助 GC 回收&lt;/span&gt;
  };
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-28&quot;&gt;11.2 Worker 错误处理 🚨&lt;/h3&gt;
&lt;p&gt;Worker 里的代码也可能报错（比如数据格式不对、计算溢出等），不处理的话报错会&quot;静默消失&quot;，很难排查。需要用 &lt;code&gt;onerror&lt;/code&gt; 来捕获：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;onerror&lt;/span&gt; = &lt;span class=&quot;hljs-function&quot;&gt;(&lt;span class=&quot;hljs-params&quot;&gt;e&lt;/span&gt;) =&amp;gt;&lt;/span&gt; {
  &lt;span class=&quot;hljs-variable language_&quot;&gt;console&lt;/span&gt;.&lt;span class=&quot;hljs-title function_&quot;&gt;error&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&#39;Worker 出错了：&#39;&lt;/span&gt;, e.&lt;span class=&quot;hljs-property&quot;&gt;message&lt;/span&gt;, &lt;span class=&quot;hljs-string&quot;&gt;&#39;行号：&#39;&lt;/span&gt;, e.&lt;span class=&quot;hljs-property&quot;&gt;lineno&lt;/span&gt;);
  &lt;span class=&quot;hljs-title function_&quot;&gt;setLoading&lt;/span&gt;(&lt;span class=&quot;hljs-literal&quot;&gt;false&lt;/span&gt;);  &lt;span class=&quot;hljs-comment&quot;&gt;// 恢复按钮，避免用户干等&lt;/span&gt;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;e&lt;/code&gt; 是一个 &lt;code&gt;ErrorEvent&lt;/code&gt; 对象，包含 &lt;code&gt;message&lt;/code&gt;（错误信息）、&lt;code&gt;filename&lt;/code&gt;（出错的脚本名）、&lt;code&gt;lineno&lt;/code&gt;（行号）。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-29&quot;&gt;十二、Transferable Objects（零拷贝传输）🚀&lt;/h2&gt;
&lt;p&gt;默认 &lt;code&gt;postMessage&lt;/code&gt; 传数据是&quot;复制&quot;（结构化克隆），传大数组时复制很慢。可以用&quot;转移所有权&quot;方式实现&lt;strong&gt;零拷贝&lt;/strong&gt;（适用于 &lt;code&gt;ArrayBuffer&lt;/code&gt; 等二进制数据，普通 JS 对象不适用）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// 普通方式：复制 100MB 数据的 ArrayBuffer，慢&lt;/span&gt;
worker.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;(bigArray.&lt;span class=&quot;hljs-property&quot;&gt;buffer&lt;/span&gt;);

&lt;span class=&quot;hljs-comment&quot;&gt;// 转移所有权：不复制，直接把这块内存&quot;移交&quot;给 Worker，快&lt;/span&gt;
worker.&lt;span class=&quot;hljs-title function_&quot;&gt;postMessage&lt;/span&gt;(bigArray.&lt;span class=&quot;hljs-property&quot;&gt;buffer&lt;/span&gt;, [bigArray.&lt;span class=&quot;hljs-property&quot;&gt;buffer&lt;/span&gt;]);
&lt;span class=&quot;hljs-comment&quot;&gt;// 注意：传完后，主线程的 bigArray 就不能用了（所有权已转走）&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;适用场景：处理大文件（图片、视频、大型数据集）时性能差距明显。日常小数据用不上，了解即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-30&quot;&gt;十三、踩坑记录 🕳️&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-31&quot;&gt;坑 1：JS 大数精度丢失&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// worker.js 里 500 亿次 sum += num * i&lt;/span&gt;
&lt;span class=&quot;hljs-comment&quot;&gt;// sum 最终会是一个天文数字，远超 JS 安全整数范围&lt;/span&gt;
&lt;span class=&quot;hljs-title class_&quot;&gt;Number&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;MAX_SAFE_INTEGER&lt;/span&gt;  &lt;span class=&quot;hljs-comment&quot;&gt;// 9007199254740991（约 9 千万亿）&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当计算结果超过 &lt;code&gt;Number.MAX_SAFE_INTEGER&lt;/code&gt; 时，后面的数字会丢失精度。如果需要精确的大数计算，要用 &lt;code&gt;BigInt&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; sum = &lt;span class=&quot;hljs-number&quot;&gt;0n&lt;/span&gt;;  &lt;span class=&quot;hljs-comment&quot;&gt;// BigInt 类型&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;let&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0n&lt;/span&gt;; i &amp;lt; &lt;span class=&quot;hljs-number&quot;&gt;50000000000n&lt;/span&gt;; i++) {
  sum += &lt;span class=&quot;hljs-title class_&quot;&gt;BigInt&lt;/span&gt;(num) * i;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;注意：&lt;code&gt;BigInt&lt;/code&gt; 不能通过结构化克隆传给主线程，需要特殊处理。学习 Demo 不用管精度，实际项目要注意。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-id=&quot;heading-32&quot;&gt;坑 2：忘记清理 Worker 🧟&lt;/h3&gt;
&lt;p&gt;组件卸载后 Worker 还在跑 → 浏览器多出一条&quot;僵尸线程&quot;，浪费 CPU 和内存。&lt;/p&gt;
&lt;p&gt;✅ 解法：&lt;code&gt;useEffect&lt;/code&gt; 的 &lt;code&gt;return&lt;/code&gt; 里调 &lt;code&gt;terminate() + = null&lt;/code&gt;。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-33&quot;&gt;坑 3：Worker 里用 React Hook&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ❌ Worker 里不能这样写！&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; { useState } &lt;span class=&quot;hljs-keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;hljs-string&quot;&gt;&#39;react&#39;&lt;/span&gt;;
&lt;span class=&quot;hljs-keyword&quot;&gt;const&lt;/span&gt; [count, setCount] = &lt;span class=&quot;hljs-title function_&quot;&gt;useState&lt;/span&gt;(&lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Worker 是纯 JS 环境，没有 React，没有组件概念。Worker 里只能写纯 JavaScript 代码。&lt;/p&gt;
&lt;h3 data-id=&quot;heading-34&quot;&gt;坑 4：&lt;code&gt;useEffect&lt;/code&gt; 依赖忘了空数组&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 没有 []，每次组件渲染都会 new Worker → 内存爆炸&lt;/span&gt;
&lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(...);
});

&lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 空依赖，只在挂载时执行一次&lt;/span&gt;
&lt;span class=&quot;hljs-title function_&quot;&gt;useEffect&lt;/span&gt;(&lt;span class=&quot;hljs-function&quot;&gt;() =&amp;gt;&lt;/span&gt; {
  workerRef.&lt;span class=&quot;hljs-property&quot;&gt;current&lt;/span&gt; = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(...);
}, []);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-id=&quot;heading-35&quot;&gt;坑 5：Vite 打包路径&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;hljs language-js&quot; lang=&quot;js&quot;&gt;&lt;span class=&quot;hljs-comment&quot;&gt;// ❌ 直接用字符串路径，Vite 可能找不到&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&quot;./worker.js&quot;&lt;/span&gt;);

&lt;span class=&quot;hljs-comment&quot;&gt;// ✅ 用 URL 构造函数，Vite 会正确处理打包路径&lt;/span&gt;
&lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title class_&quot;&gt;Worker&lt;/span&gt;(&lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;hljs-title function_&quot;&gt;URL&lt;/span&gt;(&lt;span class=&quot;hljs-string&quot;&gt;&quot;./worker.js&quot;&lt;/span&gt;, &lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;meta&lt;/span&gt;.&lt;span class=&quot;hljs-property&quot;&gt;url&lt;/span&gt;));
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-36&quot;&gt;十四、常见疑问 FAQ ❓&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-37&quot;&gt;Q：JS 到底是单线程还是多线程？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;A：JS 语言本身是单线程的（见上文&quot;浏览器架构&quot;节的详解）。简单说：浏览器（C++ 多线程软件）提供了 Worker 让你多开几个 V8 实例并行跑，JS 的单线程本质没变。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 data-id=&quot;heading-38&quot;&gt;Q：Worker 和 Event Loop 异步有什么区别？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;A：Event Loop 异步还是同一个线程，只是换个时间执行。Worker 是真的多开了一条线程，物理上并行执行。就像你一个人分时段做两件事 vs 两个人同时各做一件事。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 data-id=&quot;heading-39&quot;&gt;Q：为什么 Worker 不能操作 DOM？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;A：多线程同时改 DOM 会出现竞态条件，页面状态不可预测。所以设计上只有主线程有 DOM 权限，Worker 只能算完发结果回来。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 data-id=&quot;heading-40&quot;&gt;Q：postMessage 传大数组会不会慢？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;A：默认会复制（结构化克隆），大数据确实慢。可以用 Transferable Objects（转移所有权）实现零拷贝传输，但传完后原线程的变量就不可用了。&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 data-id=&quot;heading-41&quot;&gt;十五、总结 📝&lt;/h2&gt;
&lt;h3 data-id=&quot;heading-42&quot;&gt;15.1 核心概念速览 📋&lt;/h3&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;概念&lt;/th&gt;&lt;th&gt;一句话&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;JS 单线程&lt;/td&gt;&lt;td&gt;为页面一致性而设计，一次只做一件事&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Event Loop&lt;/td&gt;&lt;td&gt;异步无阻塞，挂起耗时任务先做别的&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Web Worker&lt;/td&gt;&lt;td&gt;浏览器开的独立线程，专门干重活&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;消息通信&lt;/td&gt;&lt;td&gt;postMessage 发、onmessage 收，数据是复制不是共享&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;内存隔离&lt;/td&gt;&lt;td&gt;Worker 不能碰 DOM/主线程变量，只能靠消息&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 data-id=&quot;heading-43&quot;&gt;15.2 React 中使用 Worker 的套路 🧩&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;useRef&lt;/code&gt; —— 存 Worker 实例，不触发渲染&lt;/li&gt;
&lt;li&gt;&lt;code&gt;useEffect([])&lt;/code&gt; —— 组件挂载时创建一个 Worker，卸载时 &lt;code&gt;terminate()&lt;/code&gt; 销毁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;postMessage&lt;/code&gt; —— 按钮点击时给 Worker 派活&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onmessage&lt;/code&gt; —— 收结果，&lt;code&gt;setState&lt;/code&gt; 更新页面&lt;/li&gt;
&lt;li&gt;清理函数 —— &lt;code&gt;terminate() + = null&lt;/code&gt;，防止内存泄漏&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-id=&quot;heading-44&quot;&gt;15.3 关键认知 🧠&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;JS 依旧是单线程语言。主线程负责脚本执行、DOM 渲染、用户交互等，忙得飞起。繁重的 CPU 计算会阻塞主线程，造成页面卡顿。于是浏览器提供 Web Worker 开辟独立后台线程承担纯计算任务。Worker 无法访问 DOM，只能通过消息和主线程相互通信。它只是浏览器提供的辅助线程，页面渲染、组件更新、交互事件依旧只能在唯一的 JS 主线程上运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;</description><link>https://juejin.cn/post/7671142702860058670</link><guid isPermaLink="false">https://juejin.cn/post/7671142702860058670</guid><pubDate>Fri, 07 Aug 2026 10:59:59 GMT</pubDate><author>想要成为糕糕手</author><category>前端</category><category>浏览器</category><category>JavaScript</category><category>React.js</category></item></channel></rss>