面试官问"这段逻辑为什么这样写"——我才发现,这半年我写的代码,我自己都解释不了

前两天面了一家公司,前两轮聊得还行,项目经历、技术选型、团队协作都答得上来。第三轮是技术深挖,面试官打开了我简历里提到的一个项目,屏幕共享让我打开某个组件的代码。

他指着一段逻辑问我:"这里为什么用useCallback包了一层?"

我看了一眼那段代码------确实是我项目里的,但那一瞬间我脑子完全空白。因为那个组件是我用Claude Code生成的,当时prompt写的是"写一个带搜索功能的面板组件",它给我生成了整个文件,我看了一眼能跑就提交了,从来没想过为什么要用useCallback

那次面试之后我做了一件事:把最近半年写的核心代码翻了一遍,对每段逻辑问自己"为什么这样写"。结果让我出了一身冷汗。

第一个说不出来的问题:"这个useCallback是必要的吗?"

面试官问的原始代码大概长这样:

tsx 复制代码
function SearchPanel({ onSearch }) {
  const [keyword, setKeyword] = useState('');

  const handleChange = useCallback((e) => {
    setKeyword(e.target.value);
  }, []);

  const handleSubmit = useCallback(() => {
    onSearch(keyword);
  }, [keyword, onSearch]);

  return (
    <div>
      <input value={keyword} onChange={handleChange} />
      <button onClick={handleSubmit}>搜索</button>
    </div>
  );
}

面试官追问:"handleChange这里用useCallback,收益是什么?"

我当时的回答是:"防止子组件不必要的重新渲染。"

面试官又问:"input是原生DOM元素,不是React.memo包裹的子组件------原生元素会因为父组件传入的onChange引用变化而重新渲染吗?"

我愣住了。

答案是不会。 原生DOM元素(<input><div><button>)不参与React的引用比较优化,它们每次父组件渲染时都会重新执行,不管你传入的props引用有没有变。useCallback在这里完全没有意义------既不能减少渲染次数,还多了一层闭包和依赖数组维护的心智成本。

tsx 复制代码
// ✅ 原生元素接收的回调,不需要useCallback
function SearchPanel({ onSearch }) {
  const [keyword, setKeyword] = useState('');

  const handleChange = (e) => {
    setKeyword(e.target.value);
  };

  const handleSubmit = () => {
    onSearch(keyword);
  };

  return (
    <div>
      <input value={keyword} onChange={handleChange} />
      <button onClick={handleSubmit}>搜索</button>
    </div>
  );
}

useCallback只在一个场景下有意义:传给用React.memo包裹的子组件,或者作为useEffect/useMemo的依赖时需要引用稳定。传给原生元素纯属浪费。

这个错误我犯了多少次?翻了一下项目,至少12处。每一处都是AI生成整个组件时自动带上的,我从来没质疑过。

第二个说不出来的问题:"并发请求怎么处理?"

面试进入第二个代码片段,是一个搜索联想组件:

tsx 复制代码
function AutoComplete({ fetchSuggestions }) {
  const [query, setQuery] = useState('');
  const [suggestions, setSuggestions] = useState([]);

  useEffect(() => {
    if (query.length > 0) {
      fetchSuggestions(query).then((data) => {
        setSuggestions(data);
      });
    } else {
      setSuggestions([]);
    }
  }, [query]);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ul>
        {suggestions.map((s) => <li key={s.id}>{s.text}</li>)}
      </ul>
    </div>
  );
}

面试官:"如果用户快速输入'react'五个字母,'r'的请求比'react'的请求晚回来,会发生什么?"

我想了几秒才反应过来------显示的是'r'的搜索结果,而不是'react'的。竞态条件(Race Condition)

面试官接着问:"你项目里这个场景是怎么处理的?"

我诚实地说我不确定。因为这段代码也是AI一次性生成的整个组件,"能跑"之后我就没再深想。

正确的处理方式是用AbortController或者忽略过期请求:

