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

193577746(kyriewen) · 2026年08月10日 · 17 次阅读

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

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

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

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

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

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

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 在这里完全没有意义——既不能减少渲染次数,还多了一层闭包和依赖数组维护的心智成本。

// ✅ 原生元素接收的回调,不需要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 生成整个组件时自动带上的,我从来没质疑过。

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

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

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 或者忽略过期请求:

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 过滤过期响应:

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 怎么办"。

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

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

// 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、多了一层组件调用栈——纯粹增加了项目的复杂度。

// ✅ 静态内容直接内联,不需要单独组件
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 解释清楚再接受。

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

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号