面试官让我用 AI 重构一个 8 年陈的 React 组件------他说他不看代码,只看我会不会拆
他把笔记本推过来,屏幕上趴着一个 2017 年写的 Class 组件,props 17 个,state 里塞了 5 个布尔开关,生命周期里挂着 4 个异步请求,像一具裹满胶带的木乃伊。旁边 Cursor 闪闪发亮,像个跃跃欲试的殡葬师。
"25 分钟,用 AI 重构。"他说,"我不看代码,我只看你敢不敢拆。"
我当时的内心 OS:这不就是考 Prompt 工程吗?我会。我啪嗒啪嗒敲了一串"把这个 Class 组件改成函数式,优化性能",Cursor 也很争气,30 秒产出了一份看起来像 2026 年的代码:useEffect、useState、箭头函数,整整齐齐。
面试官扫了一眼,问:"原来的 bug 在哪?"
我愣住了。AI 把 bug 包上了一层现代语法糖,而我连糖纸都没撕开。
那一刻我懂了:他要的不是 AI 写得多漂亮,而是我能不能指着这堆陈年垃圾说"这里烂、那里毒、这块要切掉"。AI 是手术刀,但握刀的人得先知道病人哪根血管不能碰。
下面是我复盘出来的四步拆法,以及一张我现在逢重构必用的检查清单。
第一步:先别动代码,先画依赖图
老组件最恶心的不是行数,是它把关系藏得像前任的聊天记录。一个 props 从爷爷组件传下来,在 componentDidMount 里变成 state,在 shouldComponentUpdate 里决定生死,最后在某个回调里反向写回去------你改一行,三个地方一起中风。
AI 读这种代码,会彬彬有礼地把它"翻译"成新语法,但不会告诉你:"嘿,这里有个 props 和 state 互相代理的癌变。"
所以我的规矩是:改第一行代码之前,先画三张图。
- 数据流图:谁进来、谁出去、谁被谁改
- 副作用图:请求、定时器、事件监听、DOM 操作,一个都不能漏
- 渲染决策图:什么条件下渲染、什么条件下重渲染
面试现场我用白板,平时我用 Miro 或者直接写 markdown。别嫌麻烦,不画这三张图就动手重构,和闭着眼睛拆炸弹没区别。
画完我才知道,那个 8 年老组件真正的癌不是 Class,是 props 和 state 互相代理。A 从 props 来,B 从 A 派生,C 又在回调里写回 A。AI 重写一遍,这种代理关系只会藏得更深,Bug 从明面上的一颗痘,变成皮下的一颗瘤。
第二步:把"隐藏假设"全部揪出来
老代码里有一种 bug,堪称幽灵:代码本身没错,是它成立的前提已经烂透了。
比如这段:
jsx
componentDidMount() {
if (this.props.userId) {
fetchUser(this.props.userId).then(user => this.setState({ user }));
}
}
看着人畜无害对吧?2017 年写的时候,userId 是数字,非空,上游不会发疯。现在呢?上游可能传字符串、传空对象、传 undefined 的远房亲戚。代码没崩,是因为你的测试数据比真实世界仁慈。
我让 Cursor 做了一件它不爱做的事:别重写,先把所有默认成立但没写下来的条件列出来。
它列出了 11 条,挑几条毒的给你看:
userId是数字且非空fetchUser一定返回对象而不是 null- 组件卸载前请求一定已经完成
onSave回调一定存在isAdmin是布尔值而不是字符串"true"
这些就是隐藏假设。重构之前不把她们写成断言、类型、或者显式分支,等于把地雷从毛坯房搬到精装修房,爆炸的时候更贵。
第三步:小步拆,别搞"全量重写"的葬礼
面试官后来直言,他最烦看到候选人让 AI 一键"全量重写"。生产环境里没人能一次验证 800 行新代码,除非你想凌晨三点接 P0 电话。
我的策略是:按副作用边界拆,而不是按视觉边界拆。
原来的组件你乍一看能拆成 Header、Form、Footer 三块,拆完发现它们三个还在抢同一个 state,拆了个寂寞。真正该这么拆:
- 把数据请求抽出去 :让 hook 或数据层接管
fetchUser/saveData - 把渲染决策抽出去 :5 个布尔开关合并成 1-2 个有限状态(
idle | loading | error | success) - 把 UI 表现抽出去:Header / Form / Footer 这时候才真正独立
- 最后才把 Class 转成函数式:这反而是最不重要的一步
下面这个示例展示什么叫"先把数据请求和 UI 切开"。
原始组件(已精简,但还能闻到 2017 年的味道):
jsx
import React, { Component } from 'react';
class LegacyUserCard extends Component {
constructor(props) {
super(props);
this.state = { user: null, loading: false, error: null, editing: false };
}
componentDidMount() {
this.loadUser();
}
loadUser = () => {
this.setState({ loading: true });
fetchUser(this.props.userId)
.then(user => this.setState({ user, loading: false }))
.catch(error => this.setState({ error, loading: false }));
};
toggleEdit = () => this.setState(s => ({ editing: !s.editing }));
render() {
const { user, loading, error, editing } = this.state;
if (loading) return <p>加载中...</p>;
if (error) return <p>出错了:{error.message}</p>;
if (!user) return null;
return (
<div className="user-card">
<h3>{user.name}</h3>
{editing ? (
<input defaultValue={user.name} />
) : (
<p>{user.bio}</p>
)}
<button onClick={this.toggleEdit}>
{editing ? '保存' : '编辑'}
</button>
</div>
);
}
}
拆完第一步,数据请求滚出 UI:
jsx
import { useState, useEffect } from 'react';
// 副作用边界先封死
function useUser(userId) {
const [state, setState] = useState({ status: 'idle', user: null, error: null });
useEffect(() => {
if (!userId) return;
setState(s => ({ ...s, status: 'loading' }));
fetchUser(userId)
.then(user => setState({ status: 'success', user, error: null }))
.catch(error => setState({ status: 'error', user: null, error }));
}, [userId]);
return state;
}
// UI 只负责展示,别再碰请求
function UserCard({ userId }) {
const { status, user, error } = useUser(userId);
const [editing, setEditing] = useState(false);
if (status === 'loading') return <p>加载中...</p>;
if (status === 'error') return <p>出错了:{error.message}</p>;
if (status !== 'success' || !user) return null;
return (
<div className="user-card">
<h3>{user.name}</h3>
{editing ? <input defaultValue={user.name} /> : <p>{user.bio}</p>}
<button onClick={() => setEditing(e => !e)}>
{editing ? '保存' : '编辑'}
</button>
</div>
);
}
这一步没解决所有问题,但已经做到两件事:
- 5 个布尔变量收敛成 1 个有限状态机
- UI 不再直接碰数据层,后面换 GraphQL、TanStack Query、RTK,都只改 hook
第四步:给 AI 下"可验收"的指令
面试中我最大的转变,是 Prompt 从"帮我重构"变成"完成这个可验证的小任务"。
别这样写:
"帮我把这个 Class 组件改成函数组件,优化性能。"
AI 听完会给你一个"看起来对"的答案,然后你在凌晨三点发现它把 componentWillUnmount 里的清理逻辑吃掉了。
要这样写:
"这个组件用了 5 个 state 变量控制请求/编辑/错误状态。请把它们合并成 1 个 status 状态机(idle/loading/success/error),并保证:1)原来所有渲染分支都不丢失;2)卸载时不会 setState;3)给出 3 个单元测试用例覆盖状态转换。"
差别在哪?后者有验收标准。 AI 写出来对不对,你扫一眼就知道。
我把这套话术整理成了一张检查清单,每次重构老组件之前都先让 AI 按这个顺序来:
AI 重构老组件检查清单(建议收藏)
| 步骤 | 要 AI 做的事 | 验收标准 |
|---|---|---|
| 1. 依赖梳理 | 列出所有 props / state / 副作用 / 外部依赖 | 输出 markdown 表格,不许写"等等" |
| 2. 假设暴露 | 找出默认成立但没有断言的条件 | 列出 ≥5 条,标风险等级 |
| 3. 状态机收敛 | 把多个布尔控制变量合并成有限状态 | 状态数 ≤3 个,覆盖所有渲染分支 |
| 4. 副作用边界 | 把请求/定时器/监听抽到 hook 或工具函数 | UI 组件不再直接调用 fetch/setInterval |
| 5. 最小可测步 | 每次只重构一个边界,生成对应测试 | 每步改动 ≤80 行,有 1-2 个测试 |
| 6. 行为回归 | 对比重构前后输入输出 | 列出 3 组相同输入下的预期相同输出 |
| 7. 性能断言 | 只优化有测量数据的性能问题 | 必须给出 before/after 的 render 次数或耗时 |
| 8. 删除而非隐藏 | 确认删除的代码真的没人用 | 全局搜索被删除的函数/变量名 |
这张表不是用来让 AI 更聪明的,是用来防止你被 AI 唬住的。面试官说的"看你会不会拆",翻译过来就是:你能不能给出清晰的边界和验收标准,而不是对着 AI 的输出点头。
面试最后,他问了我一个灵魂问题
"如果 AI 给的答案看起来很完美,但你不知道它为什么对,你敢不敢用?"
我说不敢。
他点头:"那就可以。重构不是重写,是把不懂的东西拆成懂的。AI 能写代码,但拆代码的责任只能是人。"
我拿到了 offer。更实在的是,我把这套四步法带进了真实项目。上周刚拆了一个 2019 年的表单组件,没有一次大提交,全是小步验证,rollback 成本接近零。
最后送你一句话:AI 可以帮你写得更快,但只有你知道烂在哪里,才不会在事故复盘会上被骂得更快。
上面的检查清单建议收藏。你最近一次重构老组件,是先画依赖图,还是直接让 AI 全量重写了?