<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>193577746 (kyriewen)</title>
    <link>http://beta.w2solo.com/193577746</link>
    <description>独立开发者 / 前端工程师</description>
    <language>en-us</language>
    <item>
      <title>我让 AI 改一个 bug——它偷偷动了 5 个我没让它碰的地方</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;让 AI 改一行代码，diff 里却多了几处你没提的改动——用 AI 编程的人都遇到过。我整理了 5 种最常见的"暗改"模式，每种附真实代码和防范方法。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="从一个简单的bug说起"&gt;从一个简单的 bug 说起&lt;/h2&gt;
&lt;p&gt;最近改一个状态更新的 bug，三行代码的事。让 AI 改完，习惯性看了一眼 diff——&lt;/p&gt;

&lt;p&gt;改动不止三行。&lt;/p&gt;

&lt;p&gt;翻了翻最近的 commit 记录，这种事不是第一次了。AI 改代码有个毛病：它不只改你让它改的地方，还会"顺手"动一些它觉得"应该优化"的代码。&lt;/p&gt;

&lt;p&gt;有些改动无伤大雅，但有些能直接让项目炸掉。&lt;/p&gt;

&lt;p&gt;下面是我总结的 5 种最常见的 AI 暗改模式，每种都在社区里反复出现过。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="暗改一：删掉"&gt;暗改一：删掉"没人用"的文件——其实是框架自动加载的&lt;/h2&gt;
&lt;p&gt;这是最危险的一种。&lt;/p&gt;

&lt;p&gt;AI 在重构的时候，会扫描代码引用关系。如果一个文件没有被任何地方显式 import，它就认为是死代码，直接删掉。&lt;/p&gt;

&lt;p&gt;问题是，前端框架有大量"约定式"文件，不需要手动 import。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# AI觉得这些文件"没人引用"，删了
src/
├── middleware.ts          ← Next.js自动加载，不需要import
├── app/error.tsx          ← Next.js错误边界，约定文件名
├── plugins/analytics.ts   ← Nuxt自动加载plugins目录
└── composables/useAuth.ts ← Nuxt自动导入composables目录
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// middleware.ts — 没有任何文件import它，但Next.js自动执行&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI 看到没有任何引用，判定为死代码。删掉之后——所有页面的登录校验没了，未登录用户可以直接访问所有路由。上线之前跑测试大概率还能过，因为单测通常 mock 了 auth 逻辑。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;同类情况还有：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vite 的&lt;code&gt;import.meta.glob&lt;/code&gt;自动注册的路由/组件&lt;/li&gt;
&lt;li&gt;Vue 的全局插件注册（&lt;code&gt;app.use(plugin)&lt;/code&gt;所在文件被删）&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;require.context&lt;/code&gt;动态加载的模块（Webpack 项目常见）&lt;/li&gt;
&lt;li&gt;Tailwind CSS 的&lt;code&gt;content&lt;/code&gt;配置引用的工具 class 文件&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;防范：&lt;/strong&gt; 看到 AI 删文件，先想一下这个文件是不是框架约定的。&lt;code&gt;middleware&lt;/code&gt;、&lt;code&gt;error&lt;/code&gt;、&lt;code&gt;layout&lt;/code&gt;、&lt;code&gt;loading&lt;/code&gt;、&lt;code&gt;plugins/&lt;/code&gt;目录、&lt;code&gt;composables/&lt;/code&gt;目录——这些名字自带魔法，删了就炸。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="暗改二：把同步改成异步——"&gt;暗改二：把同步改成异步——"优雅"地炸了启动流程&lt;/h2&gt;
&lt;p&gt;AI 特别喜欢把同步代码改成异步的。在它的训练数据里，"异步=好"几乎是共识。&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 改之前：同步读取配置（启动时必须阻塞等结果）&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./config.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;initDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;initCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;startServer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI 觉得&lt;code&gt;readFileSync&lt;/code&gt;不优雅，改成了：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AI"优化"后：异步读取&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;promises&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./config.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;initDatabase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;initCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;startServer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看起来没问题？问题在于这段代码跑在一个没有顶层 await 支持的 CommonJS 模块里。或者更隐蔽的场景：&lt;code&gt;initDatabase&lt;/code&gt;内部依赖同步的初始化时序，异步化之后其他模块在 config 还没读完时就开始初始化了。&lt;/p&gt;

&lt;p&gt;这种 bug 极其难排查——启动流程偶尔成功偶尔失败，取决于异步操作的完成顺序。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;同类情况：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;localStorage.getItem&lt;/code&gt; 被改成异步的 IndexedDB 调用&lt;/li&gt;
&lt;li&gt;构造函数里的同步初始化被改成 async（构造函数不支持 async）&lt;/li&gt;
&lt;li&gt;同步的校验逻辑被改成异步，导致校验还没完成就放行了&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;防范：&lt;/strong&gt; AI 把同步改异步时，问自己一个问题："这里用同步是不是有原因的？"启动流程、构造函数、校验逻辑——这三个地方的同步代码，通常都是故意的。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="暗改三：重构时弄丢了一个break"&gt;暗改三：重构时弄丢了一个 break&lt;/h2&gt;
&lt;p&gt;AI 在做代码搬移或重构的时候，偶尔会丢失控制流逻辑。最常见的是 switch 里的 break 和循环里的提前返回。&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 改之前&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;processItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;logError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 遇到无效项就停止处理&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让 AI 重构这段代码，比如拆成更小的函数：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AI重构后&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;processItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;handleItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;handleItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 这个return只退出handleItem，不退出循环！&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;break&lt;/code&gt;变成了&lt;code&gt;return&lt;/code&gt;，但语义完全不同。原来遇到无效项整个循环停止，现在只是跳过这一项，继续处理后面的。如果无效项后面的数据依赖前面的校验结果，后续处理全是脏数据。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;同类情况：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;continue&lt;/code&gt;在提取函数后变成&lt;code&gt;return&lt;/code&gt;，语义改变&lt;/li&gt;
&lt;li&gt;嵌套的&lt;code&gt;break&lt;/code&gt;（带 label 的）被 AI 忽略，直接删掉 label&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;throw&lt;/code&gt;在 try-catch 重构后被 catch 吃掉，异常不再向上传播&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;防范：&lt;/strong&gt; 重构后的第一件事——diff 里搜&lt;code&gt;break&lt;/code&gt;、&lt;code&gt;continue&lt;/code&gt;、&lt;code&gt;return&lt;/code&gt;、&lt;code&gt;throw&lt;/code&gt;。这四个关键词的任何变动都要逐个核对原始逻辑。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="暗改四：给你两行代码套了三层抽象"&gt;暗改四：给你两行代码套了三层抽象&lt;/h2&gt;
&lt;p&gt;你让 AI 写一个判断空字符串的工具函数，期望得到这个：&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI 给你的：&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;StringValidator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ValidationResult&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ValidationResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;EmptyStringValidator&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;StringValidator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;trimBeforeCheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;trimBeforeCheck&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trimBeforeCheck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;trimBeforeCheck&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;ValidationResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trimBeforeCheck&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;String is empty or whitespace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;originalLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;processedLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isEmpty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;EmptyStringValidator&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两行逻辑变成了三十行。接口、类、配置项、元数据——全套。&lt;/p&gt;

&lt;p&gt;这种暗改不会让项目崩溃，但它会让代码库快速膨胀。三个月后，团队里没人敢碰这些"AI 写的专业架构"，因为看起来太正式了，改了怕出问题。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;同类情况：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;一个简单的 fetch 请求被包成了带重试、超时、拦截器的 HTTP Client 类&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; 如果你让 AI 写的功能用一句话就能描述清楚，结果代码超过 20 行——大概率过度设计了。删掉，让它重写，prompt 里加一句"用最少的代码实现"。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="暗改五：把你手动改好的代码"&gt;暗改五：把你手动改好的代码"优化"回去&lt;/h2&gt;
&lt;p&gt;这是最让人崩溃的一种。&lt;/p&gt;

&lt;p&gt;你在 AI 生成的代码里发现了一个问题，手动改好了。过了一会儿让 AI 继续改另一个地方，它把你的修复给"优化"没了——因为它的上下文里记住了自己之前生成的版本，认为那才是"正确的"。&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AI第一次生成的&lt;/span&gt;
&lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

&lt;span class="c1"&gt;// 你手动加了依赖项&lt;/span&gt;
&lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// ← 你手动加的&lt;/span&gt;

&lt;span class="c1"&gt;// 让AI改另一个bug，它"顺手"改回来了&lt;/span&gt;
&lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt; &lt;span class="c1"&gt;// ← AI改回空数组了，因为它觉得空数组"是对的"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AI 不理解你为什么改了它的代码。在它看来，空依赖数组是"标准写法"，你加的&lt;code&gt;[userId]&lt;/code&gt;是多余的。&lt;/p&gt;

&lt;p&gt;这种情况在长对话中尤其频繁。对话越长，AI 越倾向于用自己早期生成的版本覆盖你的手动修改。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;防范：&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;手动改完 AI 的代码后，&lt;strong&gt;开一个新对话&lt;/strong&gt;再继续后面的任务&lt;/li&gt;
&lt;li&gt;或者在 prompt 里明确说"不要修改&lt;code&gt;useEffect&lt;/code&gt;的依赖数组，那是我故意改的"&lt;/li&gt;
&lt;li&gt;养成习惯：&lt;strong&gt;每次 AI 改完代码，完整看一遍 diff&lt;/strong&gt;，不只看它改的地方，看所有改动&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;
&lt;h2 id="AI暗改防范速查表"&gt;AI 暗改防范速查表&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;&lt;strong&gt;删"死代码"&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🔴致命&lt;/td&gt;
&lt;td&gt;diff 里搜删除的 class/文件&lt;/td&gt;
&lt;td&gt;有框架注解的 class 不是死代码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;同步改异步&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🔴致命&lt;/td&gt;
&lt;td&gt;diff 里搜 async/await 新增&lt;/td&gt;
&lt;td&gt;启动/构造/校验的同步是故意的&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;丢失 break/return&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🟡高危&lt;/td&gt;
&lt;td&gt;diff 里搜 break/continue/throw&lt;/td&gt;
&lt;td&gt;重构后逐个核对控制流&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;过度抽象&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🟢低危&lt;/td&gt;
&lt;td&gt;一句话需求超 20 行代码&lt;/td&gt;
&lt;td&gt;prompt 加"最少代码实现"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;覆盖手动修改&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🔴致命&lt;/td&gt;
&lt;td&gt;看完整 diff，不只看改动点&lt;/td&gt;
&lt;td&gt;改完手动代码开新对话&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;一条通用原则：AI 每次改完代码，看完整 diff，不只看你让它改的那行。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="这不是AI的错"&gt;这不是 AI 的错&lt;/h2&gt;
&lt;p&gt;说到底，这些"暗改"不是 AI 故意搞破坏。它在做它认为正确的事——清理死代码、优化性能、统一风格。问题是它没有你项目的完整上下文，不知道哪些"不规范"的代码是故意写成那样的。&lt;/p&gt;

&lt;p&gt;AI 是一个能力很强但完全不懂你项目历史的新同事。你不会让一个刚入职的人直接 push 到 main，对 AI 也一样。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;你遇到过 AI 最离谱的"暗改"是什么？评论区聊聊。&lt;/strong&gt;&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Wed, 22 Jul 2026 19:19:04 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7839</link>
      <guid>http://beta.w2solo.com/topics/7839</guid>
    </item>
    <item>
      <title>我扒了最近的前端面经——面试不背八股文了，考这 5 样</title>
      <description>&lt;p&gt;最近帮朋友看面经准备跳槽，翻了掘金和牛客上一堆帖子，发现现在的面经和两年前完全不是一个物种了。&lt;/p&gt;

&lt;p&gt;以前："手写一个快排"、"说一下事件循环"。&lt;/p&gt;

&lt;p&gt;现在："给你 30 分钟，用 AI 工具现场写一个 XX 功能"、"这段 AI 生成的代码有什么问题？"&lt;/p&gt;

&lt;p&gt;从"你会不会写"变成了"你能不能判断 AI 写得对不对"。下面说说现在考什么。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="1. 现场用AI工具解题"&gt;1. 现场用 AI 工具解题&lt;/h2&gt;
&lt;p&gt;Meta 把前端编码面试改成了"AI-Enabled Coding"——60 分钟，面试环境里内置 AI 工具，你不但可以用，而且他们观察你&lt;strong&gt;怎么用&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;Shopify 更激进：面试流程里有两轮专门的 AI 辅助编码环节，AI 使用不是"允许"，是"预期你会用"。&lt;/p&gt;

&lt;p&gt;面试官真正在看的：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompt 质量&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;需求拆解清晰、约束写全&lt;/td&gt;
&lt;td&gt;一句话甩给 AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;迭代策略&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;拿到结果后追问优化&lt;/td&gt;
&lt;td&gt;接受第一次输出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;错误判断&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;快速发现问题并修正&lt;/td&gt;
&lt;td&gt;等 AI 自己修&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;边界意识&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;主动考虑边界 case&lt;/td&gt;
&lt;td&gt;只跑 happy path&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;备考建议：&lt;/strong&gt; 每天用 AI 工具写至少一个小功能。重点不是写什么，是练"怎么给 AI 提需求"的手感。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="2. AI代码审查能力"&gt;2. AI 代码审查能力&lt;/h2&gt;
&lt;p&gt;这是增长最快的题型。面试官给你一段 AI 生成的代码，让你找问题：&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;SearchResults&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setResults&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/search?q=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;setResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Spinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;你能找出几个问题？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这段代码跑起来完全没问题。但至少有 5 个 bug：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;竞态条件&lt;/strong&gt;：query 快速变化时，旧请求覆盖新结果&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;没有错误处理&lt;/strong&gt;：fetch 失败了 loading 永远转圈&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XSS 风险&lt;/strong&gt;：query 直接拼接 URL，没有 encodeURIComponent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;内存泄漏&lt;/strong&gt;：组件卸载后 setState 仍然执行&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;无防抖&lt;/strong&gt;：每次 query 变化都发请求&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AI 写的代码&lt;strong&gt;看起来总是对的&lt;/strong&gt;——语法正确、逻辑通顺。但带着这些 bug 上生产，用户一多就炸。面试官想知道的是：你把 AI 当"打字机"还是当"初级实习生"？&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="3. 需求拆解能力"&gt;3. 需求拆解能力&lt;/h2&gt;
&lt;p&gt;面试官给你一个模糊需求，让你&lt;strong&gt;拆成可以交给 AI 执行的子任务&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;比如"首页加载太慢，优化一下"——以前背方案就行，现在要你现场拆：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;第一步：让AI跑Lighthouse，拿到LCP/FCP/CLS数据
第二步：让AI分析bundle size，找最大的3个chunk
第三步：让AI把首屏以下组件改成lazy import
第四步：再跑一次Lighthouse，对比数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同一个问题，拆得好的人用 AI 能在 30 分钟内搞定，拆得差的人对话 2 小时出不来结果。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;核心差距不在 AI 工具，在人脑里的"任务分解树"。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="4. 系统设计中的AI决策"&gt;4. 系统设计中的 AI 决策&lt;/h2&gt;
&lt;p&gt;以前的系统设计题："设计一个在线文档协作系统。"&lt;/p&gt;

&lt;p&gt;现在加了一层："哪些模块用 AI 生成，哪些必须手写？为什么？"&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;适合交给 AI 的&lt;/th&gt;
&lt;th&gt;必须人工把控的&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRUD 接口、数据校验&lt;/td&gt;
&lt;td&gt;协同冲突解决算法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI 组件搭建&lt;/td&gt;
&lt;td&gt;权限系统设计&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单元测试生成&lt;/td&gt;
&lt;td&gt;数据一致性逻辑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;常规性能优化&lt;/td&gt;
&lt;td&gt;安全防护策略&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;面试官想看到的是&lt;strong&gt;判断力&lt;/strong&gt;——你知道 AI 什么时候靠谱、什么时候不靠谱。&lt;/p&gt;

&lt;p&gt;一个加分的回答："协同编辑的冲突解决我不会交给 AI，因为这类算法依赖严格的数学证明，AI 生成的实现很可能在边界 case 上出错。但数据校验和 CRUD 我会让 AI 生成，因为这些模式成熟、验证简单。"&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="5. 工程判断力：知道AI的边界"&gt;5. 工程判断力：知道 AI 的边界&lt;/h2&gt;
&lt;p&gt;最难准备的一项。面试官会问："你用 AI 写代码遇到过最坑的一次是什么？"&lt;/p&gt;

&lt;p&gt;面试官听几句就能判断你是真用过还是只玩过 demo：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;真用过的人说的话&lt;/th&gt;
&lt;th&gt;只玩过 demo 的人说的话&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"AI 写的 CSS 在 Safari 经常出兼容问题"&lt;/td&gt;
&lt;td&gt;"AI 写代码特别快"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"长对话后 AI 会忘记前面的约束"&lt;/td&gt;
&lt;td&gt;"AI 什么都能写"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"让 AI 改 bug 有时候越改越多"&lt;/td&gt;
&lt;td&gt;"AI 代替了很多手工活"&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;真实的、具体的、带坑的经验——这是最有价值的东西，也是没法临时准备的。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="速查表：前端面试准备清单"&gt;速查表：前端面试准备清单&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;&lt;strong&gt;AI 工具熟练度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;每天用 AI 写一个小功能，练 Prompt 手感&lt;/td&gt;
&lt;td&gt;最高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI 代码审查&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;让 AI 生成代码后不看直接跑，记录出了什么问题&lt;/td&gt;
&lt;td&gt;最高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;需求拆解&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;拿到需求先拆成 5 个子任务再动手&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;系统设计&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;画架构图时标注"AI 生成"和"手写"模块&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;踩坑经验&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;积累 AI 写代码的真实踩坑 case&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;基础原理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;八股文不用背，但原理得懂&lt;/td&gt;
&lt;td&gt;中&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;hr&gt;

&lt;p&gt;现在的前端面试，不是在考"你会不会写代码"，是在考"你会不会用 AI 写代码"。&lt;/p&gt;

&lt;p&gt;这两件事的差距，比大部分人想象的要大得多。&lt;/p&gt;

&lt;p&gt;你面试时被问过 AI 相关的问题吗？最难的是哪道？留言聊聊。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;strong&gt;觉得有用？点个「赞」👍 让更多人看到&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;关注我，获取更多技术干货。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Tue, 21 Jul 2026 19:56:11 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7824</link>
      <guid>http://beta.w2solo.com/topics/7824</guid>
    </item>
    <item>
      <title>别再写 useMemo 了——2026 年这 5 个 React 性能优化已经是反模式</title>
      <description>&lt;p&gt;打开任何一个 AI 辅助开发的 React 项目，你大概率会看到同样的画面：useMemo、useCallback、React.memo 铺天盖地，仿佛不写就会崩。&lt;/p&gt;

&lt;p&gt;但 2026 年了，React Compiler 已经正式落地。&lt;strong&gt;你手动写的那些"优化"，很多不但没用，还在拖慢你的代码。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="先说结论：为什么要改"&gt;先说结论：为什么要改&lt;/h2&gt;
&lt;p&gt;React Compiler（随 React 19 正式推出）做了一件事：&lt;strong&gt;在编译阶段自动分析组件，在表达式级别做细粒度 memoization。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://files.mdnice.com/user/11765/9d5488bd-3684-4e75-a255-f0fae4351905.jpg" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;这意味着：你手写的 useMemo、useCallback、React.memo——编译器都能自动做，而且做得更细、更准确。&lt;/p&gt;

&lt;p&gt;手动写的问题在于：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;维度&lt;/th&gt;
&lt;th&gt;手动优化&lt;/th&gt;
&lt;th&gt;React Compiler&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;粒度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;组件/Hook 级别&lt;/td&gt;
&lt;td&gt;表达式级别（更细）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;准确性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;依赖数组经常写错&lt;/td&gt;
&lt;td&gt;编译器分析，不会遗漏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;代码量&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;多 30-50% 包装代码&lt;/td&gt;
&lt;td&gt;零额外代码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;维护成本&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;依赖变了要手动更新&lt;/td&gt;
&lt;td&gt;自动追踪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;出错概率&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;依赖数组写错=缓存永远不更新&lt;/td&gt;
&lt;td&gt;不存在这个问题&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;ESLint React v5.3 已经加了 &lt;code&gt;no-unnecessary-use-memo&lt;/code&gt; 和 &lt;code&gt;no-unnecessary-use-callback&lt;/code&gt; 规则——&lt;strong&gt;官方工具链都在告诉你别写了。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;下面逐个看 5 个已经变成反模式的"优化"。&lt;/p&gt;
&lt;h2 id="反模式1：useMemo包裹所有计算"&gt;反模式 1：useMemo 包裹所有计算&lt;/h2&gt;
&lt;p&gt;最常见的 AI 生成代码模式：&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;UserList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filteredUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sortedUsers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;filteredUsers&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;filteredUsers&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sortedUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sortedUsers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;sortedUsers&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;三个 useMemo 做了一件事：过滤 + 排序 + 统计。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;React Compiler 会自动分析这段代码的依赖关系，在&lt;code&gt;users&lt;/code&gt;或&lt;code&gt;filter&lt;/code&gt;没变的时候跳过所有计算。你手写三个 useMemo，其实在做编译器已经做了的事，同时还增加了：&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;2026 年的写法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;UserList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;干净、直接、编译器自动优化。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;唯一例外：&lt;/strong&gt; 如果你的计算确实很重（比如对 10 万条数据做复杂聚合），并且你能用&lt;code&gt;React DevTools Profiler&lt;/code&gt;证明它是瓶颈——那保留 useMemo。但 99% 的场景不是这样。&lt;/p&gt;
&lt;h2 id="反模式2：useCallback包裹所有传给子组件的函数"&gt;反模式 2：useCallback 包裹所有传给子组件的函数&lt;/h2&gt;&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleReset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleExport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;exportData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleClick&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleReset&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExportButton&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleExport&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码有三个问题：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;useCallback 本身不阻止子组件重渲染&lt;/strong&gt;——除非子组件用了&lt;code&gt;React.memo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;没有 React.memo 的情况下，useCallback 做的事就是"稳定引用"——但 React Compiler 已经能自动判断哪些组件需要跳过&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;handleExport&lt;/code&gt;的依赖数组里有&lt;code&gt;count&lt;/code&gt;，每次 count 变化它都会重建——useCallback 完全没起作用&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;更直接的问题是：AI 工具特别爱生成 useCallback。&lt;/strong&gt; 因为训练数据里大量"React 性能优化最佳实践"文章都在教"传给子组件的函数一定要 useCallback"。这个建议在 2023 年可能有道理，在 2026 年就是噪音。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2026 年的写法：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExportButton&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;exportData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="反模式3：React.memo包裹每个导出组件"&gt;反模式 3：React.memo 包裹每个导出组件&lt;/h2&gt;&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;UserCard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;UserCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onSelect&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;onSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Avatar&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;React.memo 的逻辑是：props 没变就跳过渲染。问题是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;React Compiler 已经在做同样的事&lt;/strong&gt;，而且粒度更细——它可以跳过组件内部的部分表达式，不需要跳过整个组件&lt;/li&gt;
&lt;li&gt;React.memo*&lt;em&gt;每次渲染都要做 props 浅比较&lt;/em&gt;*——如果 props 经常变，这个比较本身就是白白浪费&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;和 useCallback 配套才有意义&lt;/strong&gt;——但如果你已经不写 useCallback 了，React.memo 也没必要了&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;一条很好判断的规则：&lt;/strong&gt; 如果你的组件渲染成本低于 props 浅比较成本（大部分简单组件都是这样），React.memo 就是负优化。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2026 年的写法：&lt;/strong&gt; 直接导出，不包裹。&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;UserCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onSelect&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;onSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Avatar&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;avatar&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="反模式4：useEffect + useState做数据获取"&gt;反模式 4：useEffect + useState 做数据获取&lt;/h2&gt;
&lt;p&gt;这可能是最根深蒂固的反模式，也是 AI 生成代码最爱写的模式：&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cancelled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Spinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorMessage&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;25 行代码做一件事：获取数据。&lt;/strong&gt; 而且还有这些隐患：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;问题&lt;/th&gt;
&lt;th&gt;后果&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;竞态条件&lt;/td&gt;
&lt;td&gt;userId 快速变化时，旧请求覆盖新数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;瀑布请求&lt;/td&gt;
&lt;td&gt;父组件获取完→子组件才开始获取→孙组件再等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无缓存&lt;/td&gt;
&lt;td&gt;同一个 userId 每次挂载都重新请求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;首次渲染闪白屏&lt;/td&gt;
&lt;td&gt;loading 状态永远要经历 true→false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;服务端渲染不友好&lt;/td&gt;
&lt;td&gt;useEffect 在 SSR 阶段不执行&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;2026 年的写法（用 TanStack Query）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isPending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Spinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ErrorMessage&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者用 React 19 的&lt;code&gt;use&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userPromise&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userPromise&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;从 25 行到 3 行。&lt;/strong&gt; 自动处理竞态、缓存、去重、后台刷新。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;这不是风格偏好，是架构级的差距。&lt;/strong&gt; useEffect 做数据获取在 React 官方文档里已经被标注为不推荐。&lt;/p&gt;
&lt;h2 id="反模式5：过度拆分组件"&gt;反模式 5：过度拆分组件"优化性能"&lt;/h2&gt;
&lt;p&gt;我见过的一个极端案例：一个表单页面被拆成了 47 个组件——每个输入框一个组件、每个标签一个组件、每个校验提示一个组件。理由是"避免整个表单重渲染"。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;这是对 React 渲染机制的误解。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;组件拆分不等于性能优化。每多一个组件，React 就多了：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;一次 reconciliation 比较&lt;/li&gt;
&lt;li&gt;一个 Fiber 节点&lt;/li&gt;
&lt;li&gt;一次 props 序列化和比较&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;合理的拆分标准应该是：&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;组件超过 200 行&lt;/td&gt;
&lt;td&gt;✅ 可维护性&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要独立复用&lt;/td&gt;
&lt;td&gt;✅ 复用性&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;有独立的状态逻辑&lt;/td&gt;
&lt;td&gt;✅ 关注点分离&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"这一块可能会频繁更新"&lt;/td&gt;
&lt;td&gt;❌ 过早优化&lt;/td&gt;
&lt;td&gt;✅ 让 Compiler 处理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"拆小一点性能好"&lt;/td&gt;
&lt;td&gt;❌ 错误假设&lt;/td&gt;
&lt;td&gt;✅ 用 Profiler 验证&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;真正该做的性能优化是状态下沉（state co-location）：&lt;/strong&gt; 把 state 放到真正需要它的组件里，而不是提升到父组件然后靠拆分来避免重渲染。&lt;/p&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ 状态提升 + 过度拆分&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NameInput&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setName&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EmailInput&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ 状态下沉，各管各的&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;NameInput&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EmailInput&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;NameInput&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="速查表：2026年React性能优化决策"&gt;速查表：2026 年 React 性能优化决策&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;以前的"最佳实践"&lt;/th&gt;
&lt;th&gt;2026 年的做法&lt;/th&gt;
&lt;th&gt;什么时候还需要旧写法&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useMemo 包裹计算&lt;/td&gt;
&lt;td&gt;直接写，Compiler 处理&lt;/td&gt;
&lt;td&gt;10 万 + 数据量的复杂聚合，且 Profiler 证实是瓶颈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useCallback 包裹函数&lt;/td&gt;
&lt;td&gt;直接内联&lt;/td&gt;
&lt;td&gt;传给第三方库且该库依赖引用稳定性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;React.memo 包裹组件&lt;/td&gt;
&lt;td&gt;直接导出&lt;/td&gt;
&lt;td&gt;极重的渲染组件（Canvas/3D），且 Profiler 证实&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;useEffect 获取数据&lt;/td&gt;
&lt;td&gt;TanStack Query / use() / SWR&lt;/td&gt;
&lt;td&gt;永远不需要（没有例外）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;过度拆分组件&lt;/td&gt;
&lt;td&gt;按逻辑拆分 + 状态下沉&lt;/td&gt;
&lt;td&gt;永远不需要（没有例外）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;手动 shouldComponentUpdate&lt;/td&gt;
&lt;td&gt;删掉&lt;/td&gt;
&lt;td&gt;2026 年了不应该还有 Class 组件&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;判断标准就一条：先用 React DevTools Profiler 跑一遍。没有证据证明是瓶颈的，就别优化。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="升级React Compiler的最小步骤"&gt;升级 React Compiler 的最小步骤&lt;/h2&gt;
&lt;p&gt;如果你的项目还没启用 React Compiler：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;react-compiler-runtime
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; babel-plugin-react-compiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Babel 配置加一行：&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"babel-plugin-react-compiler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vite 用户：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;reactCompiler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-compiler-runtime/vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;reactCompiler&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启用后，可以逐步删掉不必要的 useMemo/useCallback/React.memo。不用一次性全删——编译器和手动优化可以共存，只是手动的部分变成了冗余代码。&lt;/p&gt;
&lt;h2 id="别让AI替你做决定"&gt;别让 AI 替你做决定&lt;/h2&gt;
&lt;p&gt;这篇文章本质上在说一件事：&lt;strong&gt;不要因为 AI 生成了 useMemo，你就不敢删它。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI 代码生成器的训练数据里充满了 2020-2024 年的"最佳实践"文章。那些文章在当时是对的——React 没有编译器，手动 memoization 是唯一选择。但 2026 年了，工具变了，实践也要变。&lt;/p&gt;

