<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>React on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/react/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Tue, 23 Jun 2026 10:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/react/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>方糖猫 AI 视频平台：ReAct Agent 任务规划实战</title>
      <link>https://ftmao.com/blog/posts/react/</link>
      <pubDate>Tue, 23 Jun 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/react/</guid>
      <description>&lt;h1 id=&#34;方糖猫-ai-视频平台react-agent-任务规划实战&#34;&gt;方糖猫 AI 视频平台：ReAct Agent 任务规划实战&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;在方糖猫项目中，我们面对的是这样一个问题：用户输入一段文案，系统需要在几分钟内自动完成素材检索、脚本生成、视频渲染和后期处理，输出一条可发布的短视频。这个链路很长、步骤之间依赖复杂、失败场景多。我们最终选择用 &lt;strong&gt;ReAct Agent&lt;/strong&gt; 来编排整个生成流程。这篇文章会还原当时的设计思路、关键实现和踩过的坑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一业务场景为什么不是简单流水线&#34;&gt;一、业务场景：为什么不是简单流水线？&lt;/h2&gt;
&lt;p&gt;短视频自动生成的典型流程大致如下：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;文案输入&amp;#34;] --&amp;gt; B[&amp;#34;素材检索&amp;#34;] --&amp;gt; C[&amp;#34;故事板生成&amp;#34;] --&amp;gt; D[&amp;#34;配音/字幕&amp;#34;] --&amp;gt; E[&amp;#34;视频渲染&amp;#34;] --&amp;gt; F[&amp;#34;FFmpeg 后处理&amp;#34;] --&amp;gt; G[&amp;#34;成片&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;最直觉的做法是用一个硬编码的工作流引擎按顺序执行每个步骤。但我们很快发现几个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;步骤之间的依赖不是固定的&lt;/strong&gt;。比如某些文案不需要 B-roll，某些素材缺失时需要回退到文字卡片，某些场景需要先合成配音再决定画面节奏。&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;p&gt;所以我们需要一个&lt;strong&gt;能够根据输入动态决策、具备工具调用能力、失败后能自主重试或降级&lt;/strong&gt;的编排机制。ReAct Agent 是其中一个可行方案。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二react-agent-的核心设计&#34;&gt;二、ReAct Agent 的核心设计&lt;/h2&gt;
&lt;h3 id=&#34;21-任务拆解&#34;&gt;2.1 任务拆解&lt;/h3&gt;
&lt;p&gt;我们把整个生成流程拆成 6 个核心步骤，每个步骤对应一个 Agent 任务：&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;AnalyzeIntent&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;RetrieveMaterials&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;根据关键词检索本地素材库&lt;/td&gt;
					&lt;td&gt;Top-K 图片/视频/文案片段&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;GenerateStoryboard&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;规划每帧画面、配音、视觉元素&lt;/td&gt;
					&lt;td&gt;故事板 JSON&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;SynthesizeSpeech&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;调用 TTS 生成配音&lt;/td&gt;
					&lt;td&gt;音频文件 URL&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;RenderVideo&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;调用 Remotion 渲染视频&lt;/td&gt;
					&lt;td&gt;视频文件 URL&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;PostProcess&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;FFmpeg 后处理：转码、压缩、加字幕&lt;/td&gt;
					&lt;td&gt;成片 URL&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;每个任务都是一个 &lt;strong&gt;Tool&lt;/strong&gt;，Agent 通过 Reasoning 决定调用哪个 Tool、以什么参数调用、如何处理返回结果。&lt;/p&gt;