tsx 复制代码
function AutoComplete({ fetchSuggestions }) {
  const [query, setQuery] = useState('');
  const [suggestions, setSuggestions] = useState([]);

  useEffect(() => {
    if (query.length === 0) {
      setSuggestions([]);
      return;
    }

    const controller = new AbortController();

    fetchSuggestions(query, { signal: controller.signal })
      .then((data) => {
        setSuggestions(data);
      })
      .catch((err) => {
        if (err.name !== 'AbortError') throw err;
      });

    return () => controller.abort();
  }, [query]);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <ul>
        {suggestions.map((s) => <li key={s.id}>{s.text}</li>)}
      </ul>
    </div>
  );
}

或者用一个更轻量的方案------通过请求ID过滤过期响应:

tsx 复制代码
useEffect(() => {
  if (query.length === 0) {
    setSuggestions([]);
    return;
  }

  let cancelled = false;

  fetchSuggestions(query).then((data) => {
    if (!cancelled) {
      setSuggestions(data);
    }
  });

  return () => { cancelled = true; };
}, [query]);

cleanup函数里把cancelled设为true,下一次effect执行时,上一次的请求回来了也不会更新state。

这个问题的要害在于:AI生成的代码默认跑的是happy path。 它给你一个"能工作"的版本,但不会主动替你想"如果网络慢/请求乱序/用户操作快会怎样"。而你用了半年AI,习惯了"生成→能跑→提交"的流程,渐渐不再问自己"edge case怎么办"。

第三个说不出来的问题:"为什么拆成三个组件?"

面试官打开了我项目里一个表单页面,结构大概是这样:

tsx 复制代码
// FormPage.tsx
function FormPage() {
  return (
    <FormContainer>
      <FormHeader />
      <FormBody />
      <FormFooter />
    </FormContainer>
  );
}

// FormHeader.tsx --- 只有标题和一行描述
function FormHeader() {
  return (
    <div>
      <h2>创建项目</h2>
      <p>填写以下信息创建新项目</p>
    </div>
  );
}

面试官:"FormHeader只有两行静态文本,为什么要单独抽成组件?"

我又愣住了。

诚实的答案是:当时我在Claude Code里说"生成一个创建项目的表单页面",它自动按Header/Body/Footer拆分的,我觉得"看起来很整洁"就接受了。但如果真要回答"为什么"------这里没有任何合理理由

一个组件值得被拆出去的标准:

  1. 需要复用------其他地方也用同样的结构
  2. 有自己的状态或逻辑------内部管理着独立的state或effect
  3. 是性能隔离边界------用React.memo包裹防止父组件渲染波及

FormHeader一个都不满足。它既不复用,也没有状态,更不需要性能隔离。拆出去唯一的效果是多了一个文件、多了一次import、多了一层组件调用栈------纯粹增加了项目的复杂度。

tsx 复制代码
// ✅ 静态内容直接内联,不需要单独组件
function FormPage() {
  return (
    <FormContainer>
      <div>
        <h2>创建项目</h2>
        <p>填写以下信息创建新项目</p>
      </div>
      <FormBody />
      <FormFooter />
    </FormContainer>
  );
}

这不是一个"对错"的问题------很多人会说"拆了也没坏处"。但面试官真正在问的是:你做这个决策时有没有思考过。如果你的回答是"AI这样生成的我就用了",等于告诉面试官:你不做设计决策,你只是AI的提交工具。

为什么AI用久了会出现这种状态

面试完回去我复盘了很久,问题的本质不是"我的技术退步了",而是AI改变了我的工作方式。现在用Claude Code或者Codex,一句话描述需求就能生成整个组件甚至整个页面,效率确实高了十倍------但代价是:

以前的工作流 现在的工作流
想清楚要写什么 → 一行行敲代码 一句prompt让AI生成整个组件 → 能跑 → 提交
遇到问题先想原因 遇到问题直接丢给AI重写
每行代码都是自己的决策 整个文件都是AI生成的,我只review了一遍
Code Review时能解释每个选择 Code Review时对AI生成的代码根本说不清"为什么"

关键区别:以前是"先思考后编码",现在是"描述需求→AI全盘实现→我看一眼→提交"。