&lt;p&gt;用 AI 写代码没问题。但 Review 的时候，你需要知道什么该留、什么该删。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;你的项目升级 React Compiler 了吗？升级后删了多少行 useMemo？评论区聊聊。&lt;/strong&gt;&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Mon, 20 Jul 2026 14:59:12 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7809</link>
      <guid>http://beta.w2solo.com/topics/7809</guid>
    </item>
    <item>
      <title>我看完这篇安全论文——AI 推荐的 npm 包，92% 是编出来的</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;你有没有想过一个问题：AI 让你 &lt;code&gt;npm install&lt;/code&gt; 的那个包，真的存在吗？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;7 月 8 日，以色列特拉维夫大学、Technion（以色列理工学院）和 Intuit 的研究人员联合发表了一篇论文，提出了一种叫 &lt;strong&gt;HalluSquatting&lt;/strong&gt; 的新型攻击方式。&lt;/p&gt;

&lt;p&gt;核心发现让我出了一身冷汗：&lt;strong&gt;AI 编程助手推荐的 GitHub 仓库名称，平均幻觉率高达 92.4%。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;不是拼错了包名，不是搞混了版本号——是整个仓库根本不存在，AI 凭空捏造了一个名字。&lt;/p&gt;

&lt;p&gt;而攻击者已经在利用这一点了。&lt;/p&gt;
&lt;h2 id="什么是 HalluSquatting"&gt;什么是 HalluSquatting&lt;/h2&gt;
&lt;p&gt;论文全名叫 &lt;em&gt;"Beware of Agentic Botnets: Scalable Untargeted Promptware Attacks via Universal and Transferable Adversarial HalluSquatting"&lt;/em&gt;，名字很长，核心思路三句话讲清：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AI 编程助手会编造不存在的包名和仓库名&lt;/strong&gt;（这就是大模型的"幻觉"）&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;同一个问题反复问，AI 编出来的假名字惊人地一致&lt;/strong&gt;——58% 的概率编出同一个名字&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;攻击者提前注册这些假名字，埋入恶意代码和 Prompt 注入&lt;/strong&gt;——坐等开发者通过 AI 上钩&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;你可能听过 &lt;em&gt;typosquatting&lt;/em&gt;（域名/包名抢注），攻击者注册和知名包相似的名字，比如 &lt;code&gt;loadash&lt;/code&gt; 冒充 &lt;code&gt;lodash&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;HalluSquatting 比这狠得多：&lt;strong&gt;它不需要猜你会打错什么字，它直接预测 AI 会编什么名字。而且预测准确率极高。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="攻击怎么运作的"&gt;攻击怎么运作的&lt;/h2&gt;
&lt;p&gt;整个攻击链分五步：&lt;/p&gt;
&lt;h3 id="第一步：挖掘"&gt;第一步：挖掘"幻觉包名"&lt;/h3&gt;
&lt;p&gt;攻击者选一个热门开源项目，反复问不同的 AI 助手："帮我克隆这个项目"或"帮我安装这个工具"。&lt;/p&gt;

&lt;p&gt;AI 会编造一个不存在的仓库名或包名。关键发现：&lt;strong&gt;不同用户问同一个问题，AI 编出来的假名字有 58% 的概率是相同的。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这意味着攻击者可以大规模、自动化地预测 AI 会推荐哪些假名字。&lt;/p&gt;
&lt;h3 id="第二步：抢注假名字"&gt;第二步：抢注假名字&lt;/h3&gt;
&lt;p&gt;攻击者拿到这些"AI 最爱编的名字"，去 GitHub、npm、PyPI 上注册同名项目，放入看起来正常但暗藏恶意逻辑的代码。&lt;/p&gt;
&lt;h3 id="第三步：植入 Prompt 注入"&gt;第三步：植入 Prompt 注入&lt;/h3&gt;
&lt;p&gt;这是 HalluSquatting 和普通供应链攻击的关键区别。攻击者不只放恶意代码，还会在 README 或代码注释里嵌入 &lt;strong&gt;Prompt Injection&lt;/strong&gt;——专门劫持 AI Agent 的指令。&lt;/p&gt;

&lt;p&gt;一旦 AI 读取了这些内容，它会被"洗脑"，开始执行攻击者预设的操作。&lt;/p&gt;
&lt;h3 id="第四步：等AI上钩"&gt;第四步：等 AI 上钩&lt;/h3&gt;
&lt;p&gt;当一个真实的开发者问 AI："帮我安装这个工具"，AI 编出同一个假名字，自动去 npm/GitHub 拉取——拿到的就是攻击者的恶意版本。&lt;/p&gt;
&lt;h3 id="第五步：感染扩散"&gt;第五步：感染扩散&lt;/h3&gt;
&lt;p&gt;被劫持的 AI Agent 开始执行攻击者的指令：&lt;strong&gt;开反向 shell、偷密码和 Token、挖矿&lt;/strong&gt;，甚至感染其他 AI Agent，形成僵尸网络（Botnet）。&lt;/p&gt;

&lt;p&gt;论文标题里的 "Agentic Botnets" 就是这个意思——由 AI Agent 组成的僵尸网络。&lt;/p&gt;
&lt;h2 id="有多严重？论文里的数字"&gt;有多严重？论文里的数字&lt;/h2&gt;
&lt;p&gt;研究人员对多款主流 AI 编程工具做了大规模测试：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;幻觉率&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025 年新发布的 GitHub 仓库&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;92.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2019 年之前的老牌 GitHub 仓库&lt;/td&gt;
&lt;td&gt;0.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;仓库克隆场景（整体）&lt;/td&gt;
&lt;td&gt;最高 &lt;strong&gt;85%&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skill/插件安装场景&lt;/td&gt;
&lt;td&gt;最高 &lt;strong&gt;100%&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;一个反直觉的发现：&lt;strong&gt;越新的项目，AI 越容易编造假名字。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;老项目（2019 年前）因为训练数据充分，幻觉率只有 0.9%。但 2025 年新发布的项目，AI 几乎在瞎编——因为这些项目不在训练数据里，模型只能"猜"。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;这意味着：当你让 AI 帮你克隆一个最近刚火起来的新项目时，它给你的仓库地址大概率是假的。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="哪些工具中招了"&gt;哪些工具中招了&lt;/h2&gt;
&lt;p&gt;论文测试了 9 款 AI 编程工具，攻击成功率差距巨大：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;20-35%&lt;/td&gt;
&lt;td&gt;⚠️ 中等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;20-35%&lt;/td&gt;
&lt;td&gt;⚠️ 中等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini CLI&lt;/td&gt;
&lt;td&gt;20-35%&lt;/td&gt;
&lt;td&gt;⚠️ 中等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windsurf&lt;/td&gt;
&lt;td&gt;20-35%&lt;/td&gt;
&lt;td&gt;⚠️ 中等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cline&lt;/td&gt;
&lt;td&gt;20-35%&lt;/td&gt;
&lt;td&gt;⚠️ 中等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenClaw&lt;/td&gt;
&lt;td&gt;80-100%&lt;/td&gt;
&lt;td&gt;🔴 极高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZeroClaw&lt;/td&gt;
&lt;td&gt;80-100%&lt;/td&gt;
&lt;td&gt;🔴 极高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NanoClaw&lt;/td&gt;
&lt;td&gt;80-100%&lt;/td&gt;
&lt;td&gt;🔴 极高&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;主流商业工具（Cursor、Copilot、Gemini CLI）的攻击成功率在 20-35%——看起来不算特别高？换个角度：&lt;strong&gt;你每让 AI 安装 5 个包，就有 1 到 2 个可能是假的。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;而基于开源模型的工具（OpenClaw 系列），成功率直接飙到 80-100%。&lt;/p&gt;
&lt;h2 id="真实案例：react-codeshift"&gt;真实案例：react-codeshift&lt;/h2&gt;
&lt;p&gt;这不是纯理论攻击。&lt;/p&gt;

&lt;p&gt;2026 年 1 月，安全公司 Aikido Security 的研究员 Charlie Eriksen 发现了一个真实案例：AI 编程助手经常向开发者推荐一个叫 &lt;code&gt;react-codeshift&lt;/code&gt; 的 npm 包。&lt;/p&gt;

&lt;p&gt;问题是——&lt;strong&gt;这个包在 npm 上根本不存在。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;它是 AI 编出来的。但 Eriksen 发现，&lt;strong&gt;已经有 237 个 GitHub 仓库的代码里包含了安装这个假包的指令&lt;/strong&gt;，全都是 AI 生成的代码里写的。&lt;/p&gt;

&lt;p&gt;如果有人抢先在 npm 上注册了 &lt;code&gt;react-codeshift&lt;/code&gt; 并放入恶意代码，这 237 个仓库的所有用户都会中招——而且他们根本不知道自己装了个假包。&lt;/p&gt;

&lt;p&gt;这不是孤例。2025 年 8 月的 &lt;strong&gt;Nx 供应链攻击&lt;/strong&gt;中，攻击者利用 AI 编程工具扫描开发者的加密货币钱包和 Token。2026 年 3 月的 &lt;strong&gt;Axios npm 攻击&lt;/strong&gt;中，维护者 Token 被盗，恶意版本被推送到 npm 官方源。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI 编程工具正在成为供应链攻击的新入口。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="防护速查表"&gt;防护速查表&lt;/h2&gt;
&lt;p&gt;收藏这张表，每次 AI 推荐你安装新包时对照检查：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;&lt;strong&gt;验包真伪&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;运行 &lt;code&gt;npm view &amp;lt;包名&amp;gt;&lt;/code&gt; 确认包存在且有下载量&lt;/td&gt;
&lt;td&gt;每次 AI 推荐新包时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;查仓库来源&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;去 GitHub 手动搜索仓库名，确认 Star/Fork/活跃度&lt;/td&gt;
&lt;td&gt;每次 AI 给你仓库链接时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;审查安装脚本&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm diff &amp;lt;包名&amp;gt;&lt;/code&gt; 查看包内容，特别看 &lt;code&gt;postinstall&lt;/code&gt; 脚本&lt;/td&gt;
&lt;td&gt;每次新增依赖时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;锁定依赖版本&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;提交 &lt;code&gt;package-lock.json&lt;/code&gt;，开启 &lt;code&gt;npm audit&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;项目初始化时&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;用私有仓库&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;配置公司级 npm 私有源，只允许白名单内的包&lt;/td&gt;
&lt;td&gt;团队项目必须&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;关闭自动执行&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI 推荐安装命令时，先审查再手动执行，不要让 Agent 自动 &lt;code&gt;npm install&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;所有场景&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;持续扫描&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;在 CI/CD 中集成 &lt;code&gt;socket.dev&lt;/code&gt; 或 &lt;code&gt;snyk&lt;/code&gt; 扫描依赖树&lt;/td&gt;
&lt;td&gt;每次构建时&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;最重要的一条原则：永远不要让 AI Agent 自动执行未经你确认的 &lt;code&gt;npm install&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;一行命令就能验证：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# AI说"npm install xxx"之前，先跑这个&lt;/span&gt;
npm view xxx

&lt;span class="c"&gt;# 看到 404？恭喜你，AI在编故事&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="写在最后"&gt;写在最后&lt;/h2&gt;
&lt;p&gt;HalluSquatting 论文揭示了一个很多开发者还没意识到的问题：&lt;strong&gt;我们对 AI 编程助手的信任，已经超出了它应得的程度。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI 确实能帮你写出大部分代码。但它推荐的包名、仓库地址、安装命令——这些看起来最"确定"的输出，恰恰是它最容易编造的部分。92.4% 的幻觉率不是个小数字。&lt;/p&gt;

&lt;p&gt;我不是说要停用 AI 编程工具。但至少，当它告诉你 &lt;code&gt;npm install xxx&lt;/code&gt; 的时候，花 10 秒钟跑一下 &lt;code&gt;npm view xxx&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;10 秒钟，可能帮你避免一次反向 shell。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;strong&gt;你平时会检查 AI 推荐安装的包是否真实存在吗？还是直接复制粘贴就装了？&lt;/strong&gt; 评论区说说你的习惯。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Fri, 17 Jul 2026 19:45:23 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7791</link>
      <guid>http://beta.w2solo.com/topics/7791</guid>
    </item>
    <item>
      <title>AI 写的 React 组件,合并前必查的 6 个坏味道——每个都有代码对比</title>
      <description>&lt;p&gt;先说一句：这篇不是让你别用 AI 写 React。&lt;/p&gt;

&lt;p&gt;我自己每天用 Claude Code + Cursor,产出效率至少翻倍。但要说得罪的话——&lt;strong&gt;AI 生成的 React 代码,直接合并的风险,比你想象的高&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;不是因为 AI 语法错。语法它比你还熟。&lt;/p&gt;

&lt;p&gt;是因为 React 的"最佳实践"是一堆&lt;strong&gt;上下文相关&lt;/strong&gt;的判断:什么时候用 useEffect、什么时候不用;state 该放哪一层;key 用什么;要不要 memo。这些判断需要看&lt;strong&gt;这段代码所在的整个组件树&lt;/strong&gt;,而 AI 大多数时候只看到你贴过去的那几行。&lt;/p&gt;

&lt;p&gt;于是就有了下面这 6 个几乎每天都能在 AI 生成的 PR 里看到的坏味道。每一个我都给出真实的对比代码,你合并前扫一眼就能看出来。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="1. 把渲染时能算的东西塞进 useEffect"&gt;1. 把渲染时能算的东西塞进 useEffect&lt;/h2&gt;
&lt;p&gt;这是 AI 生成 React 代码里&lt;strong&gt;最普遍&lt;/strong&gt;的坏味道,没有之一。&lt;/p&gt;
&lt;h3 id="AI 常见写法"&gt;AI 常见写法&lt;/h3&gt;&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;OrderList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTotal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFiltered&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

  &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filteredOrders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;setFiltered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filteredOrders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;setTotal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filteredOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;合计: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="为什么坏"&gt;为什么坏&lt;/h3&gt;
&lt;p&gt;三层问题:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;多渲染一轮&lt;/strong&gt;。orders 变 → 组件渲染 (拿到旧 total/filtered) → useEffect 跑 → setState → 再渲染一次。用户能感知的白屏/闪一下,很多来源于此。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;state 和 props 会脱同步&lt;/strong&gt;。如果哪天你多加一个 setFiltered 的分支忘了同步 total,数据就错了。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;依赖数组稍微写错就是死循环&lt;/strong&gt;。AI 写这类代码经常忘依赖或多依赖。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="正确写法"&gt;正确写法&lt;/h3&gt;
&lt;p&gt;派生数据&lt;strong&gt;直接在渲染阶段算&lt;/strong&gt;,不要 state,也不要 effect:&lt;/p&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;OrderList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;合计: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果 filter 真的很贵 (比如上万条 + 复杂计算),再考虑 &lt;code&gt;useMemo&lt;/code&gt;。&lt;strong&gt;先别急着优化&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="判断标准"&gt;判断标准&lt;/h3&gt;
&lt;p&gt;看到 AI 生成的 useEffect,第一个问自己:"这个逻辑,能不能直接写在 return 上面?" 大多数情况都能。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="2. 把 props 复制到 useState"&gt;2. 把 props 复制到 useState&lt;/h2&gt;&lt;h3 id="AI 常见写法"&gt;AI 常见写法&lt;/h3&gt;&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;UserForm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看上去很合理:表单要能编辑,所以得有 state。&lt;/p&gt;
&lt;h3 id="为什么坏"&gt;为什么坏&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useState(user.name)&lt;/code&gt; 里的 &lt;code&gt;user.name&lt;/code&gt; &lt;strong&gt;只在组件第一次挂载时被读一次&lt;/strong&gt;。之后父组件传入新的 user,&lt;code&gt;name&lt;/code&gt; 也不会更新。&lt;/p&gt;

&lt;p&gt;这个 bug 特别隐蔽——本地开发切换用户能看到,但 AI 写完你跑一遍单个用户流程,PR 就过了。&lt;/p&gt;

&lt;p&gt;上线之后一切换用户,表单显示上一个用户的数据,老板会问:"你测过没?"&lt;/p&gt;
&lt;h3 id="正确写法"&gt;正确写法&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;明确表单是"受控还是非受控"&lt;/strong&gt;。两种解:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A. 完全受控,state 在父组件&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;UserForm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
  &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 
  &lt;span class="nl"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; 
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; 
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; 
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; 
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; 
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; 
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; 
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;B. 内部 state,但要用 key 强制重挂载&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserForm&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一切换 user.id,React 会销毁旧组件重新创建,state 也会用新的 initial value。&lt;/p&gt;
&lt;h3 id="判断标准"&gt;判断标准&lt;/h3&gt;
&lt;p&gt;看到 &lt;code&gt;useState(props.xxx)&lt;/code&gt;,九成是错的。要么把 state 提上去,要么用 key 隔离。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="3. useEffect 里 fetch,不处理竞态"&gt;3. useEffect 里 fetch,不处理竞态&lt;/h2&gt;&lt;h3 id="AI 常见写法"&gt;AI 常见写法&lt;/h3&gt;&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Skeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你能一眼看出问题吗?&lt;/p&gt;
&lt;h3 id="为什么坏"&gt;为什么坏&lt;/h3&gt;
&lt;p&gt;用户快速点击不同的用户 A → B → C,fetch 会&lt;strong&gt;按顺序发出&lt;/strong&gt;,但&lt;strong&gt;返回顺序不一定&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;如果 A 的请求走网络慢,C 已经回来了显示了 C 的名字,然后 A 的响应姗姗来迟——&lt;strong&gt;页面会突然变回 A 的数据&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;这不是理论,这是每一个做过用户列表页的前端都栽过的坑。AI 几乎从不主动处理。&lt;/p&gt;
&lt;h3 id="正确写法"&gt;正确写法&lt;/h3&gt;
&lt;p&gt;用 &lt;code&gt;AbortController&lt;/code&gt; 或 stale flag:&lt;/p&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbortError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Skeleton&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者更省心,&lt;strong&gt;直接用数据请求库&lt;/strong&gt;:SWR、React Query、TanStack Query 都自带竞态处理。&lt;/p&gt;
&lt;h3 id="判断标准"&gt;判断标准&lt;/h3&gt;
&lt;p&gt;useEffect 里但凡出现异步,cleanup 函数一定要写。没写就是坏味道。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="4. 列表 key 用 index"&gt;4. 列表 key 用 index&lt;/h2&gt;&lt;h3 id="AI 常见写法"&gt;AI 常见写法&lt;/h3&gt;&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ListItem&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;))}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写这一行时 AI 甚至不会犹豫。&lt;/p&gt;
&lt;h3 id="为什么坏"&gt;为什么坏&lt;/h3&gt;
&lt;p&gt;当列表&lt;strong&gt;顺序变化&lt;/strong&gt;、&lt;strong&gt;中间插入&lt;/strong&gt;、&lt;strong&gt;删除&lt;/strong&gt;时,React 会用 key 去匹配前后两次渲染的元素。用 index 做 key,等于告诉 React:"第一位永远是第一位"——但你换了内容它认不出来。&lt;/p&gt;