&lt;h3 id=&#34;22-agent-loop-设计&#34;&gt;2.2 Agent Loop 设计&lt;/h3&gt;
&lt;p&gt;我们的 Agent 遵循典型的 ReAct 循环：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    O[&amp;#34;观察（Observation）&amp;#34;] --&amp;gt; T[&amp;#34;思考（Thought）&amp;#34;] --&amp;gt; A[&amp;#34;行动（Action）&amp;#34;] --&amp;gt; O&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;具体实现中，我把每次循环抽象为三个动作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Plan&lt;/strong&gt;：根据当前状态生成下一步计划&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Act&lt;/strong&gt;：调用 Tool 执行计划&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reflect&lt;/strong&gt;：检查 Tool 输出，决定继续、重试还是终止&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一个简化版的状态机如下：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Start[&amp;#34;Start&amp;#34;] --&amp;gt; AnalyzeIntent[&amp;#34;AnalyzeIntent&amp;#34;]
    AnalyzeIntent --&amp;gt; RetrieveMaterials[&amp;#34;RetrieveMaterials&amp;#34;]
    RetrieveMaterials -- &amp;#34;素材不足&amp;#34; --&amp;gt; Fallback[&amp;#34;FallbackToTextCards&amp;#34;]
    Fallback --&amp;gt; GenerateStoryboard[&amp;#34;GenerateStoryboard&amp;#34;]
    RetrieveMaterials --&amp;gt; GenerateStoryboard
    GenerateStoryboard --&amp;gt; SynthesizeSpeech[&amp;#34;SynthesizeSpeech&amp;#34;]
    SynthesizeSpeech -- &amp;#34;TTS 失败&amp;#34; --&amp;gt; RetryAudio[&amp;#34;Retry / SkipAudio&amp;#34;]
    SynthesizeSpeech --&amp;gt; RenderVideo[&amp;#34;RenderVideo&amp;#34;]
    RenderVideo -- &amp;#34;渲染失败&amp;#34; --&amp;gt; RetryRender[&amp;#34;Retry / ReduceResolution&amp;#34;]
    RenderVideo --&amp;gt; PostProcess[&amp;#34;PostProcess&amp;#34;]
    PostProcess --&amp;gt; EndNode[&amp;#34;End&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;三关键实现&#34;&gt;三、关键实现&lt;/h2&gt;
&lt;h3 id=&#34;31-tool-注册与统一接口&#34;&gt;3.1 Tool 注册与统一接口&lt;/h3&gt;
&lt;p&gt;所有 Tool 都实现统一的 &lt;code&gt;Tool&lt;/code&gt; 接口：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; abc &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; ABC, abstractmethod
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; typing &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; Any, Dict
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Tool&lt;/span&gt;(ABC):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@property&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@abstractmethod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt;(self) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@property&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@abstractmethod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;description&lt;/span&gt;(self) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@abstractmethod&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(self, params: Dict[str, Any], context: Dict[str, Any]) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; Dict[str, Any]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        params: LLM 生成的调用参数
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        context: 当前任务上下文，包含前面步骤的输出
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;        &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;pass&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;所有 Tool 注册到一个 Registry 中，Agent 根据 LLM 的输出动态调用：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ToolRegistry&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;__init__&lt;/span&gt;(self):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;_tools: Dict[str, Tool] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;register&lt;/span&gt;(self, tool: Tool):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;_tools[tool&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;name] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tool
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;get&lt;/span&gt;(self, name: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; Tool:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;_tools[name]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这种设计的好处是：新增一种视频类型或新增一个处理步骤，只需要新增一个 Tool，Agent 的编排逻辑不需要改动。&lt;/p&gt;
&lt;h3 id=&#34;32-agent-loop-核心代码&#34;&gt;3.2 Agent Loop 核心代码&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ReActAgent&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;__init__&lt;/span&gt;(self, llm_client, tool_registry, max_steps&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;15&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;llm &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; llm_client
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;tools &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tool_registry
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;max_steps &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; max_steps
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;run&lt;/span&gt;(self, user_input: str) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; Dict[str, Any]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        context &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_input&amp;#34;&lt;/span&gt;: user_input,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;history&amp;#34;&lt;/span&gt;: [],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;running&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; step &lt;span style=&#34;color:#f92672&#34;&gt;in&lt;/span&gt; range(self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;max_steps):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 1. 让 LLM 根据当前状态决定下一步&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            prompt &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;_build_prompt(context)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            action &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;llm&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;generate(prompt)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 2. 解析 LLM 输出：Tool 名称 + 参数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            tool_name, params &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;_parse_action(action)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; tool_name &lt;span style=&#34;color:#f92672&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Finish&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;completed&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 3. 执行 Tool&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            tool &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;tools&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;get(tool_name)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; tool&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;run(params, context)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                result[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;success&amp;#34;&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Exception&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; e:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;success&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;False&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;error&amp;#34;&lt;/span&gt;: str(e)}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 4. 更新上下文&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;history&amp;#34;&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;step&amp;#34;&lt;/span&gt;: step,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;tool&amp;#34;&lt;/span&gt;: tool_name,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;params&amp;#34;&lt;/span&gt;: params,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;result&amp;#34;&lt;/span&gt;: result
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#75715e&#34;&gt;# 5. 失败时触发重试或降级&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;not&lt;/span&gt; result[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;success&amp;#34;&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                fallback &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;_decide_fallback(tool_name, result)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; fallback:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    context[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;history&amp;#34;&lt;/span&gt;]&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;append({&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;fallback&amp;#34;&lt;/span&gt;: fallback})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; context&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;33-prompt-设计&#34;&gt;3.3 Prompt 设计&lt;/h3&gt;
&lt;p&gt;Prompt 的核心不是让 LLM「自由发挥」，而是把它限定在一个结构化的决策空间里：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;你是一名短视频生成助手。当前任务状态如下：

- 用户输入：{user_input}
- 已执行步骤：{history}
- 可用工具：
  1. AnalyzeIntent: 分析文案意图
  2. RetrieveMaterials: 检索素材
  3. GenerateStoryboard: 生成故事板
  4. SynthesizeSpeech: 合成配音
  5. RenderVideo: 渲染视频
  6. PostProcess: 后处理
  7. Finish: 任务完成

请输出下一步操作，格式如下：

Thought: &amp;lt;你的推理过程&amp;gt;
Action: &amp;lt;Tool 名称&amp;gt;
ActionInput: &amp;lt;JSON 格式的参数&amp;gt;&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;为了让输出更稳定，我们做了几层约束：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;JSON Schema 约束&lt;/strong&gt;：要求 LLM 输出固定字段&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Few-shot 示例&lt;/strong&gt;：在 Prompt 里放 2-3 个正确示例&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pydantic 校验 + 重试&lt;/strong&gt;：后端严格校验输出格式，失败自动重试&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四踩过的坑&#34;&gt;四、踩过的坑&lt;/h2&gt;
&lt;h3 id=&#34;41-llm-输出格式不稳定&#34;&gt;4.1 LLM 输出格式不稳定&lt;/h3&gt;
&lt;p&gt;最早用纯 Prompt 约束时，LLM 有时输出 &lt;code&gt;Action: GenerateStoryboard&lt;/code&gt;，有时输出 &lt;code&gt;下一步：生成故事板&lt;/code&gt;，导致解析失败率很高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;引入 JSON Schema，明确要求输出 JSON&lt;/li&gt;
&lt;li&gt;后端用 Pydantic 校验，失败自动重试 3 次&lt;/li&gt;
&lt;li&gt;重试时 temperature 从 0.7 降到 0.2，让输出更稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;效果：结构解析成功率从约 75% 提升到 98% 以上。&lt;/p&gt;
&lt;h3 id=&#34;42-任务卡住没有超时机制&#34;&gt;4.2 任务卡住没有超时机制&lt;/h3&gt;
&lt;p&gt;早期 Agent 会无限循环，比如某一步失败后 LLM 反复决定重试同一个 Tool。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置 &lt;code&gt;max_steps=15&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;每个 Tool 设置独立超时&lt;/li&gt;
&lt;li&gt;同一 Tool 连续失败 2 次后强制降级或人工介入&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;43-上下文过长导致成本飙升&#34;&gt;4.3 上下文过长导致成本飙升&lt;/h3&gt;
&lt;p&gt;随着步骤推进，&lt;code&gt;history&lt;/code&gt; 越来越长，LLM Token 消耗快速增长。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决方案&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对 history 做摘要，只保留关键结果&lt;/li&gt;
&lt;li&gt;每个 Tool 输出只保留必要字段&lt;/li&gt;
&lt;li&gt;长视频场景下采用「分段生成 + 本地组装」策略&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五当前效果与待优化方向&#34;&gt;五、当前效果与待优化方向&lt;/h2&gt;
&lt;p&gt;目前这套 Agent 编排已经支撑方糖猫平台的核心生成链路，能够覆盖常见的口播、科普类视频生成场景。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;已验证的收益&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新增一种视频类型的支持成本明显降低，从改动整个工作流变成新增一个 Tool&lt;/li&gt;
&lt;li&gt;失败恢复路径清晰，常见异常无需人工介入&lt;/li&gt;
&lt;li&gt;渲染失败、素材缺失等场景可以自动降级，任务完成率稳定在较高水平&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;还在探索的方向&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更细粒度的 Agent 评估：不是只看任务是否完成，还要看成片质量&lt;/li&gt;
&lt;li&gt;多 Agent 协作：把创意策划、素材审核、质量评估拆成独立 Agent&lt;/li&gt;
&lt;li&gt;成本优化：不同步骤路由到不同模型，复杂推理用大模型，简单调用用小模型&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六什么场景适合-react什么不适合&#34;&gt;六、什么场景适合 ReAct，什么不适合&lt;/h2&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;/li&gt;
&lt;li&gt;流程经常变化，硬编码维护成本高&lt;/li&gt;
&lt;/ul&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;对延迟极度敏感的场景，Agent 的多次 LLM 调用会显著增加耗时&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七总结&#34;&gt;七、总结&lt;/h2&gt;
&lt;p&gt;ReAct Agent 不是银弹，但它确实帮我们解决了一个核心问题：&lt;strong&gt;如何把复杂的 AI 视频生成流程，从「硬编码脚本」变成「可编排、可观测、可恢复」的工程系统&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;关键经验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Tool 接口要统一，新增能力时不要改动 Agent 核心逻辑&lt;/li&gt;
&lt;li&gt;Prompt 输出要做 Schema 约束和后端校验，否则线上会不稳定&lt;/li&gt;
&lt;li&gt;失败处理、超时、降级必须一开始就想清楚&lt;/li&gt;
&lt;li&gt;Agent 的评估指标不能只看「跑通」，要看「生成质量」和「成本」&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;后续我会继续分享 RAG 素材库、多 Worker 分布式渲染、以及 Whisper + Remotion 渲染链路的实践，欢迎关注。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你也在做 AI Agent 相关的项目，欢迎在评论区交流。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>React 从入门到精通：环境搭建、核心语法、组件库选型与 AI 时代的开发方式</title>
      <link>https://ftmao.com/blog/posts/react-beginner-to-mastery/</link>
      <pubDate>Fri, 26 Aug 2022 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/react-beginner-to-mastery/</guid>
      <description>&lt;h1 id=&#34;react-从入门到精通&#34;&gt;React 从入门到精通&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：会 HTML/CSS/JS 基础，想系统学前端框架的同学；或写过 Vue/jQuery 想转 React 的工程师。
阅读目标：把环境搭起来、把核心语法学会、把坑提前踩掉、知道组件库怎么选，最后聊聊 AI 时代写前端的新姿势。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;React 的入门门槛不高，但「会写」和「写对」之间隔着一批经典的坑。这篇按真实学习顺序组织：环境 → 语法 → 心智模型 → 踩坑 → 组件库 → Demo → AI 时代的开发方式。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一学习路线图&#34;&gt;一、学习路线图&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;JS 基础&amp;lt;br/&amp;gt;ES6 语法&amp;#34;] --&amp;gt; B[&amp;#34;环境搭建&amp;lt;br/&amp;gt;Node + Vite&amp;#34;]
    B --&amp;gt; C[&amp;#34;核心语法&amp;lt;br/&amp;gt;JSX / 组件 / Props / State&amp;#34;]
    C --&amp;gt; D[&amp;#34;Hooks&amp;lt;br/&amp;gt;useState / useEffect / 自定义 Hook&amp;#34;]
    D --&amp;gt; E[&amp;#34;工程化&amp;lt;br/&amp;gt;路由 / 状态管理 / 组件库&amp;#34;]
    E --&amp;gt; F[&amp;#34;进阶&amp;lt;br/&amp;gt;性能优化 / 服务端渲染&amp;#34;]
    F --&amp;gt; G[&amp;#34;AI 协同开发&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;二开发环境搭建2022-年的正确姿势&#34;&gt;二、开发环境搭建（2022 年的正确姿势）&lt;/h2&gt;
&lt;h3 id=&#34;21-装-node用-nvm不要官网下载安装包&#34;&gt;2.1 装 Node：用 nvm，不要官网下载安装包&lt;/h3&gt;
&lt;p&gt;第一个坑往往出在环境上：老教程让你去 nodejs.org 下安装包，半年后项目要求换个 Node 版本，你就得重装。&lt;strong&gt;用 nvm 管理多版本&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Windows 用 nvm-windows，Mac/Linux 用 nvm&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;nvm install &lt;span style=&#34;color:#ae81ff&#34;&gt;16&lt;/span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# 2022 年的 LTS 是 16/18&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;nvm use &lt;span style=&#34;color:#ae81ff&#34;&gt;16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;node -v               &lt;span style=&#34;color:#75715e&#34;&gt;# v16.x.x&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;国内网络记得配镜像：&lt;code&gt;npm config set registry https://registry.npmmirror.com&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id=&#34;22-创建项目用-vite不要-create-react-app&#34;&gt;2.2 创建项目：用 Vite，不要 create-react-app&lt;/h3&gt;
&lt;p&gt;2022 年的第二个坑：老教程还在教 &lt;code&gt;create-react-app&lt;/code&gt;（CRA），它启动慢、打包慢、官方已基本停止维护。&lt;strong&gt;用 Vite&lt;/strong&gt;，秒级启动：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm create vite@latest my-app -- --template react
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cd my-app
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm install
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm run dev           &lt;span style=&#34;color:#75715e&#34;&gt;# 打开 http://localhost:5173&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;目录结构：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;my-app/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── index.html          # 入口 HTML
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── package.json
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├── vite.config.js
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;└── src/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ├── main.jsx        # 挂载根组件
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ├── App.jsx         # 根组件
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    └── components/     # 自己的组件放这里&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;23-编辑器与插件&#34;&gt;2.3 编辑器与插件&lt;/h3&gt;
&lt;p&gt;VS Code + 两个必装插件：&lt;strong&gt;ES7+ React Snippets&lt;/strong&gt;（输入 &lt;code&gt;rafce&lt;/code&gt; 一键生成组件骨架）、&lt;strong&gt;ESLint&lt;/strong&gt;（边写边提示 Hooks 依赖遗漏等问题）。再开 VS Code 的保存自动格式化（Prettier），团队风格从此不打架。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三核心语法记住一个公式就够了&#34;&gt;三、核心语法：记住一个公式就够了&lt;/h2&gt;
&lt;h3 id=&#34;31-心智模型ui--fstate&#34;&gt;3.1 心智模型：UI = f(state)&lt;/h3&gt;
&lt;p&gt;React 的全部思想一句话：&lt;strong&gt;界面是状态的函数&lt;/strong&gt;。你不直接操作 DOM，只改数据（state），React 负责把界面更新到和数据一致。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    S[&amp;#34;state 变化&amp;lt;br/&amp;gt;setState&amp;#34;] --&amp;gt; R[&amp;#34;React 重新执行组件函数&amp;lt;br/&amp;gt;得到新的虚拟 DOM&amp;#34;]
    R --&amp;gt; D[&amp;#34;Diff：对比新旧虚拟 DOM&amp;lt;br/&amp;gt;找出最小差异&amp;#34;]
    D --&amp;gt; DOM[&amp;#34;只把差异 patch 到真实 DOM&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这就是为什么 React 代码里&lt;strong&gt;永远不要 &lt;code&gt;document.getElementById&lt;/code&gt; 去改 DOM&lt;/strong&gt;——绕过状态直接改界面，两边就对不上了。&lt;/p&gt;
&lt;h3 id=&#34;32-组件与-jsx&#34;&gt;3.2 组件与 JSX&lt;/h3&gt;
&lt;p&gt;组件就是一个返回 JSX 的函数。JSX 是「长得像 HTML 的 JS」，注意三条规则：标签必须闭合、属性用驼峰（&lt;code&gt;className&lt;/code&gt; 不是 &lt;code&gt;class&lt;/code&gt;）、JS 表达式放 &lt;code&gt;{}&lt;/code&gt; 里：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Welcome&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt; }) {          &lt;span style=&#34;color:#75715e&#34;&gt;// props 是父组件传进来的参数
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;h1&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;className&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&amp;gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;你好&lt;/span&gt;, {&lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt;}&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;h1&lt;/span&gt;&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;App&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;Welcome&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;React&amp;#34;&lt;/span&gt; /&amp;gt;;    &lt;span style=&#34;color:#75715e&#34;&gt;// 组件像标签一样使用
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;33-state-与事件让界面动起来&#34;&gt;3.3 state 与事件：让界面动起来&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;useState&lt;/span&gt; } &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;react&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Counter&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; [&lt;span style=&#34;color:#a6e22e&#34;&gt;count&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;setCount&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;useState&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;);   &lt;span style=&#34;color:#75715e&#34;&gt;// 声明状态：当前值 + 修改函数
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onClick&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{() =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;setCount&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;count&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;)}&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a6e22e&#34;&gt;点了&lt;/span&gt; {&lt;span style=&#34;color:#a6e22e&#34;&gt;count&lt;/span&gt;} &lt;span style=&#34;color:#a6e22e&#34;&gt;次&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;规则：&lt;strong&gt;props 是父组件给的（只读），state 是自己的（用 set 函数改）&lt;/strong&gt;。数据流向永远是父 → 子，这叫单向数据流，是 React 排查问题比双向绑定框架容易的根本原因。&lt;/p&gt;
&lt;h3 id=&#34;34-列表渲染与-key&#34;&gt;3.4 列表渲染与 key&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TodoList&lt;/span&gt;({ &lt;span style=&#34;color:#a6e22e&#34;&gt;items&lt;/span&gt; }) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;ul&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {&lt;span style=&#34;color:#a6e22e&#34;&gt;items&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;map&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;item&lt;/span&gt;) =&amp;gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;li&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;item&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;}&amp;gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;item&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;text&lt;/span&gt;}&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;li&lt;/span&gt;&amp;gt;   &lt;span style=&#34;color:#75715e&#34;&gt;// key 用稳定 id，不要用 index！
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      ))}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;ul&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;35-useeffect处理副作用&#34;&gt;3.5 useEffect：处理副作用&lt;/h3&gt;
&lt;p&gt;数据请求、定时器、订阅这类「和渲染无关的事」放 &lt;code&gt;useEffect&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;useEffect&lt;/span&gt;(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/api/todos&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .&lt;span style=&#34;color:#a6e22e&#34;&gt;then&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;) =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;json&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    .&lt;span style=&#34;color:#a6e22e&#34;&gt;then&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;setTodos&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}, []);   &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;// 依赖数组为空 = 只在组件首次渲染后执行一次&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;四踩坑故事六个我真实掉进去过的坑&#34;&gt;四、踩坑故事：六个我真实掉进去过的坑&lt;/h2&gt;
&lt;h3 id=&#34;坑-1setstate-是异步的连续-1-只加了一次&#34;&gt;坑 1：setState 是「异步」的，连续 +1 只加了一次&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 想点一下加 3，结果只加了 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;setCount&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;count&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;setCount&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;count&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;setCount&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;count&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;原因：三次 &lt;code&gt;count&lt;/code&gt; 读的都是本次渲染的旧值。改法：&lt;strong&gt;用函数式更新&lt;/strong&gt;，每次拿到最新值：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;setCount&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;) =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;setCount&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;) =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;setCount&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;) =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;);   &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;// 这次真的 +3&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;教训：&lt;strong&gt;新 state 依赖旧 state 时，永远用函数形式&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;坑-2useeffect-里-setstate死循环刷爆页面&#34;&gt;坑 2：useEffect 里 setState，死循环刷爆页面&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;useEffect&lt;/span&gt;(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;setList&lt;/span&gt;([...&lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;newItem&lt;/span&gt;]);   &lt;span style=&#34;color:#75715e&#34;&gt;// list 变了 → 重新渲染 → effect 又执行 → 死循环
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}, [&lt;span style=&#34;color:#a6e22e&#34;&gt;list&lt;/span&gt;]);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;排查套路：页面卡死/请求刷屏，先找「&lt;strong&gt;effect 的依赖里有没有它自己改的东西&lt;/strong&gt;」。解法通常是改用函数式更新把依赖摘掉：&lt;code&gt;setList((l) =&amp;gt; [...l, newItem])&lt;/code&gt;，依赖数组就能清空。&lt;/p&gt;
&lt;h3 id=&#34;坑-3列表-key-用-index勾选状态全乱了&#34;&gt;坑 3：列表 key 用 index，勾选状态全乱了&lt;/h3&gt;
&lt;p&gt;Todo 列表勾了第 1 项，删掉它之后，第 2 项变成「已勾选」——因为 key 用 index 时，React 按位置复用 DOM，删除一项后后面所有项位置错位，复用错了组件内部状态。&lt;strong&gt;key 必须用数据的稳定 id&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;坑-4直接改-state-对象界面死活不更新&#34;&gt;坑 4：直接改 state 对象，界面死活不更新&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;user&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;新名字&amp;#34;&lt;/span&gt;;    &lt;span style=&#34;color:#75715e&#34;&gt;// 页面没反应！
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;setUser&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;user&lt;/span&gt;);           &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;// 还是没反应——引用没变，React 认为没变化&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;React 靠&lt;strong&gt;引用比较&lt;/strong&gt;判断要不要重渲染。正确姿势是造一个新对象：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;setUser&lt;/span&gt;({ ...&lt;span style=&#34;color:#a6e22e&#34;&gt;user&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;新名字&amp;#34;&lt;/span&gt; });&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;数组同理：用 &lt;code&gt;filter/map/concat&lt;/code&gt; 造新数组，不要 &lt;code&gt;push/splice&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id=&#34;坑-5定时器里的-stale-closure闭包旧值&#34;&gt;坑 5：定时器里的 stale closure（闭包旧值）&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;useEffect&lt;/span&gt;(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;timer&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;setInterval&lt;/span&gt;(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;console&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;log&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;count&lt;/span&gt;);        &lt;span style=&#34;color:#75715e&#34;&gt;// 永远是 0！
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }, &lt;span style=&#34;color:#ae81ff&#34;&gt;1000&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; () =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;clearInterval&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;timer&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}, []);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;effect 只在首次渲染执行，回调里捕获的 &lt;code&gt;count&lt;/code&gt; 永远是首次渲染的旧值。解法：把 &lt;code&gt;count&lt;/code&gt; 加进依赖数组，或用 &lt;code&gt;useRef&lt;/code&gt; 保存最新值。这类 bug 不报错、只是数据不新鲜，最难查——&lt;strong&gt;ESLint 的 exhaustive-deps 规则能提前拦住大半，别随手关掉&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;坑-6在组件里定义组件输入框每敲一个字就失焦&#34;&gt;坑 6：在组件里定义组件，输入框每敲一个字就失焦&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Form&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Input&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; () =&amp;gt; &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;input&lt;/span&gt; /&amp;gt;;   &lt;span style=&#34;color:#75715e&#34;&gt;// 每次渲染都是全新组件，DOM 被销毁重建
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;Input&lt;/span&gt; /&amp;gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;组件定义必须放模块顶层。这个坑的典型症状是「input 输入一个字符就丢焦点」，看到这个词直接想到它。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五底层原理为什么-react-是这样设计的&#34;&gt;五、底层原理：为什么 React 是这样设计的&lt;/h2&gt;
&lt;p&gt;前面六个坑，背规则可以避开，但理解原理才能「预判」。这一章讲透四件事：虚拟 DOM 与 Diff、Fiber 架构、渲染两阶段、Hooks 的实现。学完你会发现：&lt;strong&gt;第四节所有的坑，都是这四个原理的直接推论&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;51-虚拟-dom-与-diff-算法用-js-对象换性能&#34;&gt;5.1 虚拟 DOM 与 Diff 算法：用 JS 对象换性能&lt;/h3&gt;
&lt;p&gt;虚拟 DOM 就是用普通 JS 对象描述界面长什么样（&lt;code&gt;{ type: &#39;div&#39;, props: {...}, children: [...] }&lt;/code&gt;）。每次 state 变化，React 重新生成一棵新的虚拟 DOM 树，和旧的对比（Diff），只把差异更新到真实 DOM——直接操作 DOM 慢，操作 JS 对象快，这就是虚拟 DOM 的收益。&lt;/p&gt;
&lt;p&gt;全量对比两棵树的算法复杂度是 O(n³)，一万个节点就是十亿次比较，不可用。React 用三条「妥协」策略把它降到 O(n)：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph 策略[&amp;#34;Diff 的三条妥协策略&amp;#34;]
        T[&amp;#34;tree diff：只同层比较&amp;lt;br/&amp;gt;不跨层移动节点&amp;#34;]
        C[&amp;#34;component diff：组件类型变了&amp;lt;br/&amp;gt;整棵子树直接销毁重建&amp;#34;]
        E[&amp;#34;element diff：同层列表&amp;lt;br/&amp;gt;靠 key 识别 谁移动/新增/删除&amp;#34;]
    end
    T --&amp;gt; R[&amp;#34;结果：O(n) 完成对比&amp;#34;]
    C --&amp;gt; R
    E --&amp;gt; R&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三条策略各自对应一条实践纪律：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;tree diff&lt;/strong&gt; → 跨层级移动 DOM 节点代价极大，设计组件树时保持层级稳定；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;component diff&lt;/strong&gt; → 同一个位置不要渲染「类型忽变」的组件（&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; 来回切换），类型一变整棵重建；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;element diff&lt;/strong&gt; → 这就是&lt;strong&gt;坑 3 的原理&lt;/strong&gt;：key 是 React 识别「哪个节点是哪个」的唯一依据，用 index 当 key，删除中间项后后面所有 key 全变，React 就把节点张冠李戴了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;52-fiber-架构把渲染拆成可中断的小任务&#34;&gt;5.2 Fiber 架构：把渲染拆成可中断的小任务&lt;/h3&gt;
&lt;p&gt;React 15 之前，更新是递归遍历组件树，一旦开始就不能停——组件树大了，一帧 16ms 里算不完，页面就掉帧卡顿。React 16 重写架构，引入 &lt;strong&gt;Fiber&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fiber 是什么&lt;/strong&gt;：每个组件对应一个 Fiber 节点，渲染工作被拆成一个个以 Fiber 为单位的小任务；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;怎么连起来&lt;/strong&gt;：节点之间不用递归，用链表（&lt;code&gt;child&lt;/code&gt; 第一个子节点 / &lt;code&gt;sibling&lt;/code&gt; 下一个兄弟 / &lt;code&gt;return&lt;/code&gt; 父节点）——&lt;strong&gt;用指针代替调用栈，所以随时可以暂停、恢复、放弃&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双缓存&lt;/strong&gt;：内存里同时存在两棵树——屏幕上正在显示的 &lt;code&gt;current&lt;/code&gt; 树，和后台正在构建的 &lt;code&gt;workInProgress&lt;/code&gt; 树。新树在后台慢慢算，算完一次性把指针切过去，用户永远看不到「渲染到一半」的界面。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 旧架构[&amp;#34;React 15 栈调和&amp;#34;]
        A1[&amp;#34;递归遍历整棵树&amp;lt;br/&amp;gt;不可中断&amp;#34;] --&amp;gt; A2[&amp;#34;大树直接卡顿掉帧&amp;#34;]
    end
    subgraph 新架构[&amp;#34;React 16+ Fiber&amp;#34;]
        B1[&amp;#34;每个 Fiber 一个小任务&amp;lt;br/&amp;gt;可随时暂停让出主线程&amp;#34;] --&amp;gt; B2[&amp;#34;高优先级任务插队&amp;lt;br/&amp;gt;（用户输入 &amp;gt; 列表渲染）&amp;#34;]
        B2 --&amp;gt; B3[&amp;#34;workInProgress 树算完&amp;lt;br/&amp;gt;一次性切换&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;时间切片&lt;/strong&gt;带来的直接体验：输入框输入（高优先级）可以打断长列表渲染（低优先级），输入再也不卡。&lt;code&gt;useTransition&lt;/code&gt; 就是把这个优先级能力开放给开发者——把「不紧急的更新」包进 &lt;code&gt;startTransition&lt;/code&gt;，界面保持响应。&lt;/p&gt;
&lt;h3 id=&#34;53-渲染两阶段render-可中断commit-不可中断&#34;&gt;5.3 渲染两阶段：render 可中断，commit 不可中断&lt;/h3&gt;
&lt;p&gt;一次更新分两个阶段，分工严格：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph R[&amp;#34;render 阶段（可中断）&amp;#34;]
        R1[&amp;#34;调用组件函数&amp;lt;br/&amp;gt;生成新虚拟 DOM&amp;#34;]
        R2[&amp;#34;Diff，算出变更清单&amp;#34;]
    end
    subgraph C[&amp;#34;commit 阶段（不可中断）&amp;#34;]
        C1[&amp;#34;把变更一次性&amp;lt;br/&amp;gt;写入真实 DOM&amp;#34;]
        C2[&amp;#34;执行 useLayoutEffect&amp;lt;br/&amp;gt;再异步执行 useEffect&amp;#34;]
    end
    R --&amp;gt; C&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这一阶段划分解释了两个「为什么」：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;为什么组件函数里不能有副作用&lt;/strong&gt;（发请求、改全局变量）：render 阶段可中断、可重试、&lt;strong&gt;可能被调用多次&lt;/strong&gt;，副作用会重复执行。所以副作用必须放 &lt;code&gt;useEffect&lt;/code&gt;——它在 commit 之后执行，每次提交只跑一次。&lt;strong&gt;坑 2 的死循环本质也是没搞清这个边界&lt;/strong&gt;：effect 里改 state 触发新一轮 render→commit→effect，链条首尾相接就成了循环。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;为什么 setState 看起来是异步的&lt;/strong&gt;（坑 1）：setState 只是「登记一个更新任务」，组件函数要等 React 调度到 render 阶段才重新执行。你在 &lt;code&gt;setCount&lt;/code&gt; 之后立刻读 &lt;code&gt;count&lt;/code&gt;，读到的当然是本次渲染的旧值。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;54-hooks-的实现一条链表--调用顺序&#34;&gt;5.4 Hooks 的实现：一条链表 + 调用顺序&lt;/h3&gt;
&lt;p&gt;Hooks 没有魔法。每个 Fiber 节点上挂着一条&lt;strong&gt;链表&lt;/strong&gt;，组件每次调用一个 Hook（useState/useEffect/&amp;hellip;），React 就顺着链表取下一个节点存状态：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    F[&amp;#34;Fiber 节点&amp;lt;br/&amp;gt;memoizedState&amp;#34;] --&amp;gt; H1[&amp;#34;Hook 节点 1&amp;lt;br/&amp;gt;useState count&amp;#34;]
    H1 --&amp;gt; H2[&amp;#34;Hook 节点 2&amp;lt;br/&amp;gt;useState text&amp;#34;]
    H2 --&amp;gt; H3[&amp;#34;Hook 节点 3&amp;lt;br/&amp;gt;useEffect&amp;#34;]
    H3 --&amp;gt; N[&amp;#34;null&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;关键：&lt;strong&gt;状态不是按变量名存的，是按调用顺序存的&lt;/strong&gt;。第 n 次调用 useState，就拿链表上第 n 个节点。这就是「&lt;strong&gt;Hook 不能写在 if/循环里&lt;/strong&gt;」这条铁律的唯一原因——条件调用会让本次渲染的调用顺序和上次错位，状态全部错位：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// ❌ 条件调用：show 为 false 时少调一次，后面所有 Hook 状态错位
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;show&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; [&lt;span style=&#34;color:#a6e22e&#34;&gt;a&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;setA&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;useState&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; [&lt;span style=&#34;color:#a6e22e&#34;&gt;b&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;setB&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;useState&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;用一个 20 行的迷你实现体会一下原理：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 极简版 useState：一个数组 + 一个游标
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;hookStates&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;let&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cursor&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;useState&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;initial&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cursor&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;++&lt;/span&gt;;                    &lt;span style=&#34;color:#75715e&#34;&gt;// 本次调用占第 i 个槽位
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a6e22e&#34;&gt;hookStates&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;hookStates&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;??&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;initial&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;setState&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;hookStates&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;typeof&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;===&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;function&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;hookStates&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt;]) &lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;cursor&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;; &lt;span style=&#34;color:#a6e22e&#34;&gt;render&lt;/span&gt;();                &lt;span style=&#34;color:#75715e&#34;&gt;// 重置游标，重新渲染
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; [&lt;span style=&#34;color:#a6e22e&#34;&gt;hookStates&lt;/span&gt;[&lt;span style=&#34;color:#a6e22e&#34;&gt;i&lt;/span&gt;], &lt;span style=&#34;color:#a6e22e&#34;&gt;setState&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 每次 render 前 cursor 归零，Hook 按调用顺序对号入座
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;真实 React 里这个数组换成 Fiber 上的链表、render 换成调度器触发，思想一模一样。也顺带解释了&lt;strong&gt;坑 5（stale closure）&lt;/strong&gt;：effect 回调捕获的是「它创建那次渲染」的作用域，而 Hook 槽位里的状态早已更新——两者不是一份数据，于是读到旧值。&lt;/p&gt;
&lt;h3 id=&#34;55-批量更新多次-setstate-合并成一次渲染&#34;&gt;5.5 批量更新：多次 setState 合并成一次渲染&lt;/h3&gt;
&lt;p&gt;React 会把同一个事件循环里的多次 setState &lt;strong&gt;合并（batching）成一次渲染&lt;/strong&gt;，避免白白渲染三次。React 18 起连 Promise、setTimeout 里的更新也自动批量。坑 1 的「连续 +1 只加一次」正是批量更新的副产品：三次调用都基于同一个旧值计算，合并后只生效最后一次；函数式更新 &lt;code&gt;setCount(c =&amp;gt; c + 1)&lt;/code&gt; 之所以能正确累加，是因为每次计算都基于「上一个更新任务的结果」而不是旧快照。&lt;/p&gt;
&lt;h3 id=&#34;56-一张图总结从-setstate-到屏幕更新&#34;&gt;5.6 一张图总结：从 setState 到屏幕更新&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    A[&amp;#34;setState 登记更新任务&amp;#34;] --&amp;gt; B[&amp;#34;Scheduler 调度&amp;lt;br/&amp;gt;按优先级插队/合并&amp;#34;]
    B --&amp;gt; C[&amp;#34;render 阶段（可中断）&amp;lt;br/&amp;gt;沿 Fiber 链表逐个执行组件函数&amp;lt;br/&amp;gt;Hooks 按调用顺序取状态&amp;#34;]
    C --&amp;gt; D[&amp;#34;生成 workInProgress 树&amp;lt;br/&amp;gt;Diff 出变更清单&amp;#34;]
    D --&amp;gt; E[&amp;#34;commit 阶段（不可中断）&amp;lt;br/&amp;gt;一次性写入真实 DOM&amp;#34;]
    E --&amp;gt; F[&amp;#34;执行 useEffect&amp;#34;]
    F --&amp;gt;|&amp;#34;effect 里又 setState&amp;#34;| A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;回头看第四节的坑：&lt;strong&gt;坑 1 是批量更新、坑 2 是 render/commit 边界、坑 3 是 element diff、坑 4 是引用比较（Diff 的判断依据）、坑 5 是 Hooks 快照机制、坑 6 是 component diff（类型变=重建）&lt;/strong&gt;。规则会过时，原理不会——这就是先学原理的价值。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六主流-ui-组件库选型对比&#34;&gt;六、主流 UI 组件库：选型对比&lt;/h2&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;th&gt;备注&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Ant Design (antd)&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;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Material UI (MUI)&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Google Material 风格&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;Ant Design Mobile&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;移动端&lt;/td&gt;
					&lt;td&gt;H5/混合 APP&lt;/td&gt;
					&lt;td&gt;与 antd 同门&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Tailwind + Headless UI&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;原子化 CSS&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;Chakra UI&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;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;中后台选 antd 基本不会错。三分钟搭出一个像样的页面：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;Table&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;Button&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;Form&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;Input&lt;/span&gt; } &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;antd&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;UserPage&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;columns&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    { &lt;span style=&#34;color:#a6e22e&#34;&gt;title&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;姓名&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;dataIndex&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    { &lt;span style=&#34;color:#a6e22e&#34;&gt;title&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;邮箱&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;dataIndex&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;email&amp;#34;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;Form&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;layout&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;inline&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onFinish&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{(&lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;) =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;console&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;log&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;v&lt;/span&gt;)}&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;Form.Item&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;name&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;label&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;姓名&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;Input&lt;/span&gt; /&amp;gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;Form.Item&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;Button&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;type&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;primary&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;htmlType&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;submit&amp;#34;&lt;/span&gt;&amp;gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;查询&lt;/span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;Button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;Form&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;Table&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;columns&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;columns&lt;/span&gt;} &lt;span style=&#34;color:#a6e22e&#34;&gt;dataSource&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{[]} &lt;span style=&#34;color:#a6e22e&#34;&gt;rowKey&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;/&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;组件库避坑&lt;/strong&gt;：不要混用两个重型组件库（打包体积爆炸）；版本升级先看迁移指南（antd 4→5 改了样式方案）；表单校验规则写在 &lt;code&gt;rules&lt;/code&gt; 里而不是手写 if-else。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七完整-demo一个-todo-app&#34;&gt;七、完整 Demo：一个 Todo App&lt;/h2&gt;
&lt;p&gt;把前面的知识拼起来——增删改查、状态提升、组件拆分，都在这 60 行里：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-jsx&#34; data-lang=&#34;jsx&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;import&lt;/span&gt; { &lt;span style=&#34;color:#a6e22e&#34;&gt;useState&lt;/span&gt; } &lt;span style=&#34;color:#a6e22e&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;react&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;App&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; [&lt;span style=&#34;color:#a6e22e&#34;&gt;todos&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;setTodos&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;useState&lt;/span&gt;([]);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; [&lt;span style=&#34;color:#a6e22e&#34;&gt;text&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;setText&lt;/span&gt;] &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;useState&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;text&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;trim&lt;/span&gt;()) &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;setTodos&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;) =&amp;gt; [...&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;, { &lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; Date.&lt;span style=&#34;color:#a6e22e&#34;&gt;now&lt;/span&gt;(), &lt;span style=&#34;color:#a6e22e&#34;&gt;text&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&gt; }]);  &lt;span style=&#34;color:#75715e&#34;&gt;// 新数组！
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;setText&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;toggle&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;) =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;setTodos&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;) =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;map&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;) =&amp;gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;===&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt; { ...&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt; } &lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;)));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;remove&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;) =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;setTodos&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;) =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;filter&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;) =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!==&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;style&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{{ &lt;span style=&#34;color:#a6e22e&#34;&gt;maxWidth&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;400&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;margin&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;40px auto&amp;#34;&lt;/span&gt; }}&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;h1&lt;/span&gt;&amp;gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;Todo&lt;/span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;h1&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;input&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;text&lt;/span&gt;} &lt;span style=&#34;color:#a6e22e&#34;&gt;onChange&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{(&lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;) =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;setText&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;e&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;target&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;value&lt;/span&gt;)} /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onClick&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;}&amp;gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;添加&lt;/span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;ul&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {&lt;span style=&#34;color:#a6e22e&#34;&gt;todos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;map&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;item&lt;/span&gt;) =&amp;gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;li&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;key&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;item&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;}&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;span&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#a6e22e&#34;&gt;onClick&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{() =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;toggle&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;item&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;)}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#a6e22e&#34;&gt;style&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{{ &lt;span style=&#34;color:#a6e22e&#34;&gt;textDecoration&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;item&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;?&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;line-through&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;none&amp;#34;&lt;/span&gt; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              {&lt;span style=&#34;color:#a6e22e&#34;&gt;item&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;text&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;span&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onClick&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{() =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;remove&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;item&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;)}&amp;gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;删除&lt;/span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;li&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ))}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;ul&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &amp;lt;&lt;span style=&#34;color:#f92672&#34;&gt;p&lt;/span&gt;&amp;gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;共&lt;/span&gt; {&lt;span style=&#34;color:#a6e22e&#34;&gt;todos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;length&lt;/span&gt;} &lt;span style=&#34;color:#a6e22e&#34;&gt;项&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;，&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;完成&lt;/span&gt; {&lt;span style=&#34;color:#a6e22e&#34;&gt;todos&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;filter&lt;/span&gt;((&lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;) =&amp;gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;x&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;length&lt;/span&gt;} &lt;span style=&#34;color:#a6e22e&#34;&gt;项&lt;/span&gt;&amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;p&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;/&lt;span style=&#34;color:#f92672&#34;&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  );
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;default&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;App&lt;/span&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;注意里面处处是前面坑点的正确示范：&lt;code&gt;setTodos&lt;/code&gt; 全部函数式更新、key 用 &lt;code&gt;id&lt;/code&gt;、改对象用展开运算符造新对象。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八ai-时代怎么少写代码完成-app-开发&#34;&gt;八、AI 时代：怎么少写代码完成 APP 开发&lt;/h2&gt;
&lt;p&gt;写完上面这些，必须承认一件事：&lt;strong&gt;这篇博客里 80% 的代码，AI 现在都能直接生成&lt;/strong&gt;。Todo、表单、表格页这类「模式明确」的代码，人的价值已经不在敲键盘上。我现在的前端开发流程变成了这样：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;人：写清需求&amp;lt;br/&amp;gt;页面结构 / 字段 / 交互 / 接口契约&amp;#34;] --&amp;gt; B[&amp;#34;AI：生成组件代码&amp;lt;br/&amp;gt;一次给完整页面而非碎片&amp;#34;]
    B --&amp;gt; C[&amp;#34;人：Review + 跑起来验证&amp;lt;br/&amp;gt;重点看 state 设计和依赖数组&amp;#34;]
    C --&amp;gt; D[&amp;#34;AI：按反馈迭代&amp;lt;br/&amp;gt;改样式 / 加校验 / 拆组件&amp;#34;]
    D --&amp;gt; E[&amp;#34;人：集成进项目&amp;lt;br/&amp;gt;接真实接口 / 统一风格&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几条实战经验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;描述越具体，代码越能直接用&lt;/strong&gt;。「写个用户管理页」和「用 antd Table 写用户管理页，列：姓名/邮箱/状态，顶部一个内联查询表单，分页 pageSize 20，数据从 GET /api/users 拿」得到的代码质量天差地别——这和给实习生派活是一个道理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;让 AI 一次生成完整页面，而不是一次次要碎片&lt;/strong&gt;。碎片代码的集成成本比生成成本高；整页生成后你再删改，快得多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review 的重点变了&lt;/strong&gt;：语法 AI 很少错，要盯的是架构层面的东西——state 放哪层合不合理、useEffect 依赖有没有坑（本文第四节的坑 AI 也会犯）、组件拆分粒度、key 用没用 index。&lt;strong&gt;基本功越扎实，Review 越快越准&lt;/strong&gt;——AI 淘汰的是只会写不会看的人。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具形态&lt;/strong&gt;：代码补全（Copilot）管「写着顺」；对话式 IDE（Cursor、Claude Code）管「整个功能」；v0.dev 这类管「从截图/描述直接出页面」。三者是不同粒度，按任务大小选。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;APP 整体路径&lt;/strong&gt;：需求描述 → AI 出页面原型 → 选定组件库统一风格 → AI 按接口文档接数据 → 人工处理鉴权、错误处理、边界情况。一个中后台 CRUD 类 APP，这个流程能把工期压到原来的三分之一。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九延伸话题为什么国内流行-vue海外是-react-统治&#34;&gt;九、延伸话题：为什么国内流行 Vue，海外是 React 统治&lt;/h2&gt;
&lt;p&gt;先校准这个说法：不是非黑即白——国内大厂核心产品用 React 的不少（蚂蚁的 Ant Design 就是 React 生态），海外 Laravel 社区也带起了一批 Vue 用户。但总体格局确实存在：&lt;strong&gt;国内中小厂、外包、移动端 H5 里 Vue 占比明显高，海外从创业公司到大厂 React 一家独大&lt;/strong&gt;（npm 全球周下载量 React 常年是 Vue 的数倍）。原因是多层的：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph 历史时机[&amp;#34;历史时机（2014-2016）&amp;#34;]
        H1[&amp;#34;Vue 2014 年发布&amp;lt;br/&amp;gt;作者尤雨溪是华人&amp;lt;br/&amp;gt;中文文档从第一天就是一流的&amp;#34;]
        H2[&amp;#34;恰逢国内移动互联网爆发&amp;lt;br/&amp;gt;海量新人入行&amp;lt;br/&amp;gt;需要低门槛框架&amp;#34;]
        H3[&amp;#34;AngularJS 在国内已流行&amp;lt;br/&amp;gt;Vue 继承模板+双向绑定心智&amp;lt;br/&amp;gt;平滑承接&amp;#34;]
    end
    subgraph 生态锁定[&amp;#34;生态锁定&amp;#34;]
        E1[&amp;#34;饿了么 Element UI（2016）&amp;lt;br/&amp;gt;阿里系采用 Vue&amp;#34;]
        E2[&amp;#34;Element / Ant Design Vue / Vant&amp;lt;br/&amp;gt;中后台组件库成套&amp;#34;]
        E3[&amp;#34;uni-app 用 Vue 语法写小程序&amp;lt;br/&amp;gt;吃透国内小程序红利&amp;#34;]
    end
    subgraph 正循环[&amp;#34;正循环&amp;#34;]
        L1[&amp;#34;Vue 门槛低 → 培训机构主教 Vue&amp;#34;]
        L2[&amp;#34;会 Vue 的人多 → 企业招 Vue 便宜快&amp;#34;]
        L3[&amp;#34;企业用 Vue → 岗位多 → 新人继续学 Vue&amp;#34;]
    end
    subgraph 海外对照[&amp;#34;海外对照&amp;#34;]
        O1[&amp;#34;Facebook 背书 + 自家产品验证&amp;#34;]
        O2[&amp;#34;React Native 吃下跨端&amp;#34;]
        O3[&amp;#34;Next.js 等元框架强化生态&amp;#34;]
        O4[&amp;#34;大厂招聘需求锁定人才供给&amp;#34;]
    end
    H1 &amp;amp; H2 &amp;amp; H3 --&amp;gt; 国内[&amp;#34;国内 Vue 流行&amp;#34;]
    E1 &amp;amp; E2 &amp;amp; E3 --&amp;gt; 国内
    L1 --&amp;gt; L2 --&amp;gt; L3 --&amp;gt; 国内
    O1 &amp;amp; O2 &amp;amp; O3 &amp;amp; O4 --&amp;gt; 海外[&amp;#34;海外 React 统治&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;拆开看，几个关键变量：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;文档与语言的近因效应&lt;/strong&gt;。Vue 的中文文档从发布第一天就是母语级质量，而 React 中文文档长期靠社区翻译、滞后且质量参差。对自学者，这个差异是决定性的——学什么框架，往往就是「哪份文档看得懂」决定的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生态位被谁先占住&lt;/strong&gt;。2016 年饿了么开源 Element UI、阿里系大量采用 Vue，中后台组件库（Element/Ant Design Vue/Vant）成套出现，「Vue + Element」成了国内中后台的标准答案。海外对应位置被 React 的 Ant Design（蚂蚁国际版反哺）、MUI、Next.js 占住。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;培训机构的放大器作用&lt;/strong&gt;。Vue 模板语法接近 HTML、双向绑定写表单快、SFC 单文件组件直观——对从后端/jQuery 转前端的人极其友好，培训周期短、就业反馈快，于是机构主教 Vue，供给端被锁定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;移动端的分野&lt;/strong&gt;。海外跨端 = React Native，进一步巩固 React；国内跨端的真正战场是&lt;strong&gt;小程序&lt;/strong&gt;，而 uni-app 这类「用 Vue 语法写小程序」的框架吃掉了这块红利，又反过来强化了 Vue 心智。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React 的门槛是真实的&lt;/strong&gt;。JSX、函数式思维、不可变数据、Hooks 的心智负担（本文四、五章的坑就是证据），对英语文档和工程化基础都有要求——这在海外是标配，在国内快速扩张期的团队里是成本。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;对学习者的启示&lt;/strong&gt;：两个框架的核心思想（组件化、状态驱动 UI、虚拟 DOM）完全相通，Vue3 的组合式 API 和 React Hooks 已经趋同。学会一个，另一个一周能上手——&lt;strong&gt;别把宝押在框架上，押在原理上&lt;/strong&gt;。而且 AI 时代框架差异进一步被抹平：AI 两个都写得很好，你需要的只是读懂它写的是什么的判断力。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十思考与感悟&#34;&gt;十、思考与感悟&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. React 的本质不是语法，是「UI = f(state)」这个心智模型。&lt;/strong&gt; Hooks 语法三天能学会，但「什么该进 state、state 放哪层、如何从 state 推导 UI」要想清楚需要几个月。判断一个人 React 水平，不要问语法，问他「这个状态你会放在哪个组件」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 坑大多是「引用」和「时机」两类。&lt;/strong&gt; 本文六个坑归个类：坑 1/3/4 是没理解引用比较（key 复用、state 不可变），坑 2/5/6 是没理解渲染时机（effect 何时跑、闭包捕获哪次渲染的值）。把「每次渲染都是一次全新函数调用」这句话想透，大半坑自动消失。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 组件库是杠杆，不是拐杖。&lt;/strong&gt; antd 让中后台效率翻倍，但只会在组件里塞业务逻辑的人，换个库就废了。组件库之上是交互设计，之下是 React 基本功，两头都得懂一点。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. AI 时代，「读代码」比「写代码」更值钱。&lt;/strong&gt; 生成不再稀缺，判断力才稀缺。能一眼看出 AI 生成的代码里 useEffect 依赖少了、state 设计反了的人，会把 AI 用得越来越顺手；只会复制粘贴的人，会被 AI 生成的坑埋掉。学基础不是为了手写，是为了 Review。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十一学习路线建议&#34;&gt;十一、学习路线建议&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;第一周&lt;/strong&gt;：搭好 Vite 环境，跟着官方教程（react.dev 的互动教程质量很高）过完 JSX/组件/state。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二周&lt;/strong&gt;：手写 Todo App，故意踩一遍第四节的坑（比如把 key 换成 index 看看怎么乱），踩过才算学会。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第三周&lt;/strong&gt;：加路由（React Router）和组件库（antd），做一个有列表页+表单页的两页应用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第四周&lt;/strong&gt;：接一个真实后端 API，处理 loading/错误/空数据三种状态——真实项目和 Demo 的差距全在这里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进阶&lt;/strong&gt;：自定义 Hook 抽逻辑、Context/Zustand 管全局状态、性能优化（memo/useMemo）、Next.js 服务端渲染。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;参考资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://react.dev/&#34;&gt;React 官方文档（react.dev）&lt;/a&gt;——新版文档质量极高，教程可交互&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://vitejs.dev/&#34;&gt;Vite 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://ant.design/&#34;&gt;Ant Design 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://reactrouter.com/&#34;&gt;React Router 文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
  </channel>
</rss>