AI不会替你思考"为什么"。它给你"what"------一个能跑的完整实现。但面试官问的永远是"why"------为什么这样做而不是那样做。如果你只有what没有why,在面试里就是裸奔。

面试前自检:你的代码你能解释吗?

面试后我给自己列了一个清单,翻项目代码时逐条对照:

5个信号判断你是不是"AI代理人"而不是"代码作者":

  1. 打开三个月前写的组件,能在30秒内说出"为什么用这个Hook而不是另一个"吗? 说不出来=你没做过这个决策
  2. 项目里有多少个useCallback/useMemo?去掉它们会有可测量的性能差别吗? 回答"不确定"=大概率是AI生成时惯性添加的
  3. 搜索项目里所有的useEffect,每一个的cleanup函数你都知道为什么要那样写吗? 不知道=你没思考过生命周期边界
  4. 随机打开一个你"写"的工具函数,能不看代码画出它的执行流程吗? 画不出来=那是AI写的,你只是点了accept
  5. 把你最近一次PR里的代码打印出来,不看电脑,能对着纸回答同事的追问吗? 不能=这段代码你只"拥有"但不"理解"

如果5个里面命中3个以上,说明你现在的状态是:你负责写prompt描述需求,AI负责整个实现。 面试官只要追问两次"为什么",你就露馅。

面试前3天AI代码自审清单

审查维度 问自己的问题 常见AI盲区
性能决策 每个useMemo/useCallback都有必要吗? 生成整个组件时惯性给所有函数包useCallback
边界处理 请求失败/超时/并发冲突怎么办? 只实现happy path,不主动处理edge case
组件边界 为什么这样拆分?有更简单的方式吗? 按"最佳实践模板"过度抽象
类型设计 TS类型为什么这样定义?能收窄吗? 大量any或过于宽泛的联合类型
状态管理 这个state为什么放这层?有没有可以提升/下沉的? 默认把state全塞在同一个组件里
依赖关系 useEffect的依赖数组每一项你都知道为什么需要吗? 按eslint提示全加,不思考必要性

核心原则:面试前把简历项目里AI写的代码全过一遍,对每段逻辑能回答"为什么这样做"和"如果不这样做会怎样"。 不是让你重写,是让你理解。

不是不用AI,是用了之后多问一个"为什么"

这次面试给我最大的教训不是"AI不好",而是:我把AI当成了可以全权委托的外包,但它其实只是一个不解释理由的代码生成器。

同事帮你写一段代码,你会问"为什么这样写";AI帮你生成整个组件,你看一眼能跑就merge了。这个差距在日常工作里看不出来------代码能跑,Review能过,线上不出事。但面试官专门戳的就是这个缝隙:你到底是代码的作者,还是AI的提交工具?

以后我用AI的习惯会加一步:每次AI生成完代码,花5分钟逐段问自己"如果面试官问我为什么,我怎么回答"。答不上来的------要么搞懂再提交,要么让AI解释清楚再接受。

你有没有也遇到过面试官追问"为什么"的时候答不上来的瞬间?评论区聊聊你的版本。

相关推荐
谢小飞1 小时前
大文件上传很难?学会这招,GB文件秒传不是梦
前端·node.js
一壶浊酒..2 小时前
Scrape Webpack练习
开发语言·javascript·webpack
程序员黑豆2 小时前
Windows 系统 Java 环境变量配置全攻略:解决“不是内部或外部命令”
java·前端·ai编程
To_OC2 小时前
别只拿 useRef 绑 DOM 了,搭配 Web Worker 解决页面卡顿才是真的香
前端·react.js·dom
IT_陈寒3 小时前
Vue的响应式让我原地破防,原来问题出在这
前端·人工智能·后端
子兮曰4 小时前
Bun vs Node.js 深度对决:跑分快 4 倍,真实业务只剩 3%,2026 年到底该怎么选?
前端·后端·typescript
计算机魔术师4 小时前
终端用户
前端
大龄秃头程序员4 小时前
深入梳理 UIView touch 事件全链路原理与实战
面试
码事漫谈4 小时前
把 AI 拆掉,你的系统还能跑吗?
前端·后端