&lt;p&gt;后果:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;列表项里如果有 &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;,用户输入到一半,你在上面插入一项,焦点就跑了&lt;/li&gt;
&lt;li&gt;列表项如果是带内部 state 的组件,state 会串位&lt;/li&gt;
&lt;li&gt;动画/transition 会错乱&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="正确写法"&gt;正确写法&lt;/h3&gt;
&lt;p&gt;用&lt;strong&gt;数据本身的稳定 id&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ListItem&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;))}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果数据真的没 id,前端可以在拿到数据时用 &lt;code&gt;crypto.randomUUID()&lt;/code&gt; 或自增计数补一个,&lt;strong&gt;但要保证同一个数据永远拿同一个 id&lt;/strong&gt;——不能每次渲染新生成。&lt;/p&gt;
&lt;h3 id="判断标准"&gt;判断标准&lt;/h3&gt;
&lt;p&gt;看到 &lt;code&gt;key={index}&lt;/code&gt; &lt;code&gt;key={i}&lt;/code&gt; &lt;code&gt;key={idx}&lt;/code&gt;,直接怀疑。除非这个列表&lt;strong&gt;永远不会重排、不会插入删除、不会有内部 state&lt;/strong&gt;——才 100% 安全。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="5. 内联对象 / 函数,搭配 memo 组件 = memo 失效"&gt;5. 内联对象 / 函数,搭配 memo 组件 = memo 失效&lt;/h2&gt;&lt;h3 id="AI 常见写法"&gt;AI 常见写法&lt;/h3&gt;&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;borderBottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1px solid #eee&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看上去合理:子组件 memo 了,应该优化了。&lt;/p&gt;
&lt;h3 id="为什么坏"&gt;为什么坏&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;style={{ padding: 8 }}&lt;/code&gt; 和 &lt;code&gt;onClick={() =&amp;gt; ...}&lt;/code&gt; &lt;strong&gt;每次渲染都是新的引用&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;React.memo&lt;/code&gt; 默认用 &lt;code&gt;Object.is&lt;/code&gt; 做浅比较,新旧引用不等 → memo 直接失效 → 每次父组件渲染,所有 Row 都重渲染。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;你以为你优化了,其实一点没优化,还多了个浅比较的开销。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="正确写法"&gt;正确写法&lt;/h3&gt;
&lt;p&gt;内联对象提到外面 (如果是纯静态的):&lt;/p&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rowStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;borderBottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1px solid #eee&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rowStyle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleClick&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;rowId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rowId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rowId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意:让 Row 内部生成 &lt;code&gt;() =&amp;gt; onClick(rowId)&lt;/code&gt; 才能保证 &lt;code&gt;onClick&lt;/code&gt; 是稳定引用。&lt;/p&gt;
&lt;h3 id="判断标准"&gt;判断标准&lt;/h3&gt;
&lt;p&gt;看到 &lt;code&gt;React.memo&lt;/code&gt; 出现,立即扫一眼调用处的 props——如果传的是 &lt;code&gt;{...}&lt;/code&gt;、&lt;code&gt;[...]&lt;/code&gt;、&lt;code&gt;() =&amp;gt; ...&lt;/code&gt;,memo 直接白包。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="6. useMemo / useCallback 无脑加"&gt;6. useMemo / useCallback 无脑加&lt;/h2&gt;
&lt;p&gt;这条和上一条正好相反——&lt;strong&gt;AI 也经常反过来滥用 memo&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="AI 常见写法"&gt;AI 常见写法&lt;/h3&gt;&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;Greeting&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;greeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`Hello, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleClick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleClick&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三行代码,两个 hook。&lt;/p&gt;
&lt;h3 id="为什么坏"&gt;为什么坏&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;useMemo&lt;/code&gt; 和 &lt;code&gt;useCallback&lt;/code&gt; &lt;strong&gt;本身有成本&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;每次渲染都要跑依赖数组的浅比较&lt;/li&gt;
&lt;li&gt;React 内部要维护一个额外的记忆槽&lt;/li&gt;
&lt;li&gt;代码可读性直线下降&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;一个字符串拼接,&lt;strong&gt;直接计算比 useMemo 快十倍&lt;/strong&gt;。给 button 传的 onClick 也不是 memo 组件,useCallback 完全没意义。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;滥用 memo 的代码,通常比不加 memo 更慢——因为该做的事情没少做,还多做了一层浅比较。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="正确写法"&gt;正确写法&lt;/h3&gt;&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;Greeting&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;greeting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Hello, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;greeting&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;只在下面两种情况才加 memo&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;计算真的贵 (比如上万条数据的 filter/sort/reduce)&lt;/li&gt;
&lt;li&gt;值/函数&lt;strong&gt;要作为 prop 传给 memo 组件&lt;/strong&gt;,或者作为 useEffect/useMemo 的依赖&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;除此之外——&lt;strong&gt;不加就是对的&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="判断标准"&gt;判断标准&lt;/h3&gt;
&lt;p&gt;看到 useMemo/useCallback,反问一句:"如果去掉它,哪里会重新计算/重渲染?" 答不上来,就删掉。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="最后:一份合并前 60 秒 checklist"&gt;最后:一份合并前 60 秒 checklist&lt;/h2&gt;
&lt;p&gt;如果你没时间读完全文,把下面这段贴到你的 Code Review 模板里:&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;□ useEffect 里的逻辑,能不能挪到渲染阶段?
□ useState(props.xxx) 出现了吗?出现就是错。
□ useEffect 里的 fetch,有没有 AbortController 或 cleanup?
□ 列表 key 是不是稳定 id?不是就改。
□ React.memo 组件的 props,有没有内联对象/函数/数组?
□ useMemo/useCallback,如果删掉会怎样?说不上来就删。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;六个问题,合并前扫一眼一分钟搞定。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="一句话总结"&gt;一句话总结&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;AI 生成的 React 代码,语法都对,但 React 不是纯粹语法。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;它是一套"什么时候该干什么、什么时候该什么都不干"的判断力。AI 看不到你的组件树、看不到用户如何交互、看不到线上的性能数据——这些判断,现在还得你来做。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;用 AI 写代码不丢人,合并前不看 AI 写的什么,才丢人。&lt;/strong&gt;&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Thu, 16 Jul 2026 21:53:41 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7779</link>
      <guid>http://beta.w2solo.com/topics/7779</guid>
    </item>
    <item>
      <title>面试官让我优化一个卡死的表格，我打开 Claude 五秒重写——他放下咖啡问我要不要来当组长</title>
      <description>&lt;p&gt;上周去面试一家做 SaaS 的中厂，二面是一位四十出头的前端负责人。&lt;/p&gt;

&lt;p&gt;寒暄结束，他直接把笔记本转过来推到桌子中间：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"这是我们线上一个真实的报表页，10 万行数据，滚起来直接卡死。你帮我看看怎么优化。用什么工具都行。"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;最后那半句他说得很平淡，我当时没多想。&lt;/p&gt;

&lt;p&gt;结果二十分钟后，他把咖啡杯往桌上一放，问了我一个我准备了整场面试都没想到的问题。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="一、题目本身：一个"&gt;一、题目本身：一个"教科书级"的坑&lt;/h2&gt;
&lt;p&gt;页面代码大概长这样（我做了脱敏改写）：&lt;/p&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;ReportTable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// rows.length ≈ 100_000&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;thead&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;thead&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Select&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;statusOptions&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DatePicker&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写过一年 React 的都能一眼看出问题：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;10 万行一次性渲染 → 首屏就是白屏 6-8 秒&lt;/li&gt;
&lt;li&gt;每行还挂着 &lt;code&gt;&amp;lt;Select&amp;gt;&lt;/code&gt; &lt;code&gt;&amp;lt;DatePicker&amp;gt;&lt;/code&gt; 这种带弹层的重组件&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;key&lt;/code&gt; 没问题，但 diff 也扛不住 40 万个 DOM 节点&lt;/li&gt;
&lt;li&gt;滚动过程再触发 hover / active，浏览器直接躺平&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;搁在教科书里，标准优化路径也有三条：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;虚拟滚动（react-window / &lt;a href="/tanstack" class="user-mention" title="@tanstack"&gt;&lt;i&gt;@&lt;/i&gt;tanstack&lt;/a&gt;/react-virtual）&lt;/li&gt;
&lt;li&gt;时间切片渐进渲染（&lt;code&gt;startTransition&lt;/code&gt; / &lt;code&gt;requestIdleCallback&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;Canvas / 半 Canvas 渲染（AG Grid、Handsontable 那种）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;三种我都懂，但每种都有细节：单元格要不要支持编辑？行高是否等高？要不要支持多选？滚动过程能不能容忍占位闪一下？&lt;/p&gt;

&lt;p&gt;真让我在白板上写完再讲清楚三者的取舍——20 分钟够呛。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="二、我做了一件"&gt;二、我做了一件"面试大忌"的事&lt;/h2&gt;
&lt;p&gt;我抬头问了一句：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"您刚才说，用什么工具都行？"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;面试官点头。&lt;/p&gt;

&lt;p&gt;于是我把自己的笔记本翻开，打开 Claude Code，把这段组件连同 &lt;code&gt;Row&lt;/code&gt; 类型、业务约束一起丢了进去，只写了一句：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"React 18 + TS。10 万行表格，每行 5 列，其中两列有 Select 和 DatePicker。产品要求可以滚动、可以点某一行进入详情、后期可能要支持行内编辑。给我三种优化方案，并解释在哪种交互需求下谁最合适。"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;大概 4 秒之后，屏幕上刷出了三段方案。&lt;/p&gt;
&lt;h3 id="方案一：虚拟滚动（@user2/react-virtual）"&gt;方案一：虚拟滚动（&lt;a href="/tanstack" class="user-mention" title="@tanstack"&gt;&lt;i&gt;@&lt;/i&gt;tanstack&lt;/a&gt;/react-virtual）&lt;/h3&gt;&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useVirtualizer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tanstack/react-virtual&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;ReportTable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parentRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rowVirtualizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useVirtualizer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;getScrollElement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;parentRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;estimateSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;44&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;overscan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;parentRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rowVirtualizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getTotalSize&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;relative&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rowVirtualizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getVirtualItems&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;absolute&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`translateY(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* row cells */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Claude 的注释里写：&lt;em&gt;"最通用、社区最成熟。等高行下开箱即用，行内编辑也没问题。缺点是 Select / DatePicker 这类弹层组件在滚出可视区时会被销毁，弹层里的选中态要托管到外层 state。"&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="方案二：时间切片渐进渲染"&gt;方案二：时间切片渐进渲染&lt;/h3&gt;&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;ReportTable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;visibleCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setVisibleCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;visibleCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestIdleCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;setVisibleCount&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cancelIdleCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;visibleCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;visibleCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* cells */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Claude 提示：&lt;em&gt;"首屏最快，DOM 结构最简单，SEO 友好。但滚到最底部要 10+ 秒才能加载完，用户拖滚动条会看到断层。适合'看不完但不能少'的报表——比如打印导出前的预览页。"&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="方案三：半 Canvas 渲染（AG Grid / 自研）"&gt;方案三：半 Canvas 渲染（AG Grid / 自研）&lt;/h3&gt;
&lt;p&gt;Claude 直接建议接第三方库，并给出了两条使用理由：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"如果表格未来要支持行内编辑、多选、复制粘贴、列拖拽——自己造轮子会一路踩坑。AG Grid 的 Community 版本免费够用，缺点是包体积 500KB+，且样式覆盖成本高。"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我把屏幕转向面试官。他没说话，只是往前挪了挪坐姿。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="三、真正让面试官愣住的，不是 AI 给的答案"&gt;三、真正让面试官愣住的，不是 AI 给的答案&lt;/h2&gt;
&lt;p&gt;我接着说：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"我倾向于方案一。因为您说了'后期可能要支持行内编辑'——Canvas 方案改起来太重，切片方案在编辑焦点跳出时会重排。虚拟滚动只要把弹层组件的开合状态提到外层就行。要不咱们跑一下 Performance 看看？"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我把三种方案都在他们的测试环境跑了一遍，Chrome DevTools 打开 Performance。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;未优化：首屏 6.8 秒，滚动帧率 8 FPS&lt;/li&gt;
&lt;li&gt;方案一：首屏 320 ms，滚动稳定 60 FPS&lt;/li&gt;
&lt;li&gt;方案二：首屏 90 ms，但滚到底部要 12 秒&lt;/li&gt;
&lt;li&gt;方案三：首屏 480 ms，60 FPS，但 bundle 大了 480 KB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;首屏时间从 6.8 秒压到 320 毫秒，快了 &lt;strong&gt;21 倍&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;面试官这时候放下了咖啡杯，问了我第一个"意外"的问题：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"你怎么判断把弹层 state 提到外层就够了？"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我说：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"我没直接判断。Claude 给方案后我又追问了一次——'如果同一行的 Select 打开时用户滚出可视区，会发生什么？'它回答说组件会 unmount，弹层直接消失。所以我在方案一里加了一句：把 openId 提到父组件用 Portal 渲染弹层。这样滚出去也不会关。"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;面试官盯着我看了两秒，问了第二个问题：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"你组里其他前端，也是这么用 AI 的吗？"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我如实说：不是。大部分同事要么完全不用，要么直接把 AI 的答案 Ctrl+C Ctrl+V 提交。&lt;/p&gt;

&lt;p&gt;他"嗯"了一声，然后是第三个问题——就是那个让我离开会议室之后琢磨了一路的问题：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"我们前端组长下个月转岗。你要不要考虑一下这个位置？"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr&gt;
&lt;h2 id="四、我在地铁上想明白的事"&gt;四、我在地铁上想明白的事&lt;/h2&gt;
&lt;p&gt;回程地铁上我一直在想：他到底看中了什么？&lt;/p&gt;

&lt;p&gt;不是虚拟滚动。方案二方案三我都答得上来，任何一个高级前端都能。&lt;/p&gt;

&lt;p&gt;不是 AI。他们组里肯定有人 Claude、Cursor 都开着。&lt;/p&gt;

&lt;p&gt;想通之后我总结出三点，写在这里给同样在跳槽的朋友：&lt;/p&gt;
&lt;h3 id="1. 他看的是信息带宽，不是答题速度"&gt;1. 他看的是&lt;strong&gt;信息带宽&lt;/strong&gt;，不是&lt;strong&gt;答题速度&lt;/strong&gt;
&lt;/h3&gt;
&lt;p&gt;传统面试考察的是"你脑子里存了多少知识"。但当所有人脑子里都能调用同一个 AI 的时候，考察点就变成了"你能在单位时间内提出多好的问题"。&lt;/p&gt;

&lt;p&gt;我丢给 Claude 的那条 prompt 里有五个关键要素：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;技术栈（React 18 + TS）&lt;/li&gt;
&lt;li&gt;数据量级（10 万行 5 列）&lt;/li&gt;
&lt;li&gt;复杂组件（Select、DatePicker）&lt;/li&gt;
&lt;li&gt;交互约束（可滚动、可点击、可能要编辑）&lt;/li&gt;
&lt;li&gt;判断标准&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;如果我只写"帮我优化这个卡顿的表格"，Claude 给的答案就是水平线。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;面试官看到的其实是：这个人的 prompt 里，藏着五年前端经验。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="2. 他看的是验证能力，不是采纳能力"&gt;2. 他看的是&lt;strong&gt;验证能力&lt;/strong&gt;，不是&lt;strong&gt;采纳能力&lt;/strong&gt;
&lt;/h3&gt;
&lt;p&gt;我在讲方案时说过一句话："这是 Claude 给的，但我用 Performance 面板验证过了。"&lt;/p&gt;

&lt;p&gt;这句话之所以关键，是因为它划出了两类人：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;把 AI 输出当权威，直接提交&lt;/li&gt;
&lt;li&gt;把 AI 输出当同事的初稿，看完了再判断&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;后一种人，团队里其实不多。而这种人是能带团队的。&lt;/p&gt;
&lt;h3 id="3. 他看的是你怎么让 AI 干活，不是你会不会用 AI"&gt;3. 他看的是&lt;strong&gt;你怎么让 AI 干活&lt;/strong&gt;，不是&lt;strong&gt;你会不会用 AI&lt;/strong&gt;
&lt;/h3&gt;
&lt;p&gt;前端组长这个岗位在他心里对应的画像大概是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"一个能把 AI 当放大器用的人，一个人的产出等于三个人。而且他知道什么时候不该信 AI——比如弹层脱离视口、比如包体积不能牺牲。"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我在展示解题过程里，无意间把这两点都露出来了。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="五、给还在准备面试的前端"&gt;五、给还在准备面试的前端&lt;/h2&gt;
&lt;p&gt;如果你也在跳槽，尤其在中厂到大厂的过渡区间，几个我踩过之后的建议：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;能带电脑就带电脑。&lt;/strong&gt; 越来越多的公司允许现场用工具了。别怕面试官觉得你依赖 AI——他们在乎的是你怎么用。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;练习"提问"比练习"手写"回报大。&lt;/strong&gt; 手写 Promise、手写深拷贝、手写 EventEmitter，AI 三秒能解。但"给出三种方案 + 比较适用场景 + 结合数据量和交互约束"这种 prompt，得真练。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;每一次 AI 给答案后，都强迫自己追问一次"如果 X 前提不成立，会怎样"。&lt;/strong&gt; 例如：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果表格行高不等，虚拟滚动会怎样？&lt;/li&gt;
&lt;li&gt;如果用户网速慢，切片渲染的最后一屏会怎样？&lt;/li&gt;
&lt;li&gt;如果产品要打印全表，Canvas 方案怎么办？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这是把 AI 的天花板变成你的地板的唯一方法。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;准备一段"我怎么用 AI"的故事。&lt;/strong&gt; 不是理论，是具体的一次协作过程。这段故事的价值，比你把 Fiber 原理背下来大得多。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="尾声"&gt;尾声&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;2026 年了，考察前端的方式已经变了。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;变化不是"你会不会用 AI"，而是"你能不能带着 AI，做出一个人做不完的事"。&lt;/p&gt;

&lt;p&gt;面试官那天放下咖啡的那 2 秒，我大概能听见他脑子里在想什么：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"这人来了之后，我这个季度的排期能砍一半。"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;能被这么想，比手写十道八股都值钱。&lt;/strong&gt;&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Wed, 15 Jul 2026 20:47:17 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7767</link>
      <guid>http://beta.w2solo.com/topics/7767</guid>
    </item>
    <item>
      <title>看了微软几万人用 AI 编程的数据——效率涨 24% 的代价没人提</title>
      <description>&lt;p&gt;微软刚发了一份研究报告：给几万名工程师部署 Claude Code 和 Copilot CLI 四个月后，每人每天多合并了 24% 的 PR。管理层都在转发。但我把它和另外几份数据放在一起看完之后——坐不住了。&lt;/p&gt;
&lt;h2 id="24% 的效率提升，听起来很美"&gt;24% 的效率提升，听起来很美&lt;/h2&gt;
&lt;p&gt;先说微软这份报告的核心数据：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;研究对象&lt;/strong&gt;：数万名微软工程师&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;部署工具&lt;/strong&gt;：Claude Code + GitHub Copilot CLI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;时间跨度&lt;/strong&gt;：2026 年初，四个月&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;核心结论&lt;/strong&gt;：每人每天合并的 PR 增加了 24%（置信区间 14.5%-33.7%）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;24% 的提升确实不小。如果一个团队以前一天能合并 10 个 PR，现在能合并 12-13 个。管理层看到这个数字，第一反应是"全员推广"。&lt;/p&gt;

&lt;p&gt;但 PR 数量增加 = 生产力提升吗？&lt;/p&gt;

&lt;p&gt;这份报告在 Hacker News 的讨论帖里，点赞最高的评论说了一句话：&lt;/p&gt;

&lt;p&gt;Lines of code isn't productivity, and neither is CLs landed.（代码行数不是生产力，合并的 PR 数也不是。）&lt;/p&gt;
&lt;h2 id="效率涨了 24%，但另一组数据没人提"&gt;效率涨了 24%，但另一组数据没人提&lt;/h2&gt;
&lt;p&gt;把微软报告和其他几份最近的研究放在一起，画风完全不同了：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;数据一：AI 代码的 bug 率高 70%&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Shiplight 对数千个 PR 的分析显示，AI 生成的代码比人写的多 &lt;strong&gt;1.7 倍 bug&lt;/strong&gt;，逻辑错误多 75%，可读性问题多 &lt;strong&gt;3 倍&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;PR 是多了 24%，但每个 PR 里藏的 bug 也多了 70%。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;数据二：96% 的开发者不完整审查 AI 代码&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sonar 的调查显示，96% 的开发者不会完整审查 AI 生成的代码。AI 写完了，扫一眼，能跑就合，下一个。&lt;/p&gt;

&lt;p&gt;24% 更多的 PR + 每个 PR bug 多 70% + 96% 没人仔细看 = ？&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;数据三：开发者对 AI 代码的信任度降至 33%&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;2026 年的调查数据显示，只有 33% 的开发者信任 AI 生成的代码质量。但信不信是一回事，用不用是另一回事——工期在那摆着，老板在催，不用 AI 就是效率低。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;数据四：有人靠清理 AI 代码月入 4 万美元&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;三个工程师辞职创办了 Slopfix，专门帮企业清理 AI 生成的代码，收费每周 1 万美元。生意好到接不完。&lt;/p&gt;

&lt;p&gt;Forbes 上周的标题更直接：&lt;strong&gt;AI Coding Is Starting To Feel Like A Gamble&lt;/strong&gt;（AI 编程开始像赌博了）。&lt;/p&gt;
&lt;h2 id="真正的代价是什么"&gt;真正的代价是什么&lt;/h2&gt;
&lt;p&gt;效率涨 24% 很好。但代价是什么？我整理了五个"报告里没写"的隐性成本。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;代价一：技术债加速积累&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI 写代码快，但它不会考虑架构一致性。一个 10 人团队里 10 个人都用 AI，每个人生成的代码风格、错误处理方式、状态管理策略都不一样。&lt;/p&gt;

&lt;p&gt;三个月后，代码库变成缝合怪。以前技术债线性增长，现在 AI 让代码产出翻倍，技术债也翻倍，但还的速度没变。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;代价二：Code Review 变成新瓶颈&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PR 增加了 24%——谁来 Review？&lt;/p&gt;

&lt;p&gt;AI 可以替你写代码，但不能替你 Review 别人的代码。PR 多了 24%，Review 的工作量也多了 24%。而且 AI 代码的 Review 比人写的更难——因为你不知道"为什么"这样写，只能看到"是什么"。&lt;/p&gt;

&lt;p&gt;结果：要么 Review 质量下降（96% 的人已经在这么做了），要么 Review 成为整个团队的瓶颈。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;代价三：初级开发者不成长了&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI 写代码最大的受益者是初级工程师——产出"看起来"和高级差不多。但"看起来差不多"和"真的差不多"是两回事。&lt;/p&gt;

&lt;p&gt;初级工程师用 AI 绕过了"自己思考解决方案"的过程，产出有了，能力没涨。一年后：公司账面上有一批"高产出"的初级工程师，但没人能 debug AI 写的代码，也没人能做架构决策。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;代价四：安全漏洞乘数效应&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;安全研究显示，AI 生成的代码安全漏洞率是人写代码的 &lt;strong&gt;2.74 倍&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;以前一天 100 行代码，可能 2 个安全漏洞。现在一天 300 行，漏洞可能有 16 个。产出涨 3 倍，漏洞涨 8 倍。而且 AI 的安全漏洞更难发现——代码"看起来"很标准，就是在某个边界条件上漏了一个验证。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;代价五：调试成本不降反升&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI 代码最讽刺的地方：写得快，修得慢。&lt;/p&gt;

&lt;p&gt;你没有亲手写过，不知道设计意图，不知道为什么选了这个方案。出了 bug，面对的是"别人写的代码"——只不过这个"别人"是 AI，你连问都没法问。&lt;/p&gt;

&lt;p&gt;Slopfix 每周 1 万美元帮人清理 AI 代码，不是因为 AI 代码特别烂——是因为没有人知道它是怎么回事，只能推倒重来。&lt;/p&gt;
&lt;h2 id="速查表：你的 AI 代码有没有踩这些坑"&gt;速查表：你的 AI 代码有没有踩这些坑&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;AI 代码合并后从不回头看&lt;/td&gt;
&lt;td&gt;查最近 10 个 AI PR 的后续修改记录&lt;/td&gt;
&lt;td&gt;🔴 高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review 时只看"能不能跑"&lt;/td&gt;
&lt;td&gt;能说出这段代码为什么这样写吗？&lt;/td&gt;
&lt;td&gt;🔴 高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;团队没有 AI 代码审查标准&lt;/td&gt;
&lt;td&gt;有没有专门的 AI 代码 checklist？&lt;/td&gt;
&lt;td&gt;🟡 中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;初级成员产出突然和高级持平&lt;/td&gt;
&lt;td&gt;对比 debug 能力和架构讨论参与度&lt;/td&gt;
&lt;td&gt;🟡 中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;代码风格越来越不一致&lt;/td&gt;
&lt;td&gt;翻最近一个月的提交看格式/命名&lt;/td&gt;
&lt;td&gt;🟡 中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;从没跑过安全扫描&lt;/td&gt;
&lt;td&gt;对 AI 代码做一次安全扫描&lt;/td&gt;
&lt;td&gt;🔴 高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;线上 bug 修复时间变长&lt;/td&gt;
&lt;td&gt;对比引入 AI 前后的修复时长&lt;/td&gt;
&lt;td&gt;🔴 高&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;收藏这张表。下次做月度回顾的时候，对着查一遍。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="不是不该用 AI，是不该盲目用"&gt;不是不该用 AI，是不该盲目用&lt;/h2&gt;
&lt;p&gt;微软的 24% 效率提升是真的。AI 编程确实有价值。&lt;/p&gt;

&lt;p&gt;但"写得快"和"写得好"是两件事。微软的报告衡量的是前者，我们日常承受的是后者的后果。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;正确的用法：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AI 写代码 → 但必须有人类 Review（不是扫一眼）&lt;/li&gt;
&lt;li&gt;用 AI 加速 → 省出来的时间做 Code Review 和测试&lt;/li&gt;
&lt;li&gt;初级可以用 AI → 但必须能解释 AI 生成的每一行代码&lt;/li&gt;
&lt;li&gt;衡量效率 → 同时衡量 bug 率、安全漏洞数、技术债增速&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;PR 涨了 24%，如果 bug 也涨了 70%，净效果是正的还是负的？&lt;/p&gt;

&lt;p&gt;这道算术题，每个技术负责人都应该自己算一遍。&lt;/p&gt;

&lt;p&gt;你的团队用 AI 编程后，代码质量有变化吗？评论区说说你的观察。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;strong&gt;觉得有用？点个「赞」👍 让更多人看到&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;关注我，获取更多技术干货。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Tue, 14 Jul 2026 21:06:42 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7759</link>
      <guid>http://beta.w2solo.com/topics/7759</guid>
    </item>
    <item>
      <title>别再用 AI debug 了——这 5 种 bug 越修越烂</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;上周 Hacker News 上火了一个帖子：三个工程师开了家公司叫 Slopfix，专门帮人删 AI 生成的代码，收费每周 1 万美元。生意好到接不完。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="AI debug 的死亡循环"&gt;AI debug 的死亡循环&lt;/h2&gt;
&lt;p&gt;你一定经历过这个场景：&lt;/p&gt;

&lt;p&gt;代码报错了。你把报错信息丢给 AI。AI 给你改了。跑了一下，原来的错没了，但出了一个新错。你又丢给 AI。AI 又改了。又出了一个新错。&lt;/p&gt;

&lt;p&gt;三轮下来，你的代码从 1 个 bug 变成了 5 个 bug，而且你已经完全不知道 AI 到底改了什么。&lt;/p&gt;

&lt;p&gt;这不是你的问题。Shiplight 对数千个 PR 的数据分析显示：&lt;strong&gt;AI 生成的代码比人写的多 1.7 倍 Bug，逻辑错误多 75%。&lt;/strong&gt; 也就是说，AI 在"修 bug"的过程中，大概率在制造新的 bug。&lt;/p&gt;

&lt;p&gt;更扎心的是——Sonar 的调查说 96% 的开发者不会完整审查 AI 生成的代码。AI 改了三轮，你看都没仔细看就跑了，然后发现又炸了。&lt;/p&gt;

&lt;p&gt;现在有人靠清理 AI 代码每周赚 1 万美元。这说明 AI debug 的"售后成本"已经大到可以撑起一门生意了。&lt;/p&gt;

&lt;p&gt;以下 5 种 bug，你越让 AI 修，就越烂。&lt;/p&gt;
&lt;h2 id="第 1 种：CSS 和样式问题"&gt;第 1 种：CSS 和样式问题&lt;/h2&gt;
&lt;p&gt;AI 修 CSS 最大的问题：&lt;strong&gt;它看不到级联。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;你说"这个按钮的边距不对"。AI 加了一个 &lt;code&gt;margin-top: 20px&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;但这个按钮在另一个页面也用到了，那边的边距本来是对的，现在也被影响了。&lt;/p&gt;

&lt;p&gt;你再告诉 AI。AI 怎么修？加一个更高优先级的选择器覆盖。&lt;/p&gt;

&lt;p&gt;两轮下来，样式表里多了两层覆盖，一堆 &lt;code&gt;!important&lt;/code&gt;，和一个完全看不懂的选择器优先级链。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么 AI 修不好 CSS：&lt;/strong&gt; CSS 是全局作用域的，改一处影响多处。AI 每次只看你指给它的那一处。它的修法永远是"在上面再叠一层"，而不是找到根因。越叠越多，最后只有重写能理清。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;自己修：&lt;/strong&gt; 打开 DevTools，看 Computed Styles 找到实际生效的规则，改根因，一次改对。&lt;/p&gt;
&lt;h2 id="第 2 种：跨组件的状态 bug"&gt;第 2 种：跨组件的状态 bug&lt;/h2&gt;
&lt;p&gt;列表页和详情页共享一个全局状态。用户在详情页做了修改，回到列表页没更新。&lt;/p&gt;

&lt;p&gt;你把 bug 丢给 AI。AI 在列表页加了一个 &lt;code&gt;useEffect&lt;/code&gt; 重新请求数据。&lt;/p&gt;

&lt;p&gt;好了，数据更新了。但现在每次从&lt;strong&gt;任何页面&lt;/strong&gt;回到列表，都会重新请求——包括从搜索页、设置页回来。接口被打爆了。&lt;/p&gt;

&lt;p&gt;你再跟 AI 说"请求太频繁了"。AI 加了一个防抖。现在数据有时候更新有时候不更新，取决于你切页面的速度够不够快。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;两轮修复 = 一个 bug 变三个 bug。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么 AI 修不好状态 bug：&lt;/strong&gt; 状态 bug 的根因通常不在报错的那个组件里，而在数据流的上游。AI 看到的是"这个组件数据不对"，修法是"让这个组件自己拿数据"——治标不治本。每治一次标，就多一条数据流路径，系统复杂度指数级上升。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;自己修：&lt;/strong&gt; 先画出数据流向——哪个组件读、哪个组件写、经过几层传递。状态 bug 的修复点在数据源头，不在展示末端。&lt;/p&gt;
&lt;h2 id="第 3 种：只在特定环境出现的 bug"&gt;第 3 种：只在特定环境出现的 bug&lt;/h2&gt;
&lt;p&gt;线上报了一个 bug，本地复现不了。&lt;/p&gt;

&lt;p&gt;你把报错日志丢给 AI。AI 说"可能是没做空值判断"，给你加了个 &lt;code&gt;?.&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;部署上去，线上还是报错。因为问题根本不是空值——是某个环境变量在生产环境的值和开发环境不一样。&lt;/p&gt;

&lt;p&gt;你又把新的报错丢给 AI。AI 又猜了一个原因，又改了一处。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;这是 AI debug 最危险的场景：它在猜。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI 没有你的生产环境，没有你的日志系统，没有你的监控面板。它只能根据你给它的那几行报错信息"推测"原因。而环境类 bug 的根因通常在配置、网络、权限这些层面——不在代码层面。&lt;/p&gt;

&lt;p&gt;AI 的每次"猜测性修复"都在往代码里塞不必要的防御逻辑。三轮下来，代码里全是 &lt;code&gt;?.&lt;/code&gt;、&lt;code&gt;try-catch&lt;/code&gt;、&lt;code&gt;if (!x) return&lt;/code&gt;——真正的问题一个没解决。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;自己修：&lt;/strong&gt; 先确认是不是代码 bug。看日志、看监控、比较环境差异。如果确认是代码问题，给 AI 完整的上下文（报错日志 + 环境信息 + 复现步骤）再让它帮忙。&lt;/p&gt;
&lt;h2 id="第 4 种：依赖冲突"&gt;第 4 种：依赖冲突&lt;/h2&gt;
&lt;p&gt;某个包升级后，项目跑不起来了。&lt;/p&gt;

&lt;p&gt;你告诉 AI。AI 说"试试降级到 X 版本"。降了，那个包好了，另一个包又报错了——因为它依赖更高版本。&lt;/p&gt;

&lt;p&gt;你又告诉 AI。AI 说"那把另一个包也降级"。降了，TypeScript 报了一屏的类型错误。&lt;/p&gt;

&lt;p&gt;这就是&lt;strong&gt;依赖地狱&lt;/strong&gt;，AI 不但解决不了，还会让你陷得更深。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么 AI 修不好依赖冲突：&lt;/strong&gt; 依赖关系是一张网，不是一条线。改一个版本号可能影响几十个包。AI 的修法永远是"改版本号"——它不会帮你看 changelog，不会帮你判断哪个大版本有 breaking change。每改一次版本号，可能性空间都在指数膨胀。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;自己修：&lt;/strong&gt; 先看报错包的 changelog 和 peer dependencies。依赖冲突不是"试版本号"能解决的，是"理解兼容性矩阵"的问题。&lt;/p&gt;
&lt;h2 id="第 5 种：异步时序和竞态条件"&gt;第 5 种：异步时序和竞态条件&lt;/h2&gt;
&lt;p&gt;两个接口同时请求，后发的先回来，页面显示了旧数据。&lt;/p&gt;

&lt;p&gt;你告诉 AI。AI 加了一个 loading 状态。现在页面不闪了，但数据还是错的——因为竞态的根因不在渲染层面，在请求层面。&lt;/p&gt;

&lt;p&gt;你再告诉 AI。AI 加了一个 &lt;code&gt;AbortController&lt;/code&gt;。但位置不对，有时候把正常请求也取消了。&lt;/p&gt;

&lt;p&gt;你再告诉 AI。AI 加了一个请求计数器。但计数器在组件卸载时没清理，内存泄漏了。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;三轮下来：一个竞态 bug → 变成竞态 + 请求取消异常 + 内存泄漏。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么 AI 修不好时序 bug：&lt;/strong&gt; AI 的思维是"逐行执行"的，它天生不擅长推理"两件事同时发生会怎样"。竞态条件的修复需要理解整个异步流的时序关系，不是在某个点加一行代码。AI 的每次修复都是局部的，但时序问题本质上是全局的。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;自己修：&lt;/strong&gt; 先用 Network 面板理清请求的实际时序。竞态问题的方案通常是架构层面的（请求取消策略、乐观更新、版本号比对），不是在单个函数里打补丁。&lt;/p&gt;
&lt;h2 id="速查表：这个 bug 该自己修还是让 AI 修？"&gt;速查表：这个 bug 该自己修还是让 AI 修？&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;bug 类型&lt;/th&gt;
&lt;th&gt;让 AI 修？&lt;/th&gt;
&lt;th&gt;理由&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CSS/样式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 自己修&lt;/td&gt;
&lt;td&gt;AI 看不到级联，只会叠覆盖&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;跨组件状态&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 自己修&lt;/td&gt;
&lt;td&gt;AI 治标不治本，越修数据流越乱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;环境差异&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 自己修&lt;/td&gt;
&lt;td&gt;AI 无法访问你的环境，只能猜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;依赖冲突&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 自己修&lt;/td&gt;
&lt;td&gt;AI 不理解兼容性矩阵&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;异步时序&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ 自己修&lt;/td&gt;
&lt;td&gt;AI 不擅长推理并发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;语法错误&lt;/td&gt;
&lt;td&gt;✅ 让 AI 修&lt;/td&gt;
&lt;td&gt;AI 的强项，秒修&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;类型报错&lt;/td&gt;
&lt;td&gt;✅ 让 AI 修&lt;/td&gt;
&lt;td&gt;AI 很擅长 TS 类型修复&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单函数逻辑&lt;/td&gt;
&lt;td&gt;✅ 让 AI 修&lt;/td&gt;
&lt;td&gt;输入输出明确，修得又快又好&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写测试用例&lt;/td&gt;
&lt;td&gt;✅ 让 AI 写&lt;/td&gt;
&lt;td&gt;AI 很擅长生成边界测试&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;报错信息翻译&lt;/td&gt;
&lt;td&gt;✅ 让 AI 查&lt;/td&gt;
&lt;td&gt;比 Google 快&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;收藏这张表。下次 debug 前先看一眼——有些 bug 你自己 30 分钟搞定，丢给 AI 折腾半天还越修越多。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="不是 AI 不好，是 debug 不是它擅长的事"&gt;不是 AI 不好，是 debug 不是它擅长的事&lt;/h2&gt;
&lt;p&gt;AI 最擅长的是&lt;strong&gt;生成&lt;/strong&gt;——给它一个清晰的需求，它写代码又快又好。&lt;/p&gt;

&lt;p&gt;AI 最不擅长的是&lt;strong&gt;诊断&lt;/strong&gt;——给它一个模糊的症状，让它找根因，它只会猜。猜错了就在猜错的方向上继续修，越修越远。&lt;/p&gt;

&lt;p&gt;Slopfix 每周 1 万美元帮人清理 AI 代码，说明这已经不是个别现象了——&lt;strong&gt;AI 代码的"售后成本"正在变成一个真实的行业问题。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;与其让 AI 修三轮越修越烂，不如花 30 分钟自己找到根因一次改对。&lt;/p&gt;

&lt;p&gt;你经历过最离谱的"AI 越修越烂"是什么场景？评论区说说。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="发布信息"&gt;发布信息&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;标签：&lt;/strong&gt; &lt;code&gt;前端&lt;/code&gt; &lt;code&gt;AI编程&lt;/code&gt; &lt;code&gt;JavaScript&lt;/code&gt; &lt;code&gt;代码质量&lt;/code&gt; &lt;code&gt;程序员&lt;/code&gt; &lt;code&gt;debug&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;话题：&lt;/strong&gt; 每日精选文章 / 日新计划&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;分类：&lt;/strong&gt; 前端&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;摘要：&lt;/strong&gt; Shiplight 数据显示 AI 代码多 1.7 倍 Bug，逻辑错误多 75%。上周 Slopfix 团队靠清理 AI 代码每周收费 1 万美元上了 HN 热帖。本文整理了 5 种越让 AI 修就越烂的 bug 类型——CSS 级联、跨组件状态、环境差异、依赖冲突、异步时序——附"该自己修还是让 AI 修"速查表。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Mon, 13 Jul 2026 21:30:18 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7744</link>
      <guid>http://beta.w2solo.com/topics/7744</guid>
    </item>
    <item>
      <title>我用 AI 写代码快了 3 倍——然后同事的 Code Review 全变成了我的</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;这不是一个人的故事。LeadDev 上周发了一篇文章叫"AI productivity is burning out your best engineers"，Reddit 上"谁还在帮初级开发者擦屁股"的帖子 24 小时冲上热榜。AI 在团队里制造的矛盾，比它提升的效率还大。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="你以为 AI 提升的是整个团队的效率"&gt;你以为 AI 提升的是整个团队的效率&lt;/h2&gt;
&lt;p&gt;团队开始用 AI 编程之后，理论上每个人都应该更快。&lt;/p&gt;

&lt;p&gt;现实是：&lt;strong&gt;快的人变得更快，慢的人没变快——但所有人都在产出更多代码。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这意味着什么？&lt;strong&gt;需要 Review 的代码量暴增了，但有能力 Review 的人没变多。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;一份行业数据：2024 年全球 41% 的代码由 AI 生成，相当于 2560 亿行。到 2026 年这个比例只会更高。82% 的开发者在使用 AI 编程工具。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;代码在以 3 倍速度产出，但 Code Review 的速度还是人脑速度。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;瓶颈转移了。&lt;/p&gt;
&lt;h2 id=""&gt;"隐形验证者"——全组最累的人&lt;/h2&gt;
&lt;p&gt;LeadDev 上周的文章给这类人起了个名字：&lt;strong&gt;Invisible Validator&lt;/strong&gt;（隐形验证者）。&lt;/p&gt;

&lt;p&gt;特征：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;团队里 AI 用得最熟的那个人&lt;/li&gt;
&lt;li&gt;写代码最快，提 PR 也最快&lt;/li&gt;
&lt;li&gt;因为"你最懂"，所有复杂的 Code Review 都默认分给了他&lt;/li&gt;
&lt;li&gt;因为"你最快"，其他人没做完的需求也默认排到了他身上&lt;/li&gt;
&lt;li&gt;因为"你用 AI 写的，那你帮忙看看其他人用 AI 写的"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;他的产出翻了 3 倍，他的工作量翻了 5 倍。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;更扎心的是——这些额外的 Review 工作&lt;strong&gt;在任何管理看板上都看不到&lt;/strong&gt;。你帮 5 个人 Review 了 20 个 PR，Dashboard 上显示的是"5 个人完成了 20 个需求"，不是"你审查了 20 个需求的每一行代码"。&lt;/p&gt;

&lt;p&gt;功劳是团队的，疲劳是你的。&lt;/p&gt;
&lt;h2 id="Reddit 上的真实声音"&gt;Reddit 上的真实声音&lt;/h2&gt;
&lt;p&gt;r/ExperiencedDevs 上一个几小时前的热帖标题直接就是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Anyone Else Sick of Getting Flooded with Slop from Junior Devs?"&lt;/strong&gt;
（有没有人也受够了被初级开发者的垃圾代码淹没？）&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;里面的讨论指向同一个问题：&lt;strong&gt;初级开发者用 AI 生成大量"看起来能跑"的代码，但资深开发者需要花更多时间去 Review、去找隐藏的问题。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;另一个帖子里有人写：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"AI has removed the majority of the 15% of my job that I found most satisfying."
（AI 把我工作中最满足的那 15% 拿走了。）&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;他说的是&lt;strong&gt;那种亲手写完一段精巧代码的成就感&lt;/strong&gt;——现在 AI 三秒就写完了，他的工作变成了审查 AI 写的代码、修 AI 留下的坑。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;从"写代码的人"变成"检查 AI 写的代码的人"。角色变了，但没人调整过预期。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="6000 人调查：团队正在被 AI 撕成两半"&gt;6000 人调查：团队正在被 AI 撕成两半&lt;/h2&gt;
&lt;p&gt;Lenny's Newsletter 引用了一份 6000 名技术从业者的调查，结论是：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"The workforce is bifurcating."&lt;/strong&gt;
（劳动力正在两极分化。）&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;一半人觉得 AI 赋予了超能力——效率飞升，工作更有趣。&lt;/p&gt;

&lt;p&gt;另一半人觉得自己在被淘汰——AI 做了他们的活，但他们既不会用 AI，也没有被重新分配到更有价值的事情上。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;但文章里最扎心的一句话是：&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The fastest way to end up with resentment on your team is to pocket the productivity and turn saved time into more work for them."
（让团队产生怨恨的最快方式，就是把 AI 节省的时间全部变成更多的工作量。）&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这正是大多数团队正在做的事：&lt;strong&gt;AI 让你快了 3 倍，那就给你 3 倍的需求。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;效率提升被管理层全部吃掉了。你没有更轻松，你只是在相同的时间里做了更多的事——直到你崩溃。&lt;/p&gt;
&lt;h2 id="为什么"&gt;为什么"最快的人"最先想走&lt;/h2&gt;
&lt;p&gt;把上面的现象串起来，逻辑就清楚了：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第一步：&lt;/strong&gt; 团队开始用 AI，你是最先适应的那个人。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第二步：&lt;/strong&gt; 你的产出翻了 3 倍，领导很满意，给你分更多需求。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第三步：&lt;/strong&gt; 其他人也开始用 AI 写代码，但质量参差不齐。你被要求帮忙 Review。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第四步：&lt;/strong&gt; 你变成了全组的"AI 代码质检员"——别人写的代码你要看，你自己还有 3 倍的需求要做。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第五步：&lt;/strong&gt; 年中评审，所有人的产出都上去了（因为你帮他们兜底了），你的"成长"反而不突出——因为你做的事在看板上看不见。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第六步：&lt;/strong&gt; 你开始想，是不是该换个组了。&lt;/p&gt;

&lt;p&gt;这不是假设。Stack Overflow 博客今年 3 月发了一篇文章叫"AI is becoming a second brain at the expense of your first one"——&lt;strong&gt;AI 在成为你的第二大脑的同时，正在消耗你的第一大脑。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="你的团队踩了几个"&gt;你的团队踩了几个&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;Review 集中到 1-2 个人身上&lt;/td&gt;
&lt;td&gt;因为"他们最懂"&lt;/td&gt;
&lt;td&gt;⬜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 节省的时间全部变成了新需求&lt;/td&gt;
&lt;td&gt;没有留出学习/重构时间&lt;/td&gt;
&lt;td&gt;⬜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;初级开发产出暴增但 Bug 率也暴增&lt;/td&gt;
&lt;td&gt;AI 代码"能跑"但不健壮&lt;/td&gt;
&lt;td&gt;⬜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;资深开发越来越沉默&lt;/td&gt;
&lt;td&gt;从"带团队"变成了"擦屁股"&lt;/td&gt;
&lt;td&gt;⬜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;效率最高的人反而考虑离职&lt;/td&gt;
&lt;td&gt;功劳被均摊，疲劳没人看到&lt;/td&gt;
&lt;td&gt;⬜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;团队氛围从协作变成了割裂&lt;/td&gt;
&lt;td&gt;会用 AI 的 vs 不会用的&lt;/td&gt;
&lt;td&gt;⬜&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;中 3 个以上，你的团队已经在 AI 效率陷阱里了。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="怎么办"&gt;怎么办&lt;/h2&gt;
&lt;p&gt;这不是"不要用 AI"——AI 确实提升了效率，这是事实。&lt;/p&gt;

&lt;p&gt;问题出在&lt;strong&gt;团队没有调整分工模型&lt;/strong&gt;，还在用 AI 之前的管理方式管理 AI 之后的团队。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;如果你是那个"最快的人"：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;让隐形工作可见&lt;/strong&gt;。Review 数量、Review 发现的问题数、帮其他人解决的阻塞——写进周报、写进 1:1。不自己说出来，没人知道。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;拒绝"因为你最快所以你来"的逻辑&lt;/strong&gt;。所有人都该学会 Review AI 代码，不只是你。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;设定 Review 上限&lt;/strong&gt;。每天最多 Review 3 个 PR，超出的排队。不设上限就是无限兜底。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;如果你是 Tech Lead 或管理者：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;不要把 AI 节省的时间 100% 转化成新需求&lt;/strong&gt;。至少留 20% 给技术债、学习、工具建设。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review 负载均衡&lt;/strong&gt;。GitHub 的 CODEOWNERS 可以按目录轮换，不要让一个人扛所有模块。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;承认"验证"是一种产出&lt;/strong&gt;。写代码是产出，确保代码质量也是产出。别让 Review 变成"义务劳动"。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="最后"&gt;最后&lt;/h2&gt;
&lt;p&gt;AI 让写代码变快了。但写代码从来不是软件开发最难的部分。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;最难的部分是确保代码是对的、是安全的、是可维护的。&lt;/strong&gt; 这些事 AI 做不了，但很多团队假装它能做。&lt;/p&gt;

&lt;p&gt;结果就是——&lt;strong&gt;最有能力确保代码质量的人，变成了最先被耗尽的人。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;你的团队里有没有这样的"隐形验证者"？或者你自己就是？评论区说说。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Sun, 12 Jul 2026 12:15:50 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7729</link>
      <guid>http://beta.w2solo.com/topics/7729</guid>
    </item>
    <item>
      <title>我同时用了 Claude Code、Cursor 和 Codex，说说三个工具的真实差距——2026 年选错工具比不用 AI 更浪费时间</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;这三个工具我都在正式项目里深度用过：Cursor 从 Tab 补全一路用到 Agent 模式；Codex 从 CLI 到 Web 云端并行都跑过完整项目；Claude Code 是我现在每天的主力。网上很多对比文章还停留在"Cursor = 补全神器"的旧印象，或者直接搬 SWE-bench 跑分。这篇不比 Benchmark，只说 2026 年我在真实项目里踩出来的差距。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="先纠正一个过时认知：三个都是 Agent，区别在"&gt;先纠正一个过时认知：三个都是 Agent，区别在"协作姿势"&lt;/h2&gt;
&lt;p&gt;2025 年下半年开始，这三个工具其实都走上了 Agent 化路线，谁也不比谁"低一档"。真正的差别不是能力上限，而是&lt;strong&gt;你和它协作的姿势不一样&lt;/strong&gt;：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;&lt;strong&gt;Cursor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IDE 原生 Agent&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;你在驾驶座，AI 随时切换副驾/自动驾驶&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;终端 Agent + IDE 插件&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;你派活给一个能自己跑完全流程的同事&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Codex&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;云端异步 Agent&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;你下工单给一个并行团队，回头收货&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键变化&lt;/strong&gt;：Cursor 有了 Agent 模式和 Background Agents，Claude Code 出了 VS Code / JetBrains 插件和 Web 版，Codex 有了 CLI 也有了 Web。三个工具的功能边界正在互相渗透，但&lt;strong&gt;核心工作流的哲学没变&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="Cursor：不再只是补全，Agent 模式让它成了"&gt;Cursor：不再只是补全，Agent 模式让它成了"IDE 里的全能选手"&lt;/h2&gt;
&lt;p&gt;如果你还觉得 Cursor 只是 "带 AI 的 VS Code"，那你至少落后了半年。2026 年的 Cursor 已经是四种模式并存：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;&lt;strong&gt;Tab（预测式补全）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;猜你下一步要改哪、要写啥&lt;/td&gt;
&lt;td&gt;日常敲代码，最丝滑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Composer / Inline Edit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;多文件同步修改&lt;/td&gt;
&lt;td&gt;中等范围重构&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agent 模式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;自动读代码、跑命令、改文件、验证&lt;/td&gt;
&lt;td&gt;完整功能开发&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Background Agents&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;云端异步执行任务&lt;/td&gt;
&lt;td&gt;后台批量跑活&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Cursor 现在最爽的场景：&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;体验&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;日常写业务代码&lt;/td&gt;
&lt;td&gt;Tab 补全的准确率业内第一档，几乎不用改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"重写这段逻辑"&lt;/td&gt;
&lt;td&gt;Inline Edit 原地替换，即改即看&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"实现登录模块"&lt;/td&gt;
&lt;td&gt;Agent 模式自己读现有代码、跑 dev server 验证&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调 UI/CSS&lt;/td&gt;
&lt;td&gt;所见即所得，实时预览&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bugbot 自动 review&lt;/td&gt;
&lt;td&gt;PR 提交后自动扫，指出潜在 Bug&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;依旧不适合的场景：&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;纯终端工作流（CI/脚本/服务器）&lt;/td&gt;
&lt;td&gt;它的强项是 IDE 内，脱离编辑器体验骤降&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要跨多个仓库协同&lt;/td&gt;
&lt;td&gt;Workspace 支持不如终端类工具灵活&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;一句话：Cursor 适合"你以写代码为主，需要 AI 从补全到 Agent 全档位随时切换"。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="Claude Code：终端 Agent 的天花板，也有 IDE 插件了"&gt;Claude Code：终端 Agent 的天花板，也有 IDE 插件了&lt;/h2&gt;
&lt;p&gt;Claude Code 的哲学没变——它跑在终端里，把你的整个工作目录当成"工位"，能读代码、跑命令、操作 Git、调 MCP 工具。但 2026 年它多了几张牌：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;新增能力（很多人还不知道）：&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;能力&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Skills&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;可复用的"技能包"，像给 Agent 加插件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subagents&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;派生子 Agent 并行做研究、审查&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hooks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;特定事件自动触发命令（如提交前跑 lint）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Plan Mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;先出方案再动手，避免它一头扎进去&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;跨会话记住你的偏好和项目上下文&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VS Code / JetBrains 插件&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;不想开终端也能用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web 版（claude.ai/code）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;浏览器里跑 Agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fast Mode（Opus 4.6）&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;追求速度时可切换&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;用起来最爽的场景：&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;体验&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"加一个完整功能模块"&lt;/td&gt;
&lt;td&gt;Plan 一次、执行一次，端到端搞定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debug 玄学问题&lt;/td&gt;
&lt;td&gt;会自己翻日志、grep、跑测试复现&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大范围重构（20+ 文件）&lt;/td&gt;
&lt;td&gt;全局理解能力最强&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;复杂 CI/DevOps 脚本&lt;/td&gt;
&lt;td&gt;终端原生优势&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多仓库联动&lt;/td&gt;
&lt;td&gt;直接切换目录操作&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;不适合的场景：&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;每行都想精细控制&lt;/td&gt;
&lt;td&gt;它倾向大块交付，逐行微调不如 Cursor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;纯 UI 视觉微调&lt;/td&gt;
&lt;td&gt;没有实时预览&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;一句话：Claude Code 适合"你说清楚要什么，剩下的从设计到验证全交给它"。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="Codex：云端异步的王者，本地 CLI 也补齐了"&gt;Codex：云端异步的王者，本地 CLI 也补齐了&lt;/h2&gt;
&lt;p&gt;Codex 现在是双形态：&lt;strong&gt;Codex CLI&lt;/strong&gt;（本地终端）+ &lt;strong&gt;Codex Web&lt;/strong&gt;（云端异步）。跑在 GPT-5 Codex 模型上，云端能同时开几十个任务并行。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;最爽的场景：&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;体验&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一次派 10 个任务&lt;/td&gt;
&lt;td&gt;云端并行跑，你只管收 PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大规模体力活（如 TS 迁移）&lt;/td&gt;
&lt;td&gt;拆成 N 个子任务批量丢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR 自动 Review&lt;/td&gt;
&lt;td&gt;挂到仓库上，每个 PR 都过一遍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开会时不想停工&lt;/td&gt;
&lt;td&gt;会前下单，会后收货&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨时区协作&lt;/td&gt;
&lt;td&gt;睡觉时也在干活&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;不适合的场景：&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;原因&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要实时反馈的探索性开发&lt;/td&gt;
&lt;td&gt;异步节奏打断心流&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要你频繁修正方向&lt;/td&gt;
&lt;td&gt;每轮等待成本高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;想看到"思考过程"&lt;/td&gt;
&lt;td&gt;相对黑盒，只看最终结果&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;一句话：Codex 适合"你不想盯着，把任务当工单批量下发"。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="2026 年选择速查表（更新版）"&gt;2026 年选择速查表（更新版）&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;日常敲业务代码&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cursor Tab&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Code IDE 插件&lt;/td&gt;
&lt;td&gt;补全丝滑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中等范围功能开发&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cursor Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;IDE 内闭环&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;大范围重构（20+ 文件）&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cursor Agent&lt;/td&gt;
&lt;td&gt;全局理解&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;复杂 Debug / 排障&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cursor Agent&lt;/td&gt;
&lt;td&gt;终端原生&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调 UI / CSS&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cursor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;所见即所得&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;批量体力活&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Codex Web&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cursor Background&lt;/td&gt;
&lt;td&gt;并行最强&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR 自动 Review&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Codex&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cursor Bugbot&lt;/td&gt;
&lt;td&gt;云端异步&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开会/睡觉时跑任务&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Codex Web&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cursor Background Agent&lt;/td&gt;
&lt;td&gt;真异步&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DevOps / CI 脚本&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;终端为王&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;探索性技术方案&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude Code Plan Mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cursor Agent&lt;/td&gt;
&lt;td&gt;先谋后动&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h2 id="我现在的组合用法（2026 版）"&gt;我现在的组合用法（2026 版）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;没有"用哪个"，只有"什么时候用哪个"。&lt;/strong&gt; 我的日常搭配：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;早上梳理架构 / 出方案&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude Code Plan Mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;想清楚再动手&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;白天写业务代码&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cursor Tab + Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IDE 内最快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;排 Bug / 查日志&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;终端能力全开&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;批量重复任务&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Codex Web&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;丢过去开会&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调样式细节&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cursor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;实时预览&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR Review&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Codex + Cursor Bugbot&lt;/strong&gt; 双保险&lt;/td&gt;
&lt;td&gt;云端异步不打断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;跨仓库操作&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;终端切目录方便&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h2 id="三个常见的选型误区"&gt;三个常见的选型误区&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;误区一：还以为 Cursor 只能补全&lt;/strong&gt;
Cursor Agent 模式和 Background Agents 已经能独立完成中大型任务，只在 IDE 里用它 Tab 是浪费。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;误区二：觉得 Claude Code 就是命令行小工具&lt;/strong&gt;
它现在有 VS Code / JetBrains 插件、Web 版、Skills、Subagents、Hooks——是一整套 Agent 平台。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;误区三：觉得 Codex 慢所以不好用&lt;/strong&gt;
异步不是缺点，是&lt;strong&gt;特性&lt;/strong&gt;。你需要的不是"更快的对话"，而是"下班时代码在跑"。&lt;/p&gt;
&lt;h2 id="最后"&gt;最后&lt;/h2&gt;
&lt;p&gt;2026 年 AI 编程工具的问题早就不是"用不用"，也不是"哪个最强"，而是&lt;strong&gt;你会不会根据场景切换&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;三种最典型的浪费：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;一个工具打天下&lt;/strong&gt;——用 Cursor 做多仓库 DevOps，或者用 Claude Code 调 CSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;只用旧功能&lt;/strong&gt;——买了 Cursor 只按 Tab，装了 Claude Code 只做单文件改动&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;跟风换工具&lt;/strong&gt;——听说 Codex 出新版就把其他都卸了，结果异步不适合自己&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;选工具和选框架一样：不是选"最强的"，是选"和当前任务最匹配的"。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;你现在主力用哪个？评论区聊聊你的搭配方式，尤其想听听有没有把 Cursor Background Agent 和 Codex 一起用的玩法。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Tue, 07 Jul 2026 20:23:47 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7684</link>
      <guid>http://beta.w2solo.com/topics/7684</guid>
    </item>
    <item>
      <title>Godot 全面封杀 Vibe Coding，作为每天用 AI 写代码的人，我反而觉得这是件好事</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;上周 Godot（用户量很大的开源游戏引擎）更新贡献政策：全面禁止 AI 代理直接提交代码，违者永久禁止访问仓库。我每天都在用 Claude Code 写代码，看到这新闻第一反应不是"AI 又被针对了"，而是"这规则说的正是我一直担心的问题"。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="Godot 具体做了什么"&gt;Godot 具体做了什么&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;规则&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;禁止 AI 代理直接提交&lt;/td&gt;
&lt;td&gt;违者永久禁止访问，最严厉的处罚&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;新贡献者门槛&lt;/td&gt;
&lt;td&gt;合并 PR≤3 个的用户有额外限制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;强制人工审核&lt;/td&gt;
&lt;td&gt;任何 PR 必须真人审核批准才能合并&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;原因：&lt;/strong&gt; 大量 AI 生成的低质量 PR 涌入，维护者审查负担暴增，还引发代码质量和责任归属争议——AI 代码出问题算谁的？&lt;/p&gt;
&lt;h2 id="不是个例，是趋势"&gt;不是个例，是趋势&lt;/h2&gt;
&lt;p&gt;开发者圈流行一个新词——&lt;strong&gt;Vibe Slop（氛围垃圾代码）&lt;/strong&gt;：看起来能跑，但没人真正理解、也没人愿意负责的 AI 生成代码。&lt;/p&gt;

&lt;p&gt;维护者的困境：审查人写的代码能判断意图，审查 AI 生成的代码，提交者自己可能都不知道在干什么。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;PR 数量暴增但质量下降，维护成本不降反升。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="我怎么看"&gt;我怎么看&lt;/h2&gt;
&lt;p&gt;Vibe Coding 适合"你自己负责"的场景，不适合"你要说服别人相信"的场景。&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;场景&lt;/th&gt;
&lt;th&gt;是否合适&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自己项目自己维护&lt;/td&gt;
&lt;td&gt;✅ 出问题自己扛&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;给团队提 PR&lt;/td&gt;
&lt;td&gt;⚠️ 同事需要理解你的代码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;给开源项目提 PR&lt;/td&gt;
&lt;td&gt;❌ 把责任甩给维护者&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;一次性脚本&lt;/td&gt;
&lt;td&gt;✅ 低风险&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;Godot 针对的是最后一种极端——用 AI 生成代码，自己不审查就直接提 PR，把"理解代码"的责任甩给别人。&lt;/p&gt;
&lt;h2 id="AI辅助 vs AI甩锅"&gt;AI 辅助 vs AI 甩锅&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;AI 辅助（我依然负责）：&lt;/strong&gt; 生成初稿，逐行看过理解调整，不确定的地方会验证&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI 甩锅（不再负责）：&lt;/strong&gt; AI 生成什么就是什么没 Review，出问题第一反应是"这是 AI 写的"&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Godot 这次本质是强制大家从"甩锅模式"切回"辅助模式"。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="自查清单"&gt;自查清单&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] 提交前，能不能对每一行给出理由？&lt;/li&gt;
&lt;li&gt;[ ] 同事问"为什么这样写"，不查 AI 记录能答上来吗？&lt;/li&gt;
&lt;li&gt;[ ] 真正 Review 过还是看起来能跑就提交了？&lt;/li&gt;
&lt;li&gt;[ ] 出 bug 能独立排查，还是重新问 AI？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;两个以上答不上来，说明正在从"AI 辅助"滑向"AI 甩锅"。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vibe Coding 本身没错，错的是把"我不需要理解"也一起 Vibe 掉了。&lt;/p&gt;

&lt;p&gt;你怎么看这次的政策？会不会逐行 Review AI 代码？评论区聊聊。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;觉得有用？点个赞 👍 让更多人看到&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;关注我，获取更多技术干货。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Mon, 06 Jul 2026 18:58:55 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7676</link>
      <guid>http://beta.w2solo.com/topics/7676</guid>
    </item>
    <item>
      <title>朋友最近面了 3 家公司，发现 2026 年前端面试完全变了——不考手写题了，考的全是这些</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;一个关系不错的前端朋友最近在看机会，前后面了三家公司。他之前准备面试还是老一套——背八股文、刷手写题。结果三家公司没有一家让他手写代码。他跟我吐槽的时候我才意识到，面试的考法已经彻底变了。这篇把他三家公司的面试流程和考察重点整理出来，给最近也在看机会的人一个参考。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="第一家：中型 SaaS 公司，前端 4 人团队"&gt;第一家：中型 SaaS 公司，前端 4 人团队&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;面试流程：&lt;/strong&gt; 一面技术面 1 小时 + 二面技术 Leader 1 小时 + 三面 HR&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;没考什么：&lt;/strong&gt; 手写 Promise、闭包、原型链、虚拟 DOM 实现——这些一题都没出。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;考了什么：&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;&lt;strong&gt;AI 工具使用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"你日常用什么 AI 工具写代码？用 Claude Code 还是 Cursor？能不能现场演示一下你怎么用它做一个需求？"&lt;/td&gt;
&lt;td&gt;不是问"会不会用"，是要看&lt;strong&gt;工作流&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code Review 能力&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;给了一段 AI 生成的代码，让他找问题。代码功能正确但有性能问题和冗余&lt;/td&gt;
&lt;td&gt;考的是能不能&lt;strong&gt;判断 AI 写的代码好不好&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;架构设计&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"如果让你从零搭一个后台管理系统，你会怎么选型？状态管理怎么做？"&lt;/td&gt;
&lt;td&gt;考全局思维，不是考你会不会写某个组件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;沟通和协作&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"产品经理给了一个不合理的需求，你怎么处理？"&lt;/td&gt;
&lt;td&gt;软技能，以前技术面很少问这个&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;他的感受：&lt;/strong&gt; 面试官全程没打开编辑器让他写代码。整个面试更像是在聊"你平时怎么干活的"。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="第二家：电商公司，前端 15+ 人"&gt;第二家：电商公司，前端 15+ 人&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;面试流程：&lt;/strong&gt; 一面 coding test（但不是传统手写）+ 二面系统设计 + 三面总监面&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;一面很有意思：&lt;/strong&gt; 面试官打开一个 CodeSandbox，里面有一个半成品页面——商品列表已经渲染出来了，但搜索、筛选和分页都没有。让他现场加功能，可以用任何工具，包括 AI。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;和传统手写题的区别：&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;传统手写&lt;/th&gt;
&lt;th&gt;这次的做法&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;白板上写 Promise 实现&lt;/td&gt;
&lt;td&gt;在真实项目里加功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;考你记不记得语法&lt;/td&gt;
&lt;td&gt;考你解决问题的效率和质量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不能用任何工具&lt;/td&gt;
&lt;td&gt;允许用 AI、Google、任何东西&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;写完就结束&lt;/td&gt;
&lt;td&gt;写完之后追问"为什么这样设计"&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;他用 Claude Code 生成了搜索和筛选的基础代码，然后自己改了防抖逻辑和分页的边界处理。整个过程大概 25 分钟。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;重点来了：&lt;/strong&gt; 写完之后面试官花了 20 分钟问他代码的设计决策——"为什么用 useMemo 而不是 useCallback"、"分页状态放在 URL 里还是组件 state 里，为什么"、"如果数据量从 1000 变成 10 万，你的方案会有什么问题"。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;面试官不关心你写得多快，关心的是你能不能解释清楚每一个选择背后的原因。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="第三家：AI 创业公司，全栈方向"&gt;第三家：AI 创业公司，全栈方向&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;面试流程：&lt;/strong&gt; 一面技术 + 二面 CTO + 三面 CEO&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;最直接的一家。&lt;/strong&gt; CTO 开场就说："我们团队 5 个人，80% 的代码是 AI 写的。我不需要你写代码很快，我需要你能在 AI 写完之后把控质量。"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;考察重点完全不同：&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;考察项&lt;/th&gt;
&lt;th&gt;具体方式&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debug 能力&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;给了一段有 bug 的 React 组件，让他排查。bug 藏得很深——是 useEffect 依赖数组漏了一个变量导致的闭包陷阱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;代码评审&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;给了一个 PR，让他做 Review。代码是 AI 生成的，有 3 个问题让他找&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;系统思维&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"这个功能现在能跑，但如果并发量从 100 涨到 10000，哪里会先出问题？"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI 协作&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"你怎么给 AI 写 Prompt 让它生成的代码更可控？有没有自己的 Prompt 模板？"&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;CTO 的原话：&lt;/strong&gt; "我们不缺能写代码的人，AI 能写。我们缺的是能判断代码对不对、架构合不合理的人。"&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="三家面试的共同点"&gt;三家面试的共同点&lt;/h2&gt;
&lt;p&gt;听他讲完三家的经历，我总结了 2026 年前端面试的三个明显变化：&lt;/p&gt;
&lt;h3 id="变化 1：手写八股文基本消失了"&gt;变化 1：手写八股文基本消失了&lt;/h3&gt;
&lt;p&gt;三家公司没有一家让他手写 Promise、闭包、原型链。不是说这些知识不重要，而是面试官觉得"你知不知道这些"已经不是区分好坏的标准了——因为 AI 能写，背八股文的意义被稀释了。&lt;/p&gt;
&lt;h3 id="变化 2：AI 使用能力变成了显性考察项"&gt;变化 2：AI 使用能力变成了显性考察项&lt;/h3&gt;
&lt;p&gt;三家公司都问了"你怎么用 AI 工具"，而且不是随便聊聊，是&lt;strong&gt;正式考察项&lt;/strong&gt;。第一家让现场演示，第二家允许面试中使用 AI，第三家直接问 Prompt 模板。&lt;/p&gt;
&lt;h3 id="变化 3："&gt;变化 3："判断力"取代了"执行力"&lt;/h3&gt;
&lt;p&gt;以前面试考的是"你能不能写出来"——执行力。现在考的是"AI 写出来之后，你能不能判断对不对、好不好、该不该这样设计"——判断力。&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;以前的核心考点&lt;/th&gt;
&lt;th&gt;现在的核心考点&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;你能不能手写出来&lt;/td&gt;
&lt;td&gt;AI 写的代码你能不能评审&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;你记不记得语法&lt;/td&gt;
&lt;td&gt;你能不能解释设计决策&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;执行力&lt;/td&gt;
&lt;td&gt;判断力&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="对准备面试的建议"&gt;对准备面试的建议&lt;/h2&gt;
&lt;p&gt;基于他这三次面试经验，如果你最近也在看机会，建议调整一下准备方向：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;以前花时间&lt;/th&gt;
&lt;th&gt;现在应该花时间&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;背八股文（闭包、原型链）&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;练习评审 AI 生成的代码&lt;/strong&gt;——给 Claude Code 一个需求，让它写完后自己找问题&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;刷 LeetCode&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;练习系统设计&lt;/strong&gt;——画架构图、做技术选型、评估扩展性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;记 API 语法&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;练习"解释设计决策"&lt;/strong&gt;——每写一个组件都想一下"为什么这样设计"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;准备手写 Promise&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;准备 AI 工具使用经验&lt;/strong&gt;——整理你的工作流、Prompt 模板、常用技巧&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="最后"&gt;最后&lt;/h2&gt;
&lt;p&gt;2026 年的前端面试，不再考你"能不能写"，而是考你"AI 写完之后你能不能判断"。八股文不是不需要会——你还是需要理解原理才能做出正确的判断。但面试的考核方式变了，你的准备方式也应该变。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;你最近面试有没有发现类似的变化？还是说你面的公司还在考手写题？&lt;/strong&gt; 评论区说说你的经历，想看看不同公司之间差异有多大。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Sun, 05 Jul 2026 22:26:26 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7664</link>
      <guid>http://beta.w2solo.com/topics/7664</guid>
    </item>
    <item>
      <title>别再每次都 Google 了：我整理了前端日常最常踩的 10 个 Git 坑，附速查表</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;用了三年 Git，我依然每周至少 Google 两次 Git 命令。不是不会用，是有些操作&lt;strong&gt;一个月才遇到一次&lt;/strong&gt;，每次都记不住。上周 merge 出了冲突，我又条件反射打开浏览器搜"git 怎么撤销 merge"——搜完之后我决定，把这些反复 Google 的操作整理成一篇速查表，以后直接查这篇。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="坑 1：commit 了才发现写错了提交信息"&gt;坑 1：commit 了才发现写错了提交信息&lt;/h2&gt;
&lt;p&gt;这是我踩得最多的——手速太快，&lt;code&gt;git commit -m "fix"&lt;/code&gt; 回车完才发现，应该写清楚改了什么。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 改最近一次的 commit 信息（还没 push 的情况下）&lt;/span&gt;
git commit &lt;span class="nt"&gt;--amend&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"fix: 修复登录页表单验证逻辑"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：如果已经 push 了，amend 会改变 commit hash，再 push 需要 &lt;code&gt;--force&lt;/code&gt;。&lt;/strong&gt; 如果是团队协作的分支，不要 force push，改用新 commit。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="坑 2：改了半天发现改错分支了"&gt;坑 2：改了半天发现改错分支了&lt;/h2&gt;
&lt;p&gt;写了一小时代码，突然发现自己在 &lt;code&gt;main&lt;/code&gt; 上改的，不是 &lt;code&gt;feature&lt;/code&gt; 分支。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 把当前改动暂存起来&lt;/span&gt;
git stash

&lt;span class="c"&gt;# 切到正确的分支&lt;/span&gt;
git checkout feature/login

&lt;span class="c"&gt;# 把暂存的改动拿出来&lt;/span&gt;
git stash pop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;git stash&lt;/code&gt; 就像一个"临时口袋"，把没提交的改动先装进去，换完分支再掏出来。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="坑 3：merge 冲突了不知道怎么办"&gt;坑 3：merge 冲突了不知道怎么办&lt;/h2&gt;
&lt;p&gt;这个坑不是不知道怎么解决冲突，而是&lt;strong&gt;解决到一半不想解决了，想撤回去&lt;/strong&gt;。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 取消这次 merge，回到 merge 之前的状态&lt;/span&gt;
git merge &lt;span class="nt"&gt;--abort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果已经解决完冲突但还没 commit，想重新来过：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 放弃所有冲突解决的改动&lt;/span&gt;
git checkout &lt;span class="nt"&gt;--conflict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;merge &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="坑 4：add 了不该 add 的文件"&gt;坑 4：add 了不该 add 的文件&lt;/h2&gt;
&lt;p&gt;手滑 &lt;code&gt;git add .&lt;/code&gt; 把 &lt;code&gt;.env&lt;/code&gt; 或 &lt;code&gt;node_modules&lt;/code&gt; 加进去了。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 从暂存区移除，但保留本地文件&lt;/span&gt;
git reset HEAD .env

&lt;span class="c"&gt;# 如果已经 commit 了，需要从历史中删除&lt;/span&gt;
git &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;--cached&lt;/span&gt; .env
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: 移除误提交的 .env 文件"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;预防方法&lt;/strong&gt;：项目第一天就把 &lt;code&gt;.gitignore&lt;/code&gt; 配好。别问我怎么知道的。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="坑 5：想撤销最近一次 commit（但保留代码）"&gt;坑 5：想撤销最近一次 commit（但保留代码）&lt;/h2&gt;
&lt;p&gt;commit 之后发现漏了一个文件，或者想把两个 commit 合成一个。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 撤销 commit，但代码改动保留在暂存区&lt;/span&gt;
git reset &lt;span class="nt"&gt;--soft&lt;/span&gt; HEAD~1

&lt;span class="c"&gt;# 撤销 commit，代码改动保留但不在暂存区&lt;/span&gt;
git reset &lt;span class="nt"&gt;--mixed&lt;/span&gt; HEAD~1

&lt;span class="c"&gt;# ⚠️ 撤销 commit，代码改动也丢弃（慎用！）&lt;/span&gt;
git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; HEAD~1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;三种 reset 的区别，一张图：&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;模式&lt;/th&gt;
&lt;th&gt;commit&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;tr&gt;
&lt;td&gt;&lt;code&gt;--soft&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;撤销&lt;/td&gt;
&lt;td&gt;保留&lt;/td&gt;
&lt;td&gt;保留&lt;/td&gt;
&lt;td&gt;想重新组织 commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--mixed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;撤销&lt;/td&gt;
&lt;td&gt;清空&lt;/td&gt;
&lt;td&gt;保留&lt;/td&gt;
&lt;td&gt;想重新选择要 add 的文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--hard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;撤销&lt;/td&gt;
&lt;td&gt;清空&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;清空&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;这次改动彻底不要了&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="坑 6：不小心 git reset --hard 了，代码全没了"&gt;坑 6：不小心 git reset --hard 了，代码全没了&lt;/h2&gt;
&lt;p&gt;每个人职业生涯里至少 &lt;code&gt;--hard&lt;/code&gt; 错一次。别慌：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 查看所有操作记录（包括已经"删掉"的 commit）&lt;/span&gt;
git reflog

&lt;span class="c"&gt;# 找到你想恢复的那个 commit hash，然后&lt;/span&gt;
git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; abc1234
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;reflog&lt;/code&gt; 是 Git 的"后悔药"，它记录了你所有的 HEAD 移动历史，包括 reset 掉的。&lt;strong&gt;只要没 &lt;code&gt;git gc&lt;/code&gt;，30 天内都能找回来。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="坑 7：想看某个文件之前的版本"&gt;坑 7：想看某个文件之前的版本&lt;/h2&gt;
&lt;p&gt;不记得这个文件上周长什么样了，想对比一下。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 看某个文件在某次 commit 时的内容&lt;/span&gt;
git show abc1234:src/utils/auth.js

&lt;span class="c"&gt;# 对比当前版本和上一次 commit 的差异&lt;/span&gt;
git diff HEAD~1 &lt;span class="nt"&gt;--&lt;/span&gt; src/utils/auth.js

&lt;span class="c"&gt;# 对比两个分支之间某个文件的差异&lt;/span&gt;
git diff main..feature/login &lt;span class="nt"&gt;--&lt;/span&gt; src/utils/auth.js
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="坑 8：想从另一个分支只拿一个 commit"&gt;坑 8：想从另一个分支只拿一个 commit&lt;/h2&gt;
&lt;p&gt;不想 merge 整个分支，只想拿其中一个 commit 的改动过来。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# cherry-pick：把指定 commit 的改动应用到当前分支&lt;/span&gt;
git cherry-pick abc1234
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;常见场景&lt;/strong&gt;：在 &lt;code&gt;dev&lt;/code&gt; 上修了一个 bug，想把这个修复也带到 &lt;code&gt;release&lt;/code&gt; 分支，但不想把 &lt;code&gt;dev&lt;/code&gt; 上其他未完成的功能也带过去。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="坑 9：本地分支太多，不知道哪些可以删了"&gt;坑 9：本地分支太多，不知道哪些可以删了&lt;/h2&gt;
&lt;p&gt;开了一堆 &lt;code&gt;feature/xxx&lt;/code&gt; 分支，合并完忘了删，&lt;code&gt;git branch&lt;/code&gt; 列出来一屏都放不下。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 查看哪些分支已经合并到 main（可以安全删除）&lt;/span&gt;
git branch &lt;span class="nt"&gt;--merged&lt;/span&gt; main

&lt;span class="c"&gt;# 删除已合并的本地分支&lt;/span&gt;
git branch &lt;span class="nt"&gt;--merged&lt;/span&gt; main | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; | xargs git branch &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# 清理远程已删除但本地还有引用的分支&lt;/span&gt;
git fetch &lt;span class="nt"&gt;--prune&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="坑 10：push 被拒绝，提示 "&gt;坑 10：push 被拒绝，提示 "non-fast-forward"&lt;/h2&gt;
&lt;p&gt;同事在你之前 push 了，你的本地落后于远程。&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 方案一：先拉再推（会产生一个 merge commit）&lt;/span&gt;
git pull
git push

&lt;span class="c"&gt;# 方案二：rebase 后推（保持线性历史，更干净）&lt;/span&gt;
git pull &lt;span class="nt"&gt;--rebase&lt;/span&gt;
git push
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;团队约定用哪种就用哪种。&lt;/strong&gt; 我个人偏好 &lt;code&gt;--rebase&lt;/code&gt;，因为 git log 更干净。但有些团队觉得 merge commit 能清晰看到"谁什么时候合并了什么"。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="速查表：前端 Git 日常操作一览"&gt;速查表：前端 Git 日常操作一览&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;改 commit 信息&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git commit --amend -m "新信息"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;未 push 时用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;改错分支了&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git stash&lt;/code&gt; → 切分支 → &lt;code&gt;git stash pop&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;临时口袋&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;取消 merge&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git merge --abort&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;冲突解决到一半想撤回&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;撤销 add&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git reset HEAD 文件名&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;从暂存区移除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;撤销 commit 保留代码&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git reset --soft HEAD~1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;最常用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;找回 hard 删掉的代码&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git reflog&lt;/code&gt; → &lt;code&gt;git reset --hard hash&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;后悔药&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;看文件历史版本&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git show hash:文件路径&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;只拿一个 commit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git cherry-pick hash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不 merge 整个分支&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删已合并的分支&lt;/td&gt;
&lt;td&gt;`git branch --merged main \&lt;/td&gt;
&lt;td&gt;xargs git branch -d`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;push 被拒绝&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;git pull --rebase&lt;/code&gt; → &lt;code&gt;git push&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;保持线性历史&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="一个实用建议"&gt;一个实用建议&lt;/h2&gt;
&lt;p&gt;如果你和我一样经常忘 Git 命令，在 &lt;code&gt;~/.bashrc&lt;/code&gt; 或 &lt;code&gt;~/.zshrc&lt;/code&gt; 里加几个 alias：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;gs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"git status"&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;gl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"git log --oneline -10"&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;gco&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"git checkout"&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;gcm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"git commit -m"&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;gst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"git stash"&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;gsp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"git stash pop"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不用记命令了，记缩写就行。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="最后"&gt;最后&lt;/h2&gt;
&lt;p&gt;Git 这东西，用了十年的人也经常 Google。不是因为难，是因为有些操作真的一个月才用一次。这篇速查表我自己也会收藏，以后忘了直接查。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;你最常 Google 的 Git 命令是什么？&lt;/strong&gt; 评论区补充，我加到速查表里。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Fri, 03 Jul 2026 15:13:54 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7651</link>
      <guid>http://beta.w2solo.com/topics/7651</guid>
    </item>
    <item>
      <title>我用 Codex 重写了同事维护三年的代码，他没说谢谢——而是找了领导</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;我以为我在帮忙，他以为我在"打脸"。AI 让重构变得太容易了，但它解决不了的，恰恰是最难的那个部分——人。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr&gt;
&lt;h2 id="事情是这样开始的"&gt;事情是这样开始的&lt;/h2&gt;
&lt;p&gt;我们组有一个内部管理后台，核心模块是一个表单引擎。这个模块是同事小张三年前搭的，从简单表单慢慢迭代到支持上百个字段、动态联动、多级嵌套的复杂系统。&lt;/p&gt;

&lt;p&gt;三年下来，这个模块膨胀到了大几千行——一个文件。没有拆分，没有类型定义，变量命名从 &lt;code&gt;data1&lt;/code&gt; 到 &lt;code&gt;tempArr2&lt;/code&gt; 应有尽有。&lt;/p&gt;

&lt;p&gt;我不是在黑小张。三年前的代码放到三年后看，谁的都一样。问题是，最近产品要加新的联动规则，我负责这个需求，改了两天——每次改完一个地方，另一个地方就莫名其妙地坏掉。&lt;/p&gt;
&lt;h2 id="一个周末，我"&gt;一个周末，我"手贱"了&lt;/h2&gt;
&lt;p&gt;周五下班前有点烦躁，打开 Codex，把这个文件丢了进去："帮我重构一下，拆成合理的模块结构，加上 TypeScript 类型。"&lt;/p&gt;

&lt;p&gt;大约二十分钟，Codex 给出了完整的重构方案：一个大文件拆成 6 个模块，职责清晰，类型完善，连测试框架都搭好了。&lt;/p&gt;

&lt;p&gt;我花了大半个周末逐行审查、补边界情况、跑通测试，然后在新结构上实现了产品要的联动规则——比原来轻松太多。&lt;/p&gt;

&lt;p&gt;周一早上，信心满满地提了一个大 PR。&lt;/p&gt;
&lt;h2 id="小张的反应出乎意料"&gt;小张的反应出乎意料&lt;/h2&gt;
&lt;p&gt;我以为他会说"这下好维护多了"。&lt;/p&gt;

&lt;p&gt;他看完 PR 沉默了大概十分钟，然后走到我工位旁边：&lt;strong&gt;"你是不是觉得我写的代码很烂？"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我赶紧解释是因为新需求加不进去。但他的表情告诉我，解释是苍白的。&lt;/p&gt;

&lt;p&gt;下午领导找我聊了一下，语气很明确："重构是好事，但你事先应该和小张对齐。"&lt;/p&gt;

&lt;p&gt;小张跟领导说了三个点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;没有提前沟通&lt;/strong&gt;：周一早上突然收到一个改了几千行的 PR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;感觉被否定&lt;/strong&gt;：三年的工作被 AI 用二十分钟"替换"了&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;维护成本转嫁&lt;/strong&gt;：重构后的代码他没参与设计，但以后还是他维护&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;每一点都有道理。&lt;/p&gt;
&lt;h2 id="我犯了三个错误"&gt;我犯了三个错误&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;第一，把"技术上正确"等同于"做法正确"。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;重构后的代码确实更好。但在团队里，代码不只是技术产物，还是&lt;strong&gt;人的产物&lt;/strong&gt;。每一行背后都有人的时间和判断。你"优化"它的方式，暗示着你对这些投入的评价。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第二，低估了 AI 带来的"效率暴力"。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;手动重构需要两周，同事不会觉得被"替代"——因为你付出了对等的时间。但 AI 让这件事变成了一个周末，这个速度本身就带着隐含的否定：&lt;strong&gt;"你三年写的东西，AI 二十分钟就能写得更好。"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;即使我没有这个意思，观感就是这样。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;第三，跳过了最重要的步骤——达成共识。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;重构这种大范围改动，动手前至少应该：和原作者讨论方案、让他参与设计、分阶段提交。我因为"AI 让重构太容易了"就全部跳过了。&lt;/p&gt;
&lt;h2 id="AI 时代的协作陷阱"&gt;AI 时代的协作陷阱&lt;/h2&gt;
&lt;p&gt;这件事揭示了一个深层问题：&lt;strong&gt;AI 改变了团队协作中一个不成文的契约。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;过去，重构成本是人人平等的。花两周改别人的模块，这份投入本身就是"尊重"。但 AI 把成本降到接近零。当改变别人代码的成本趋近于零，&lt;strong&gt;"要不要改"和"动手改"之间的心理门槛就消失了。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我总结了一份协作备忘录：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;重构别人的模块&lt;/td&gt;
&lt;td&gt;先讨论方案&lt;/td&gt;
&lt;td&gt;AI 改完直接提 PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 生成大量改动&lt;/td&gt;
&lt;td&gt;拆成小 PR 逐步合入&lt;/td&gt;
&lt;td&gt;一次性巨型 PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;发现别人代码问题&lt;/td&gt;
&lt;td&gt;Review 中指出&lt;/td&gt;
&lt;td&gt;自己 fork 用 AI 重写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 方案和同事冲突&lt;/td&gt;
&lt;td&gt;作为另一种思路讨论&lt;/td&gt;
&lt;td&gt;拿 AI 方案否定同事&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;&lt;h2 id="后来怎么样了"&gt;后来怎么样了&lt;/h2&gt;
&lt;p&gt;我找小张单独聊了一次，承认做法不对。他也说模块确实该重构了，只是"被 AI 二十分钟替掉三年代码"需要消化一下。&lt;/p&gt;

&lt;p&gt;最后方案：关掉大 PR，重新开几个小 PR，每个只改一个模块，小张全程参与 Review。整个过程花了一周多——比 AI 二十分钟慢了很多，但团队关系没受损。&lt;/p&gt;

&lt;p&gt;有时候，&lt;strong&gt;慢一点才是真的快。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="写在最后"&gt;写在最后&lt;/h2&gt;
&lt;p&gt;AI 让写代码、重构、测试都变容易了。但它解决不了最难的部分——沟通、共识、尊重、信任。&lt;/p&gt;

&lt;p&gt;技术能力再强，搞不定"人"的部分，在团队里就是定时炸弹。AI 放大了个体的执行力，&lt;strong&gt;也放大了不沟通的后果。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;你有没有遇到过类似情况——好心办了"坏事"？欢迎留言聊聊。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Thu, 02 Jul 2026 15:01:21 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7633</link>
      <guid>http://beta.w2solo.com/topics/7633</guid>
    </item>
    <item>
      <title>我手写了一个 EventEmitter，面试官追问了 6 个问题——第 4 个我没答上来</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;发布订阅模式是前端面试的高频手写题，但大多数人只会写一个基础版的 &lt;code&gt;on&lt;/code&gt; / &lt;code&gt;emit&lt;/code&gt;。面试官真正想考的不是你会不会写，而是你写完之后能不能接住追问。我上次手写完 EventEmitter 后被连续追问了 6 个问题，第 4 个关于内存泄漏的问题当场没答上来。这篇文章把完整实现和 6 个追问全部写出来，下次遇到直接拿满分。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="先用 30 行写一个能用的版本"&gt;先用 30 行写一个能用的版本&lt;/h2&gt;
&lt;p&gt;面试的时候不要上来就写"完美版"，先快速写一个核心能跑的版本，再根据面试官的追问逐步完善。&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;EventEmitter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;30 行，4 个核心方法：&lt;code&gt;on&lt;/code&gt;（订阅）、&lt;code&gt;off&lt;/code&gt;（取消）、&lt;code&gt;emit&lt;/code&gt;（触发）、&lt;code&gt;once&lt;/code&gt;（只触发一次）。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么用 &lt;code&gt;Map&lt;/code&gt; + &lt;code&gt;Set&lt;/code&gt; 而不是普通对象 + 数组？&lt;/strong&gt;&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;对象 + 数组&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;需手动判断&lt;/td&gt;
&lt;td&gt;删除要 &lt;code&gt;splice&lt;/code&gt;，性能差&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Map + Set&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;O(1)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;自动去重&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;delete&lt;/code&gt; 直接删，不用遍历&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;写完这个版本，面试官会满意吗？不会。追问才刚开始。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="追问 1："&gt;追问 1："链式调用怎么实现的？"&lt;/h2&gt;
&lt;p&gt;你可能注意到了，每个方法最后都 &lt;code&gt;return this&lt;/code&gt;。这不是多余的——它让你可以这样写：&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;EventEmitter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;emitter&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; 登录了`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; 登出了`&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;firstVisit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;首次访问&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;面试加分点：&lt;/strong&gt; 提一句"jQuery、RxJS、Promise 都用了这个模式——叫 Fluent Interface（流式接口）"。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="追问 2："&gt;追问 2："once 的实现原理是什么？为什么要用 wrapper？"&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;once&lt;/code&gt; 的核心是用一个 &lt;code&gt;wrapper&lt;/code&gt; 函数把原始 &lt;code&gt;listener&lt;/code&gt; 包了一层：&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// 先执行原始回调&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 再把 wrapper 从事件列表中删掉&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// 注册的是 wrapper，不是 listener&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么不能直接 &lt;code&gt;this.off(event, listener)&lt;/code&gt;？&lt;/strong&gt; 因为你注册的是 &lt;code&gt;wrapper&lt;/code&gt;，事件列表里存的也是 &lt;code&gt;wrapper&lt;/code&gt;。如果你 &lt;code&gt;off(listener)&lt;/code&gt;，找不到匹配项，删不掉。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;追问陷阱：&lt;/strong&gt; 面试官可能会问"如果我在 &lt;code&gt;once&lt;/code&gt; 回调执行之前就手动 &lt;code&gt;off&lt;/code&gt; 这个 listener，会发生什么？"&lt;/p&gt;

&lt;p&gt;答：&lt;code&gt;off(event, listener)&lt;/code&gt; 找不到（因为注册的是 wrapper），不会删除。要解决这个问题，需要在 wrapper 上挂一个原始引用：&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 保存原始引用&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_original&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个细节能答上来，面试官会认为你对设计模式的理解不是停留在"背代码"层面。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="追问 3："&gt;追问 3："emit 的时候如果 listener 抛了异常，后面的 listener 还会执行吗？"&lt;/h2&gt;
&lt;p&gt;当前实现：&lt;strong&gt;不会。&lt;/strong&gt; 因为 &lt;code&gt;forEach&lt;/code&gt; 中某个 listener 抛异常后，整个循环就中断了。&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;boom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;我不会执行&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 第二个 listener 被跳过了&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;解决方案：&lt;/strong&gt; 每个 listener 独立 try-catch。&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Event "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" listener error:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;延伸：&lt;/strong&gt; Node.js 的 EventEmitter 不会帮你 catch，它会直接抛出。如果没有监听 &lt;code&gt;error&lt;/code&gt; 事件，进程会崩。这就是为什么 Node.js 里经常看到 &lt;code&gt;emitter.on('error', handler)&lt;/code&gt; 这种写法——它是兜底用的。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="追问 4："&gt;追问 4："如果忘了 off，会不会内存泄漏？"&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;会。&lt;/strong&gt; 这是我当时没答上来的问题。&lt;/p&gt;

&lt;p&gt;场景：一个 React 组件在 &lt;code&gt;useEffect&lt;/code&gt; 里注册了事件，但组件卸载时没有 &lt;code&gt;off&lt;/code&gt;。&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ 内存泄漏&lt;/span&gt;
&lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleUpdate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// 组件卸载了，但 handleUpdate 还在 emitter 的事件列表里&lt;/span&gt;
  &lt;span class="c1"&gt;// emitter 持有 handleUpdate 的引用 → handleUpdate 持有组件闭包的引用 → 组件无法被 GC&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ 正确写法&lt;/span&gt;
&lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleUpdate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleUpdate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;面试加分答法：&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"EventEmitter 的内存泄漏本质是引用链问题：emitter → listener → 闭包 → 组件状态。解决方案有三个：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;手动 &lt;code&gt;off&lt;/code&gt;（最基本）&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;WeakRef&lt;/code&gt; 弱引用（进阶）&lt;/li&gt;
&lt;li&gt;用 &lt;code&gt;AbortController&lt;/code&gt; 统一管理生命周期（现代方案）"&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;如果面试官继续追问 &lt;code&gt;AbortController&lt;/code&gt; 方案：&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abort&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 使用&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;emitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleUpdate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 组件卸载时一键取消所有事件&lt;/span&gt;
&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 &lt;code&gt;AbortController&lt;/code&gt; 方案和 &lt;code&gt;fetch&lt;/code&gt; 取消请求是同一个 API，前端新标准正在往这个方向统一。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="追问 5："&gt;追问 5："怎么实现带命名空间的事件？"&lt;/h2&gt;
&lt;p&gt;实际项目中，事件名经常需要层级结构：&lt;code&gt;user.login&lt;/code&gt;、&lt;code&gt;user.logout&lt;/code&gt;、&lt;code&gt;order.create&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;如果要支持 &lt;code&gt;emitter.emit('user.*')&lt;/code&gt; 触发所有 &lt;code&gt;user.&lt;/code&gt; 开头的事件：&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 精确匹配&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// 通配符匹配&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不需要完整实现通配符匹配，面试中说出思路就够了。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="追问 6："&gt;追问 6："Node.js 的 EventEmitter 和你写的有什么区别？"&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;手写版&lt;/th&gt;
&lt;th&gt;Node.js EventEmitter&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最大监听数&lt;/td&gt;
&lt;td&gt;无限制&lt;/td&gt;
&lt;td&gt;默认 10 个，超过会警告（防泄漏）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;错误处理&lt;/td&gt;
&lt;td&gt;手动 try-catch&lt;/td&gt;
&lt;td&gt;必须监听 &lt;code&gt;error&lt;/code&gt; 事件，否则进程崩&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prependListener&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;eventNames()&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;listenerCount()&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;/td&gt;
&lt;td&gt;不支持&lt;/td&gt;
&lt;td&gt;本身是同步的，但生态有 &lt;code&gt;EventEmitter2&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;答这个问题的关键是"最大监听数"。&lt;/strong&gt; Node.js 默认限制 10 个 listener，超过会打印警告：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这不是 bug，是故意的——防止你忘了 &lt;code&gt;off&lt;/code&gt; 导致内存泄漏。可以通过 &lt;code&gt;emitter.setMaxListeners(20)&lt;/code&gt; 调整。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="完整最终版"&gt;完整最终版&lt;/h2&gt;
&lt;p&gt;把 6 个追问的优化点都加上：&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;EventEmitter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;maxListeners&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxListeners&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Warning: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; listeners.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;abort&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;_original&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listeners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;_original&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;setMaxListeners&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxListeners&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;listenerCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;removeAllListeners&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 30 行的基础版到完整版，每一行新增代码都对应一个追问。面试时先写基础版，面试官追问时再逐步加——这比一上来就写完整版更能展示你的思维过程。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;strong&gt;你面试中被手写题难住过吗？最难的是哪道？评论区聊聊。&lt;/strong&gt;&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Tue, 30 Jun 2026 11:51:28 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7620</link>
      <guid>http://beta.w2solo.com/topics/7620</guid>
    </item>
    <item>
      <title>马斯克说今年每个月都要发一个大模型，Grok 4.5 已经在内测了</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;2026年6月29日，周日。本周末最大的消息：马斯克宣布 SpaceX 要月更大模型，三星砸 6480 亿美元赌芯片，GPT-5.6 开始限量放人了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="#1 马斯克：Grok 4.5 已在 SpaceX 和特斯拉内测，今年每月发一个新模型"&gt;#1 马斯克：Grok 4.5 已在 SpaceX 和特斯拉内测，今年每月发一个新模型&lt;/h3&gt;
&lt;p&gt;马斯克周六在 X 上宣布，Grok 4.5 已经在 SpaceX 和特斯拉内部开启 Beta 测试，早期评测显示性能"接近甚至可能超越 Claude Opus"。更狠的是，他说 SpaceX 计划在今年剩余时间里每月发布一个全新的、从零训练的大模型。&lt;/p&gt;

&lt;p&gt;一年前 xAI 还是个配角，现在要月更模型了。当然，马斯克画饼的本事大家都知道，能不能每月兑现是另一回事。但 SpaceX 手里有算力（Colossus 2 数据中心）、有钱（上市两周市值逼近 3 万亿），真要打起来，OpenAI 和 Anthropic 的压力会很大。&lt;/p&gt;

&lt;p&gt;来源：财联社&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="#2 三星宣布未来十年投资 6480 亿美元，砸向芯片和数据中心"&gt;#2 三星宣布未来十年投资 6480 亿美元，砸向芯片和数据中心&lt;/h3&gt;
&lt;p&gt;三星集团将在 6 月 29 日的总统府会议上正式宣布：未来十年投资 1000 万亿韩元（约 6480 亿美元），覆盖芯片、数据中心、储能电池和显示器。其中可能包括斥资约 300 万亿韩元在全罗道建一座芯片工厂。&lt;/p&gt;

&lt;p&gt;6480 亿美元是什么概念？差不多等于韩国 GDP 的一半。三星这是在赌国运——赌芯片和算力就是未来十年的石油。上周费城半导体指数出现较大调整，三星选在这个时候放出天量投资计划，既是提振信心，也是逆周期布局。&lt;/p&gt;

&lt;p&gt;来源：新浪财经&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="#3 GPT-5.6 Sol/Terra/Luna 正式开放限量预览，但仅限美国"&gt;#3 GPT-5.6 Sol/Terra/Luna 正式开放限量预览，但仅限美国&lt;/h3&gt;
&lt;p&gt;OpenAI 周五启动了 GPT-5.6 的限量预览。三个型号：Sol（旗舰）、Terra（标准）、Luna（轻量），Terminal-Bench 得分 91.9%。但有个关键限制：仅限美国用户，而且只对一小批合作伙伴开放。&lt;/p&gt;

&lt;p&gt;说白了，白宫的审查令还在生效。OpenAI 虽然发了模型，但发布范围被卡得很死。对国内开发者来说，短期内用不上。想体验的只能等审查期过了之后看 API 什么时候全球开放。&lt;/p&gt;

&lt;p&gt;来源：The Hindu&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="#4 智元机器人 90 天从 1 万台到 1.5 万台，具身智能量产在加速"&gt;#4 智元机器人 90 天从 1 万台到 1.5 万台，具身智能量产在加速&lt;/h3&gt;
&lt;p&gt;智元机器人周六发布会上，第 15000 台机器人正式下线。3 个月前刚庆祝了第 10000 台，90 天又加了 5000 台。这台编号 15000 的"精灵 G2"已经直接被送去龙旗科技的工厂产线上岗了。&lt;/p&gt;

&lt;p&gt;国内具身智能赛道今年融资超 460 亿，七成集中在头部。智元这个量产速度如果能持续，年底可能冲到 3 万台以上。对比一下，波士顿动力做了几十年也没到这个规模。&lt;/p&gt;

&lt;p&gt;来源：上观新闻&lt;/p&gt;

&lt;hr&gt;
&lt;h3 id="#5 《人民日报》点名批评微短剧里的"&gt;#5《人民日报》点名批评微短剧里的"AI 脸"：流水线精致五官，看吐了&lt;/h3&gt;
&lt;p&gt;《人民日报》锐评：微短剧中大模型生成面孔占比已超 95%，千篇一律的圆眼睛、高鼻梁、尖下巴，"精致得毫无辨识度，好看得没有任何记忆点"。上周广电总局刚发了微短剧分类分层标准，这周官媒直接开始点名批评内容质量。&lt;/p&gt;

&lt;p&gt;这条对做 AIGC 内容的开发者是个信号：光能生成不够，审美同质化已经成为监管和舆论的靶子。接下来要么在风格差异化上下功夫，要么等着被市场淘汰。&lt;/p&gt;

&lt;p&gt;来源：快科技&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="一句话点评"&gt;一句话点评&lt;/h2&gt;
&lt;p&gt;本周末的关键词是"加注"——马斯克要月更模型、三星砸 6480 亿、智元 90 天出 5000 台机器人。上周全球科技股刚经历了一轮调整，但头部玩家的态度很一致：越跌越投。至于谁在裸泳，下半年见分晓。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Mon, 29 Jun 2026 16:09:17 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7615</link>
      <guid>http://beta.w2solo.com/topics/7615</guid>
    </item>
    <item>
      <title>别再 console.log 了：5 个 Chrome DevTools 调试技巧，用过就回不去了</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;我观察过身边的前端同事，90% 的人调试代码的方式就是一行行加 &lt;code&gt;console.log&lt;/code&gt;。改完了再一行行删。出了 bug 再加回来。这个循环我自己也干了两年，直到有一天我发现了 Chrome DevTools 里这些"隐藏"功能——说是隐藏，其实一直都在那，只是没人告诉你怎么用。这篇文章把 5 个最实用的技巧写出来，每个都附操作步骤，看完今天就能用上。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="为什么你应该少用 console.log"&gt;为什么你应该少用 console.log&lt;/h2&gt;
&lt;p&gt;先说清楚：&lt;code&gt;console.log&lt;/code&gt; 不是不能用。但它有三个致命问题：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;侵入式&lt;/strong&gt;：你得改代码、保存、刷新页面、看输出、再改代码、再删掉。一个 bug 可能要加删十几次。&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;code&gt;console.log('test')&lt;/code&gt; 和 &lt;code&gt;console.log('到这了')&lt;/code&gt;，你一定在别人的项目里见过。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;DevTools 的调试工具可以做到 &lt;code&gt;console.log&lt;/code&gt; 做不到的事，而且&lt;strong&gt;不需要改任何一行代码&lt;/strong&gt;。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="技巧 1：条件断点 — 替代 if (id === 5) console.log(data)"&gt;技巧 1：条件断点 — 替代 &lt;code&gt;if (id === 5) console.log(data)&lt;/code&gt;
&lt;/h2&gt;&lt;h3 id="场景"&gt;场景&lt;/h3&gt;
&lt;p&gt;你有一个列表渲染了 100 条数据，但只有第 5 条数据有 bug。用 &lt;code&gt;console.log&lt;/code&gt; 的话，要么打印 100 条慢慢找，要么加 &lt;code&gt;if&lt;/code&gt; 判断：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 你以前的做法&lt;/span&gt;
&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;有问题的数据:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;renderItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="用条件断点"&gt;用条件断点&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;打开 DevTools → Sources 面板&lt;/li&gt;
&lt;li&gt;找到对应代码，在行号上&lt;strong&gt;右键&lt;/strong&gt; → 选择 &lt;strong&gt;Add conditional breakpoint&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;输入条件：&lt;code&gt;item.id === 5&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;回车确认&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;现在代码只会在 &lt;code&gt;item.id === 5&lt;/code&gt; 时暂停，你可以在 Scope 面板里直接看到 &lt;code&gt;item&lt;/code&gt; 的所有属性，不需要改任何代码。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;进阶用法：&lt;/strong&gt; 条件表达式里可以写任何 JS，包括：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 只在数组长度异常时断住&lt;/span&gt;
&lt;span class="nx"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;

&lt;span class="c1"&gt;// 只在某个属性为 null 时断住&lt;/span&gt;
&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

&lt;span class="c1"&gt;// 组合条件&lt;/span&gt;
&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;断点的颜色会变成&lt;strong&gt;橙色&lt;/strong&gt;（普通断点是蓝色），方便你区分。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="技巧 2：Logpoints — 不改代码就能打日志"&gt;技巧 2：Logpoints — 不改代码就能打日志&lt;/h2&gt;&lt;h3 id="场景"&gt;场景&lt;/h3&gt;
&lt;p&gt;你想在某行代码执行时打印一些信息，但不想改代码（比如代码在 &lt;code&gt;node_modules&lt;/code&gt; 里，或者你不想频繁保存触发热更新）。&lt;/p&gt;
&lt;h3 id="操作步骤"&gt;操作步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Sources 面板，在行号上&lt;strong&gt;右键&lt;/strong&gt; → 选择 &lt;strong&gt;Add logpoint&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;输入要打印的内容，语法和 &lt;code&gt;console.log&lt;/code&gt; 一样：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'用户数据:', user, '请求耗时:', Date.now() - startTime, 'ms'
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;回车确认，行号旁边会出现一个&lt;strong&gt;粉色菱形标记&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;代码执行到这一行时，会在 Console 面板输出你写的内容，但&lt;strong&gt;不会暂停执行&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="和 console.log 的区别"&gt;和 console.log 的区别&lt;/h3&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;对比项&lt;/th&gt;
&lt;th&gt;console.log&lt;/th&gt;
&lt;th&gt;Logpoint&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要改代码&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;需要保存刷新&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调试完要删除&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;关掉 DevTools 自动消失&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;在 node_modules 里能用&lt;/td&gt;
&lt;td&gt;不方便&lt;/td&gt;
&lt;td&gt;可以&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;这是我用得最多的功能。&lt;/strong&gt; 团队里有个同事之前遇到第三方库的 bug，加了 20 行 &lt;code&gt;console.log&lt;/code&gt; 在 &lt;code&gt;node_modules&lt;/code&gt; 里，调完了忘删，下一次 &lt;code&gt;npm install&lt;/code&gt; 覆盖掉了他的调试代码，他又加了一遍。如果用 Logpoint，根本不需要碰源码。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="技巧 3：Console 高级 API — 替代满屏的 console.log"&gt;技巧 3：Console 高级 API — 替代满屏的 console.log&lt;/h2&gt;
&lt;p&gt;大多数人只知道 &lt;code&gt;console.log&lt;/code&gt;。但 Console API 还有这些：&lt;/p&gt;
&lt;h3 id="console.table() — 表格展示数据"&gt;console.table() — 表格展示数据&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;张三&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;李四&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;王五&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// 之前：console.log(users)  → 输出一坨折叠的对象&lt;/span&gt;

&lt;span class="c1"&gt;// 现在：&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出是一个&lt;strong&gt;整齐的表格&lt;/strong&gt;，列头是属性名，一目了然。还可以指定只显示某些列：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;role&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="console.group() — 分组折叠"&gt;console.group() — 分组折叠&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;用户请求流程&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1. 发送请求&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2. 收到响应&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3. 更新状态&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groupEnd&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;渲染流程&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1. 计算 diff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2. 更新 DOM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groupEnd&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出会按组折叠，不再是一大坨混在一起的日志。用 &lt;code&gt;console.groupCollapsed()&lt;/code&gt; 还能默认折叠。&lt;/p&gt;
&lt;h3 id="console.time() — 精确计时"&gt;console.time() — 精确计时&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;数据处理&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;processLargeDataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;数据处理&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// 输出：数据处理: 142.38ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比 &lt;code&gt;Date.now()&lt;/code&gt; 相减更精确，而且不需要创建临时变量。&lt;/p&gt;
&lt;h3 id="console.trace() — 打印调用栈"&gt;console.trace() — 打印调用栈&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;updateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updateUser 被谁调用了&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出完整的调用链，直接告诉你这个函数是从哪个路径调进来的。排查"这个函数怎么被调了两次"的问题特别好用。&lt;/p&gt;
&lt;h3 id="console.assert() — 条件日志"&gt;console.assert() — 条件日志&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;年龄不应该小于等于0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有条件为 &lt;code&gt;false&lt;/code&gt; 时才输出，替代 &lt;code&gt;if (!xxx) console.log()&lt;/code&gt;。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="技巧 4：Network Override — 不改后端代码调试接口"&gt;技巧 4：Network Override — 不改后端代码调试接口&lt;/h2&gt;&lt;h3 id="场景"&gt;场景&lt;/h3&gt;
&lt;p&gt;后端接口返回的数据有问题，你想临时改一下返回值来测试前端逻辑。以前你可能会：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;找后端同事帮忙改接口（等半天）&lt;/li&gt;
&lt;li&gt;在前端代码里硬编码 mock 数据（改完还得删）&lt;/li&gt;
&lt;li&gt;用 Charles/Whistle 抓包改响应（配置麻烦）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="用 Network Override"&gt;用 Network Override&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;DevTools → Network 面板&lt;/li&gt;
&lt;li&gt;找到目标请求，&lt;strong&gt;右键&lt;/strong&gt; → 选择 &lt;strong&gt;Override content&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;DevTools 会让你选一个本地文件夹作为 override 目录&lt;/li&gt;
&lt;li&gt;修改响应内容，保存&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;之后每次刷新页面，这个接口都会返回你修改后的内容，&lt;strong&gt;不需要改任何前端或后端代码&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="实际案例"&gt;实际案例&lt;/h3&gt;
&lt;p&gt;后端接口 &lt;code&gt;/api/users&lt;/code&gt; 返回了 10 条数据，但你要测试"空列表"状态：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;正常请求一次，Override content&lt;/li&gt;
&lt;li&gt;把响应体改成 &lt;code&gt;[]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;刷新页面，前端就会渲染空状态&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;要测试"超长数据"？把响应改成 1000 条数据。要测试"字段缺失"？删掉某个字段。&lt;/p&gt;
&lt;h3 id="进阶：Override Headers"&gt;进阶：Override Headers&lt;/h3&gt;
&lt;p&gt;不只是响应体，你还可以 Override 响应头：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;模拟 CORS 错误：删掉 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 头&lt;/li&gt;
&lt;li&gt;模拟缓存策略：修改 &lt;code&gt;Cache-Control&lt;/code&gt; 头&lt;/li&gt;
&lt;li&gt;模拟慢网络：在 Network 面板顶部的 Throttling 选项里选 Slow 3G&lt;/li&gt;
&lt;/ul&gt;

&lt;hr&gt;
&lt;h2 id="技巧 5：Live Expressions — 实时监控变量"&gt;技巧 5：Live Expressions — 实时监控变量&lt;/h2&gt;&lt;h3 id="场景"&gt;场景&lt;/h3&gt;
&lt;p&gt;你想持续观察某个变量的值变化，用 &lt;code&gt;console.log&lt;/code&gt; 的话，每次值变化都要重新打印，Console 面板很快就被刷屏了。&lt;/p&gt;
&lt;h3 id="操作步骤"&gt;操作步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;DevTools → Console 面板&lt;/li&gt;
&lt;li&gt;点击 Console 面板顶部的&lt;strong&gt;眼睛图标&lt;/strong&gt;（Create live expression）&lt;/li&gt;
&lt;li&gt;输入你要监控的表达式：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.list-item&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;这个表达式的值会&lt;strong&gt;实时更新&lt;/strong&gt;，不需要你手动执行&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="常用监控表达式"&gt;常用监控表达式&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 监控页面上有多少 DOM 节点&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;

&lt;span class="c1"&gt;// 监控某个全局状态&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__STORE__&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loginStatus&lt;/span&gt;

&lt;span class="c1"&gt;// 监控页面滚动位置&lt;/span&gt;
&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// 监控内存使用&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;memory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usedJSHeapSize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1048576&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MB&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以同时添加多个 Live Expression，相当于一个&lt;strong&gt;实时仪表盘&lt;/strong&gt;，比反复 &lt;code&gt;console.log&lt;/code&gt; 高效得多。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="总结：场景对照表"&gt;总结：场景对照表&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;调试场景&lt;/th&gt;
&lt;th&gt;console.log 做法&lt;/th&gt;
&lt;th&gt;DevTools 做法&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查看某个变量值&lt;/td&gt;
&lt;td&gt;加 &lt;code&gt;console.log(x)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;条件断点，在 Scope 面板看&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;在某行打印日志&lt;/td&gt;
&lt;td&gt;改代码加 log&lt;/td&gt;
&lt;td&gt;Logpoint，不改代码&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;打印数组/对象&lt;/td&gt;
&lt;td&gt;&lt;code&gt;console.log(arr)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;console.table(arr)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;分组打印&lt;/td&gt;
&lt;td&gt;手动加分隔符&lt;/td&gt;
&lt;td&gt;&lt;code&gt;console.group()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;计算耗时&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Date.now()&lt;/code&gt; 相减&lt;/td&gt;
&lt;td&gt;&lt;code&gt;console.time()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;查看调用栈&lt;/td&gt;
&lt;td&gt;猜&lt;/td&gt;
&lt;td&gt;&lt;code&gt;console.trace()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;修改接口返回值&lt;/td&gt;
&lt;td&gt;Mock 或找后端&lt;/td&gt;
&lt;td&gt;Network Override&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实时监控变量&lt;/td&gt;
&lt;td&gt;setInterval + log&lt;/td&gt;
&lt;td&gt;Live Expression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调试 node_modules&lt;/td&gt;
&lt;td&gt;改源码（危险）&lt;/td&gt;
&lt;td&gt;Logpoint 或条件断点&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="一个实际 debug 故事"&gt;一个实际 debug 故事&lt;/h2&gt;
&lt;p&gt;上个月我们项目有个 bug：列表页的分页组件在第 3 页之后会显示错误的总数。&lt;/p&gt;

&lt;p&gt;如果用 &lt;code&gt;console.log&lt;/code&gt; 调试，我得：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在分页组件里加 log，打印 &lt;code&gt;total&lt;/code&gt; 值 → 值是对的&lt;/li&gt;
&lt;li&gt;在请求函数里加 log，打印响应数据 → 数据也是对的&lt;/li&gt;
&lt;li&gt;在状态管理里加 log，打印 store → 也是对的&lt;/li&gt;
&lt;li&gt;困惑，加更多 log...&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我实际的做法：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;用 &lt;strong&gt;Network Override&lt;/strong&gt; 把第 3 页的接口响应 &lt;code&gt;total&lt;/code&gt; 改成一个特殊值（比如 999）&lt;/li&gt;
&lt;li&gt;页面显示 999 → 说明组件渲染逻辑没问题，数据能正确传到 UI&lt;/li&gt;
&lt;li&gt;去掉 Override，用 &lt;strong&gt;条件断点&lt;/strong&gt; 在状态更新的地方设 &lt;code&gt;page === 3&lt;/code&gt; 的条件&lt;/li&gt;
&lt;li&gt;断住后在 Scope 面板发现：第 3 页请求回来时，有一个竞态条件——第 2 页的请求比第 3 页晚到达，把 &lt;code&gt;total&lt;/code&gt; 覆盖成了第 2 页的值&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;从发现 bug 到定位根因：12 分钟。&lt;/strong&gt; 如果用 &lt;code&gt;console.log&lt;/code&gt; 大法，光加 log 删 log 就得半小时。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="快捷键速查"&gt;快捷键速查&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;Mac&lt;/th&gt;
&lt;th&gt;Windows&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;打开 DevTools&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cmd + Option + I&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;F12&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;打开 Command Menu&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cmd + Shift + P&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl + Shift + P&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;搜索文件&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cmd + P&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl + P&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;搜索代码&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Cmd + Option + F&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl + Shift + F&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;切换面板&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Cmd + ]&lt;/code&gt; / &lt;code&gt;Cmd + [&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ctrl + ]&lt;/code&gt; / &lt;code&gt;Ctrl + [&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;暂停/继续执行&lt;/td&gt;
&lt;td&gt;&lt;code&gt;F8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;F8&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单步执行&lt;/td&gt;
&lt;td&gt;&lt;code&gt;F10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;F10&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;进入函数&lt;/td&gt;
&lt;td&gt;&lt;code&gt;F11&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;F11&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;在 Command Menu（&lt;code&gt;Cmd + Shift + P&lt;/code&gt;）里输入 "logpoint" 或 "override" 可以快速找到对应功能。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;em&gt;如果对你有帮助，点赞收藏一下。这些技巧我自己每天都在用，确实比 console.log 效率高很多。如果你有其他 DevTools 的隐藏技巧，评论区分享一下。&lt;/em&gt;&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Tue, 23 Jun 2026 21:00:13 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7582</link>
      <guid>http://beta.w2solo.com/topics/7582</guid>
    </item>
    <item>
      <title>AI 生成的代码能跑就行？这 5 个坑迟早炸</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;AI 写代码的效率毋庸置疑，但"能跑"和"能维护"之间隔了一道鸿沟。这篇文章总结了 AI 生成代码最常见的 5 个质量问题，每个都附反面案例和修复方案，最后给出一份 5 条验收 checklist。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;2026 年了，AI 写代码已经不是新鲜事。但我发现一个普遍现象：很多人的工作流是——&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;让 AI 生成代码&lt;/li&gt;
&lt;li&gt;能跑就提交&lt;/li&gt;
&lt;li&gt;出了 bug 再让 AI 修&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这个循环看似高效，实际上在代码仓库里埋了一堆定时炸弹。下面是我 review AI 代码时反复遇到的 5 个问题，从高频到低频排列。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="第一坑：过度工程化——3 行能解决的事写了 30 行"&gt;第一坑：过度工程化——3 行能解决的事写了 30 行&lt;/h2&gt;
&lt;p&gt;AI 特别喜欢上设计模式。你让它写一个"判断用户是否登录"的函数，它能给你整出工厂模式 + 策略模式 + 抽象基类：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AuthStrategyFactory.js&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;AuthStrategyFactory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;createStrategy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;strategies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;JwtAuthStrategy&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;SessionAuthStrategy&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;oauth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;OAuthStrategy&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;strategies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;JwtAuthStrategy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// JwtAuthStrategy.js&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;JwtAuthStrategy&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;BaseAuthStrategy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;TokenDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;AuthResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AuthErrorCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INVALID_TOKEN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;AuthResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 另外还有：BaseAuthStrategy.js、TokenDecoder.js、AuthResult.js、AuthErrorCode.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;6 个文件，200 多行，就为了判断用户有没有登录。而这个项目从头到尾只用了 JWT 一种方式，不可能有 session 和 OAuth。&lt;/p&gt;

&lt;p&gt;实际需要的代码：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;checkAuth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;atob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个文件，15 行，功能完全一样。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么会这样：&lt;/strong&gt; AI 倾向于"展示它知道的所有设计模式"，而不是"用最简单的方式解决问题"。它没有偷懒的本能，而偷懒有时候是好事——逼你写出最简方案。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;怎么防：&lt;/strong&gt; 拿到 AI 代码先问自己——"删掉一半代码还能跑吗？"如果能，那删掉的就是过度设计。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="第二坑：幽灵代码——大量函数从未被调用"&gt;第二坑：幽灵代码——大量函数从未被调用&lt;/h2&gt;
&lt;p&gt;让 AI 写一个"订单模块的工具函数"，它会把所有它能想到的都给你：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// utils/orderHelpers.js&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;calculateOrderDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;discountRules&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userLevel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 45 行折扣计算逻辑&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;formatOrderForExport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 60 行导出格式化逻辑&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;validateOrderTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetStatus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ... 30 行状态机校验&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写得都挺好，逻辑也对。但问题是——&lt;strong&gt;项目根本没有折扣功能，没有导出功能，状态机也没用到。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;这些就是"幽灵代码"：活在仓库里，从未被执行，但你维护的时候会把它们当成有用的代码去读、去理解。实际有效代码 5000 行的模块，可能躺了 3000 行死代码。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么会这样：&lt;/strong&gt; AI 不知道 YAGNI（You Ain't Gonna Need It）。它基于"这个场景可能需要什么"生成代码，而不是"现在到底需要什么"。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;怎么防：&lt;/strong&gt; 前端用 &lt;code&gt;knip&lt;/code&gt; 检测，比 &lt;code&gt;depcheck&lt;/code&gt; 更准：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx knip &lt;span class="nt"&gt;--include&lt;/span&gt; exports
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一条命令揪出所有未被使用的导出函数、类型、组件。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="第三坑：假注释——注释和代码说的不是同一件事"&gt;第三坑：假注释——注释和代码说的不是同一件事&lt;/h2&gt;
&lt;p&gt;这个坑最阴，因为你只看注释的话，逻辑是通的：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 检查用户是否有该订单的操作权限&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;checkOrderPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formattedOrder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YYYY-MM-DD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YYYY-MM-DD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;formattedOrder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注释说"检查权限"，代码在做&lt;strong&gt;数据格式化&lt;/strong&gt;。返回值不是 &lt;code&gt;boolean&lt;/code&gt;，是一个格式化后的对象。&lt;/p&gt;

&lt;p&gt;调用方写成了这样：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;checkOrderPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;无权限&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;恰好，当 &lt;code&gt;orderId&lt;/code&gt; 不存在时 &lt;code&gt;findById&lt;/code&gt; 返回 &lt;code&gt;null&lt;/code&gt;，&lt;code&gt;!null === true&lt;/code&gt;，所以"权限检查"碰巧能跑。纯属巧合。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么会这样：&lt;/strong&gt; AI 先生成了一个权限检查函数，后来被要求加数据格式化逻辑，改了函数体但没改函数名和注释。这种"假注释"比没注释更危险——没注释你会认真读代码，有注释你会下意识信任它，跳过代码。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;怎么防：&lt;/strong&gt; 好的代码不需要注释。与其修注释，不如改函数名——&lt;code&gt;checkOrderPermission&lt;/code&gt; 改成 &lt;code&gt;formatOrderForDisplay&lt;/code&gt;，删掉注释，可读性反而更高。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="第四坑：万能 try-catch——出了 bug 永远定位不到"&gt;第四坑：万能 try-catch——出了 bug 永远定位不到&lt;/h2&gt;
&lt;p&gt;AI 特别喜欢给每个函数都包一层 try-catch：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;validateParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;notifyUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;updateInventory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error in createOrder:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;validateParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... 校验逻辑&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error in validateParams:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每一层都默默吞掉错误，打个 &lt;code&gt;console.log&lt;/code&gt; 就完事。&lt;/p&gt;

&lt;p&gt;结果：线上出 bug，Sentry 里只有 "Error in createOrder: null"。是创建失败了？通知失败了？库存扣减失败了？你不知道。所有上下文都被 catch 吃掉了。&lt;/p&gt;

&lt;p&gt;更要命的是 &lt;code&gt;return null&lt;/code&gt;。调用方拿到 &lt;code&gt;null&lt;/code&gt; 以为"没有订单"，继续往下跑，导致后面一连串空指针。一个本该在源头暴露的错误，被层层掩盖，最后在距离原始问题八百里远的地方炸出来。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;正确做法：错误冒泡，只在边界捕获。&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;createOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;validateParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 校验失败直接抛&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 数据库错误直接抛&lt;/span&gt;

  &lt;span class="c1"&gt;// 只有不影响主流程的旁路操作才 try-catch&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;notifyUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;通知发送失败，不影响订单创建&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;为什么会这样：&lt;/strong&gt; AI 出于"安全感"给每个函数加 try-catch——它不想让任何错误导致程序崩溃。但过度防御恰恰让 debug 变成噩梦。错误处理不是越多越好，是在&lt;strong&gt;正确的位置&lt;/strong&gt;处理。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="第五坑：data、data2、data3——变量名丢失业务语义"&gt;第五坑：data、data2、data3——变量名丢失业务语义&lt;/h2&gt;
&lt;p&gt;这个问题随处可见：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;processData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;submitData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;setList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;读到 &lt;code&gt;data3&lt;/code&gt; 的时候你已经忘了 &lt;code&gt;data&lt;/code&gt; 是什么了。对比一下：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allOrders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetchOrders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formattedOrders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formatForDisplay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allOrders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;activeOrders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formattedOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;submitOrders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeOrders&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;submitResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;setOrderList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;submitResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一眼就知道每一步在做什么。&lt;/p&gt;

&lt;p&gt;另一个变种更常见：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/orders/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/items`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个完全不同的接口，返回值都叫 &lt;code&gt;res&lt;/code&gt;。改 bug 的时候搜 &lt;code&gt;res&lt;/code&gt;，搜出来 47 个结果，祝你好运。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么会这样：&lt;/strong&gt; AI 在生成整段代码时倾向于用通用名词。&lt;code&gt;data&lt;/code&gt; 是最安全的命名——不会出错，但也不传达任何信息。它没有你的业务上下文，不知道 &lt;code&gt;data&lt;/code&gt; 其实是"订单列表"。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;怎么防：&lt;/strong&gt; AI 生成代码后，第一件事搜 &lt;code&gt;data&lt;/code&gt;、&lt;code&gt;res&lt;/code&gt;、&lt;code&gt;result&lt;/code&gt;、&lt;code&gt;temp&lt;/code&gt;、&lt;code&gt;item&lt;/code&gt;，全部改成业务名词。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="AI 代码验收 Checklist"&gt;AI 代码验收 Checklist&lt;/h2&gt;
&lt;p&gt;以上 5 个坑归结起来，就是一句话：&lt;strong&gt;AI 的代码和实习生的代码一样，能跑但需要 review。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;每次 AI 生成代码后，花 5 分钟过一遍：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&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;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;删减测试&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;删掉一半代码还能跑吗？能跑的就是过度设计&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;死代码检测&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;跑 &lt;code&gt;npx knip --include exports&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;注释一致性&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;每条注释和代码对一下，不确定就删注释&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;错误处理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;搜 &lt;code&gt;catch&lt;/code&gt;，是不是只有 &lt;code&gt;console.log&lt;/code&gt;？该冒泡还是该吞？&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;命名审查&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;搜 &lt;code&gt;data&lt;/code&gt;/&lt;code&gt;res&lt;/code&gt;/&lt;code&gt;result&lt;/code&gt;/&lt;code&gt;temp&lt;/code&gt;，全改成业务名词&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;5 分钟能省你三天的 debug 时间。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;em&gt;你在 review AI 代码时踩到过什么坑？评论区聊聊。&lt;/em&gt;&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Mon, 22 Jun 2026 18:36:40 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7577</link>
      <guid>http://beta.w2solo.com/topics/7577</guid>
    </item>
    <item>
      <title>2026 年了，这 6 个 npm 包可以卸载了——浏览器原生 API 已经能替代</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;前几天我跑了一下 &lt;code&gt;npx depcheck&lt;/code&gt;，发现项目里有 47 个依赖，其中至少 6 个完全可以用浏览器原生 API 替代。卸载之后，打包体积直接少了 82KB（gzip 后少了 23KB），首屏加载快了 300ms。这篇文章把每个包的原生替代方案都写出来，附迁移代码，直接抄。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="为什么要清理依赖"&gt;为什么要清理依赖&lt;/h2&gt;
&lt;p&gt;每多一个 npm 包，你的项目就多了：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;打包体积&lt;/strong&gt;：用户每次访问都要多下载这些代码&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;供应链风险&lt;/strong&gt;：还记得 &lt;code&gt;event-stream&lt;/code&gt; 投毒事件吗？依赖越少，攻击面越小&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;版本冲突&lt;/strong&gt;：包 A 依赖 lodash@4，包 B 依赖 lodash@3，解决冲突的时间比写代码还长&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;2026 年的浏览器已经非常强大了。很多你以为"必须装包"的功能，原生 API 早就支持了。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="1. 卸载 lodash.cloneDeep → 用 structuredClone()"&gt;1. 卸载 &lt;code&gt;lodash.cloneDeep&lt;/code&gt; → 用 &lt;code&gt;structuredClone()&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;之前：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;lodash.cloneDeep   &lt;span class="c"&gt;# 5.3KB gzip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cloneDeep&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lodash.cloneDeep&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cloneDeep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;complexObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;现在：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;copy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;structuredClone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;complexObject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完了。一行，零依赖。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;structuredClone&lt;/code&gt; 是浏览器原生的深拷贝方法，支持 &lt;code&gt;Map&lt;/code&gt;、&lt;code&gt;Set&lt;/code&gt;、&lt;code&gt;Date&lt;/code&gt;、&lt;code&gt;RegExp&lt;/code&gt;、&lt;code&gt;ArrayBuffer&lt;/code&gt;、循环引用——这些 &lt;code&gt;JSON.parse(JSON.stringify())&lt;/code&gt; 做不到的，它全能做。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;兼容性：&lt;/strong&gt; Chrome 98+、Firefox 94+、Safari 15.4+，2026 年你不需要担心兼容性。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;唯一限制：&lt;/strong&gt; 不支持拷贝 DOM 节点和函数。如果你的对象里有函数属性，这个方案不适用。但说实话，你的数据对象里不应该有函数。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;能省多少：&lt;/strong&gt; lodash.cloneDeep 单独引入约 5.3KB gzip，卸载后直接省掉。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="2. 卸载 uuid → 用 crypto.randomUUID()"&gt;2. 卸载 &lt;code&gt;uuid&lt;/code&gt; → 用 &lt;code&gt;crypto.randomUUID()&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;之前：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;uuid   &lt;span class="c"&gt;# 2.7KB gzip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;v4&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;uuidv4&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uuid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;uuidv4&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 'f47ac10b-58cc-4372-a567-0e02b2c3d479'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;现在：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 'f47ac10b-58cc-4372-a567-0e02b2c3d479'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出格式完全一样，都是标准的 UUID v4。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;兼容性：&lt;/strong&gt; Chrome 92+、Firefox 95+、Safari 15.4+，全线支持。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node.js 也支持：&lt;/strong&gt; Node 19+ 内置 &lt;code&gt;crypto.randomUUID()&lt;/code&gt;，前后端通吃。&lt;/p&gt;

&lt;p&gt;如果你只需要一个唯一 ID 而不需要严格的 UUID 格式，还有更轻量的方案：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;simpleId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;random&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// '5x3g7k9m2'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="3. 卸载 dayjs / moment → 用 Intl.DateTimeFormat + Temporal"&gt;3. 卸载 &lt;code&gt;dayjs&lt;/code&gt; / &lt;code&gt;moment&lt;/code&gt; → 用 &lt;code&gt;Intl.DateTimeFormat&lt;/code&gt; + &lt;code&gt;Temporal&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;这个是最重磅的。&lt;code&gt;moment.js&lt;/code&gt; 光 gzip 就 72KB，&lt;code&gt;dayjs&lt;/code&gt; 虽然轻（2KB），但大多数场景你连 2KB 都不需要。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;场景一：格式化日期显示&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 之前：dayjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dayjs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dayjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YYYY年MM月DD日&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 现在：原生 Intl&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DateTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;numeric&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2-digit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;day&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2-digit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// '2026/06/21'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;场景二：相对时间（"3 小时前"）&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 之前：dayjs + relativeTime 插件&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dayjs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dayjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;relativeTime&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dayjs/plugin/relativeTime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;relativeTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;fromNow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// 现在：原生 Intl.RelativeTimeFormat&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rtf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Intl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RelativeTimeFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zh-CN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;rtf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hour&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// '3小时前'&lt;/span&gt;
&lt;span class="nx"&gt;rtf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;day&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// '昨天'&lt;/span&gt;
&lt;span class="nx"&gt;rtf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;month&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// '后2个月'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;场景三：日期计算&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 之前&lt;/span&gt;
&lt;span class="nx"&gt;dayjs&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;day&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toDate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// 现在：Temporal API（2026 年主流浏览器已支持）&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plainDateISO&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextWeek&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextWeek&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// '2026-06-28'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;什么时候还需要 dayjs：&lt;/strong&gt; 如果你要做大量复杂的时区转换、日历系统切换（农历之类），dayjs 的插件生态还是有价值的。但如果只是格式化显示和简单计算，原生足够了。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="4. 卸载 classnames / clsx → 用模板字符串"&gt;4. 卸载 &lt;code&gt;classnames&lt;/code&gt; / &lt;code&gt;clsx&lt;/code&gt; → 用模板字符串&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;之前：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;classnames   &lt;span class="c"&gt;# 0.6KB gzip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cn&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;classnames&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isPrimary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isDisabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-large&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;large&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;})}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;现在：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isPrimary&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isDisabled&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;large&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-large&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你觉得 &lt;code&gt;filter(Boolean).join(' ')&lt;/code&gt; 写起来啰嗦，封装一个两行的工具函数：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 用法完全一样&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isPrimary&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isDisabled&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;btn-disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2 行代码替代一个 npm 包。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;更好的方案：&lt;/strong&gt; 如果项目用了 Tailwind CSS，&lt;code&gt;tailwind-merge&lt;/code&gt; 比 classnames 更合适，因为它能处理 Tailwind 的类名冲突（比如同时写了 &lt;code&gt;p-2&lt;/code&gt; 和 &lt;code&gt;p-4&lt;/code&gt;）。这种场景下原生方案做不到。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="5. 卸载 node-fetch → 用原生 fetch"&gt;5. 卸载 &lt;code&gt;node-fetch&lt;/code&gt; → 用原生 &lt;code&gt;fetch&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;之前（Node.js 环境）：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;node-fetch   &lt;span class="c"&gt;# 8.4KB gzip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;现在：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Node.js 18+ 内置了 &lt;code&gt;fetch&lt;/code&gt;，不需要再装 &lt;code&gt;node-fetch&lt;/code&gt;。2026 年还在装这个包，大概率是因为 &lt;code&gt;package.json&lt;/code&gt; 里一直没清理。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 如果你的 Node.js 版本低于 18，还是需要 &lt;code&gt;node-fetch&lt;/code&gt;。但 2026 年了，Node 18 已经是 EOL，你至少应该在 Node 20+ 上。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="6. 卸载 qs → 用 URLSearchParams"&gt;6. 卸载 &lt;code&gt;qs&lt;/code&gt; → 用 &lt;code&gt;URLSearchParams&lt;/code&gt;
&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;之前：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;qs   &lt;span class="c"&gt;# 6.2KB gzip&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;qs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;qs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 序列化&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;前端&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// 'page=1&amp;amp;size=20&amp;amp;keyword=%E5%89%8D%E7%AB%AF'&lt;/span&gt;

&lt;span class="c1"&gt;// 解析&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page=1&amp;amp;size=20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { page: '1', size: '20' }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;现在：&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 序列化&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;前端&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// 'page=1&amp;amp;size=20&amp;amp;keyword=%E5%89%8D%E7%AB%AF'&lt;/span&gt;

&lt;span class="c1"&gt;// 解析&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fromEntries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page=1&amp;amp;size=20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// { page: '1', size: '20' }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;唯一限制：&lt;/strong&gt; &lt;code&gt;URLSearchParams&lt;/code&gt; 不支持嵌套对象和数组的序列化。如果你的查询参数是 &lt;code&gt;{ filter: { status: ['active', 'pending'] } }&lt;/code&gt; 这种结构，还是需要 &lt;code&gt;qs&lt;/code&gt;。但大多数前端场景的查询参数都是扁平的 key-value。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="实操：怎么找出项目里可以卸载的包"&gt;实操：怎么找出项目里可以卸载的包&lt;/h2&gt;&lt;h3 id="第一步：找出未使用的依赖"&gt;第一步：找出未使用的依赖&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;npx depcheck
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会列出 &lt;code&gt;package.json&lt;/code&gt; 里声明了但代码里从未 import 的包，直接删。&lt;/p&gt;
&lt;h3 id="第二步：分析打包体积"&gt;第二步：分析打包体积&lt;/h3&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;npx vite-bundle-visualizer
&lt;span class="c"&gt;# 或 webpack 项目&lt;/span&gt;
npx webpack-bundle-analyzer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看看哪些包占了大头。通常 &lt;code&gt;moment&lt;/code&gt;、&lt;code&gt;lodash&lt;/code&gt; 完整包是体积杀手。&lt;/p&gt;
&lt;h3 id="第三步：逐个替换"&gt;第三步：逐个替换&lt;/h3&gt;
&lt;p&gt;按本文方案替换后，跑一遍测试，确认功能正常再发版。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="总结对照表"&gt;总结对照表&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tr&gt;
&lt;th&gt;npm 包&lt;/th&gt;
&lt;th&gt;gzip 体积&lt;/th&gt;
&lt;th&gt;原生替代&lt;/th&gt;
&lt;th&gt;限制&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lodash.cloneDeep&lt;/td&gt;
&lt;td&gt;5.3KB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;structuredClone()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不支持函数和 DOM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uuid&lt;/td&gt;
&lt;td&gt;2.7KB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.randomUUID()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dayjs&lt;/td&gt;
&lt;td&gt;2KB&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Intl&lt;/code&gt; + &lt;code&gt;Temporal&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;复杂时区场景不够用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;classnames&lt;/td&gt;
&lt;td&gt;0.6KB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;filter(Boolean).join(' ')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;node-fetch&lt;/td&gt;
&lt;td&gt;8.4KB&lt;/td&gt;
&lt;td&gt;原生 &lt;code&gt;fetch&lt;/code&gt; (Node 18+)&lt;/td&gt;
&lt;td&gt;需要 Node 18+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qs&lt;/td&gt;
&lt;td&gt;6.2KB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;URLSearchParams&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不支持嵌套对象&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;合计&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~25KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0KB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;p&gt;25KB gzip 看起来不多，但在移动端弱网环境下，这就是 &lt;strong&gt;200-500ms 的加载时间差距&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;更重要的是：&lt;strong&gt;少一个依赖，就少一个供应链攻击的入口，少一个版本冲突的可能，少一个 &lt;code&gt;npm audit&lt;/code&gt; 的告警。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;em&gt;如果你的项目里还有其他可以用原生 API 替代的包，评论区说一下，我补充进来。点赞收藏一下，下次 Code Review 看到同事装多余的包，直接把这篇甩给他。&lt;/em&gt;&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Sun, 21 Jun 2026 12:32:36 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7566</link>
      <guid>http://beta.w2solo.com/topics/7566</guid>
    </item>
    <item>
      <title>同事每天催我 Code Review，我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;先说结论：用 GitHub Actions + Claude API 实现全自动 PR review，配置一次，以后每个 PR 推上去，AI 10 秒内给出详细评审意见，覆盖代码质量、潜在 bug、安全风险三个维度。这篇文章把完整配置都给你，拿走直接用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="背景：CR 慢是前端团队的通病"&gt;背景：CR 慢是前端团队的通病&lt;/h2&gt;
&lt;p&gt;我们团队有个不成文的规定：PR 提上去，要在 1 个工作日内完成 review。&lt;/p&gt;

&lt;p&gt;听起来很合理，但实际情况是这样的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;早上刚到公司，打开 GitHub，待 review 的 PR 已经排了 6 个&lt;/li&gt;
&lt;li&gt;每个 PR 少则 200 行，多则 800 行&lt;/li&gt;
&lt;li&gt;还有自己的需求要写&lt;/li&gt;
&lt;li&gt;还有各种会&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;于是每天我的 review 质量是这样的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;上午的 PR：认真看，写评论&lt;/li&gt;
&lt;li&gt;下午的 PR：大概扫一下&lt;/li&gt;
&lt;li&gt;晚上的 PR：approve，👍&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;然后有一天，同事 A 在群里 @ 了我：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"我那个 PR 提了两天了，能帮我看一下吗？"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;我翻出来一看——800 行，涉及三个模块。&lt;/p&gt;

&lt;p&gt;我当时脑子里只有一个念头：&lt;strong&gt;要是有人能替我先过一遍就好了。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="解决思路：AI 做初审，人做终审"&gt;解决思路：AI 做初审，人做终审&lt;/h2&gt;
&lt;p&gt;理想的工作流是这样的：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;开发者 push PR
    ↓
AI 自动分析 diff
    ↓
AI 在 PR 里发评论（10秒内）
    ↓
人工看 AI 的意见 + 做最终判断
    ↓
approve 或 request changes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好处显而易见：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI 处理掉 80% 的机械检查（代码风格、潜在空指针、未处理的 Promise reject）&lt;/li&gt;
&lt;li&gt;人只需要关注 AI 标注出的问题点 + 业务逻辑&lt;/li&gt;
&lt;li&gt;平均 review 时间从 30 分钟压缩到 8 分钟&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;坏处也要说清楚：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI 不理解你的业务逻辑，会漏掉业务层面的 bug&lt;/li&gt;
&lt;li&gt;AI 有时候会给"误报"，需要人来过滤&lt;/li&gt;
&lt;li&gt;不能完全替代人工 review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;所以定位是：AI 初审 + 人工终审，不是 AI 替代人工。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="实现方案：GitHub Actions + Claude API"&gt;实现方案：GitHub Actions + Claude API&lt;/h2&gt;&lt;h3 id="第一步：申请 Claude API Key"&gt;第一步：申请 Claude API Key&lt;/h3&gt;
&lt;p&gt;去 &lt;a href="https://console.anthropic.com" rel="nofollow" target="_blank" title=""&gt;console.anthropic.com&lt;/a&gt; 注册，免费额度足够测试用。&lt;/p&gt;

&lt;p&gt;把 Key 存到 GitHub 仓库的 Secrets：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;仓库 → Settings → Secrets and variables → Actions&lt;/li&gt;
&lt;li&gt;新建 &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="第二步：创建 workflow 文件"&gt;第二步：创建 workflow 文件&lt;/h3&gt;
&lt;p&gt;在你的仓库里创建 &lt;code&gt;.github/workflows/ai-review.yml&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AI Code Review&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ai-review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node.js&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install @anthropic-ai/sdk @octokit/rest&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run AI Review&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ANTHROPIC_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;PR_NUMBER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.number }}&lt;/span&gt;
          &lt;span class="na"&gt;REPO_OWNER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.repository_owner }}&lt;/span&gt;
          &lt;span class="na"&gt;REPO_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.repository.name }}&lt;/span&gt;
          &lt;span class="na"&gt;BASE_SHA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.base.sha }}&lt;/span&gt;
          &lt;span class="na"&gt;HEAD_SHA&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.head.sha }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node .github/scripts/ai-review.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="第三步：创建核心脚本"&gt;第三步：创建核心脚本&lt;/h3&gt;
&lt;p&gt;创建 &lt;code&gt;.github/scripts/ai-review.js&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@anthropic-ai/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Octokit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@octokit/rest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ANTHROPIC_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;octokit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Octokit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_TOKEN&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getDiff&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`git diff &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BASE_SHA&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;..&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HEAD_SHA&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -- '*.js' '*.ts' '*.tsx' '*.jsx' '*.vue'`&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// 超过 8000 字符截断，避免超出 token 限制&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;... [diff truncated]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;reviewWithClaude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`你是一个资深前端代码审查工程师，请对以下 PR diff 进行代码审查。

**审查重点：**
1. 潜在 Bug（空值引用、异步竞态、边界条件未处理）
2. 安全风险（XSS、敏感信息泄露、输入未校验）
3. 性能问题（不必要的重渲染、大对象序列化、内存泄漏）
4. 可维护性（函数过长、命名不清晰、重复代码）

**输出格式：**
用 Markdown 输出，包含以下几个部分：
- 🔴 严重问题（必须修复）
- 🟡 建议优化（可以改进）
- 🟢 代码亮点（做得好的地方）
- 📊 总体评分（1-10分，附简短理由）

如果没有问题，直接说"代码质量良好，无明显问题"。
用中文回复，简洁直接，不废话。

**PR Diff：**
&lt;/span&gt;&lt;span class="se"&gt;\`\`\`&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
&lt;/span&gt;&lt;span class="se"&gt;\`\`\`&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;postComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;REPO_OWNER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;REPO_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PR_NUMBER&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;octokit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createComment&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REPO_OWNER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REPO_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;issue_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PR_NUMBER&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`## 🤖 AI Code Review\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n---\n*由 Claude AI 自动生成，仅供参考，请以人工审查为准*`&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Getting diff...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;getDiff&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No JS/TS changes detected, skipping review.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sending to Claude for review...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;review&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reviewWithClaude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Posting comment...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;postComment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;review&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Done!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="第四步：推一个 PR 测试"&gt;第四步：推一个 PR 测试&lt;/h3&gt;
&lt;p&gt;随便改一个文件，提个 PR，大约 15-20 秒后，你会看到 AI 自动在 PR 下面发了一条评论。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="真实效果展示"&gt;真实效果展示&lt;/h2&gt;
&lt;p&gt;我们团队用了两周后，AI 的评论长这样：&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;&lt;strong&gt;🔴 严重问题（必须修复）&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;第 47 行 &lt;code&gt;userData.profile.avatar&lt;/code&gt;：未做空值检查，当 &lt;code&gt;profile&lt;/code&gt; 为 &lt;code&gt;null&lt;/code&gt; 时会抛出 TypeError。建议改为 &lt;code&gt;userData.profile?.avatar&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;第 83 行：&lt;code&gt;useEffect&lt;/code&gt; 的依赖数组缺少 &lt;code&gt;userId&lt;/code&gt;，可能导致使用过期的 &lt;code&gt;userId&lt;/code&gt; 发请求，产生数据不一致。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🟡 建议优化&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;第 92-115 行：&lt;code&gt;formatUserData&lt;/code&gt; 函数做了 3 件不同的事（格式化、过滤、排序），建议拆成 3 个函数，便于测试和复用。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🟢 代码亮点&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;错误处理写得很规范，catch 块没有直接吞掉错误，而是通过 &lt;code&gt;errorReporter.capture&lt;/code&gt; 上报，赞。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📊 总体评分：7/10&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;整体结构清晰，主要问题集中在空值防护上，修复后可以直接合并。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;看到这个，review 的人只需要：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;确认 AI 找出的问题是否真实存在（通常是）&lt;/li&gt;
&lt;li&gt;判断业务逻辑是否正确（这才是人工的价值所在）&lt;/li&gt;
&lt;li&gt;approve 或 request changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;平均节省 70% 的初审时间。&lt;/strong&gt;&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="进阶：按文件类型差异化 review"&gt;进阶：按文件类型差异化 review&lt;/h2&gt;
&lt;p&gt;不是所有文件都需要一样的审查策略。可以在 prompt 里加文件类型判断：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;buildPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileTypes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;focusMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;重点关注：参数校验、错误处理、幂等性、接口安全&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;component&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;重点关注：props 类型、副作用清理、渲染性能、无障碍属性&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;util&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;重点关注：边界条件、纯函数、测试覆盖&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;重点关注：状态更新的不可变性、selector 性能、副作用隔离&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;focus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fileTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;focusMap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`你是资深前端工程师，请审查以下代码：

&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;focus&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`**本次特别关注：**\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;

&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="踩坑记录"&gt;踩坑记录&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;坑 1：diff 太大超出 token 限制&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;解决方案：只 review 修改行数 &amp;lt; 500 的文件，超过的给一个提示"文件变更过大，请人工重点审查"。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;坑 2：AI 给出"误报"，降低信任度&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;解决方案：在 prompt 里加一句 "如果你不确定是否是问题，不要写出来"。比 "尽可能找出所有问题" 效果好很多。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="现状"&gt;现状&lt;/h2&gt;
&lt;p&gt;现在我们团队的 CR 流程是这样的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;开发提 PR&lt;/li&gt;
&lt;li&gt;AI 10 秒内完成初审，发评论&lt;/li&gt;
&lt;li&gt;reviewer 看 AI 评论 + diff，平均 8 分钟完成终审&lt;/li&gt;
&lt;li&gt;merge&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;那个之前每天催我 review 的同事 A，现在每次 AI 评论发出来，他比我更认真地研究 AI 说了什么。&lt;/p&gt;

&lt;p&gt;上周他跟我说：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"AI 给我找出了一个我自己写代码时没注意的空指针，这玩意儿比我认真。"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr&gt;
&lt;h2 id="完整代码"&gt;完整代码&lt;/h2&gt;
&lt;p&gt;完整的配置文件我放在下面，可以直接 copy 到你的项目里：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.github/workflows/ai-review.yml&lt;/code&gt;&lt;/strong&gt;（上文已贴完整版）&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.github/scripts/ai-review.js&lt;/code&gt;&lt;/strong&gt;（上文已贴完整版）&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;需要的环境变量：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;：从 Anthropic 控制台获取&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GITHUB_TOKEN&lt;/code&gt;：GitHub Actions 自带，不用手动配置&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;配置完之后，去提一个测试 PR，看着 AI 自动开始 review，还挺有成就感的。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="写在最后"&gt;写在最后&lt;/h2&gt;
&lt;p&gt;这套方案不是为了让人偷懒的——而是让&lt;strong&gt;人的精力花在刀刃上&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;机械的检查（空值、类型、格式）让 AI 做，业务逻辑、架构判断、团队规范这些需要上下文的判断，留给人。&lt;/p&gt;

&lt;p&gt;如果你的团队也有 CR 堆积的问题，可以直接拿这套配置用。有什么问题评论区说，我看到了会回。&lt;/p&gt;

&lt;p&gt;点赞收藏一下，下次出团队提效工具合集时方便你找回来。&lt;/p&gt;</description>
      <author>193577746</author>
      <pubDate>Sat, 20 Jun 2026 12:31:23 +0800</pubDate>
      <link>http://beta.w2solo.com/topics/7563</link>
      <guid>http://beta.w2solo.com/topics/7563</guid>
    </item>
  </channel>
</rss>
