React 面试:虚拟 DOM、声明式渲染与副作用管理

68 阅读5分钟

React 面试笔记:虚拟 DOM、声明式渲染与副作用管理

这篇笔记是我结合自己学习和问答时的思考整理的,记录我从“听不懂”到“能表达”的过程。
比起死记硬背八股文,我更希望把当时的思考逻辑写清楚:我为什么这样理解,为什么 React 要这样设计。


一、从公式开始:UI = f(state)

我当时最先接触的是这句经典公式:

UI = f(state)

React 的核心哲学其实就藏在这句话里——UI 是由状态(state)决定的纯函数映射结果
换句话说:

  • state:组件的内部状态,是驱动 UI 变化的唯一事实来源;
  • f(函数组件) :根据 state(和 props)返回虚拟 DOM 的描述;
  • UI:不是浏览器看到的真实 DOM,而是 React 内部的一份「轻量级描述」。

我最开始的理解是: UI 的一切变化都来自 state。
Hooks(比如 useEffect、useContext)虽然能影响 UI,但它们的底层逻辑最终也都是围绕 state 的变化展开。


二、声明式 vs 命令式:为什么声明式更抽离?

当时我问自己:

“React 为什么强调声明式?以前用 jQuery 不也挺灵活吗?”

然后我发现两者的差别在于抽象层级不同

命令式(如 jQuery):

每一步都得手动写:

document.querySelector('.btn').classList.add('active');

声明式(React):

只管结果:

<button className={isActive ? 'active' : ''}>点击我</button>

我记得自己当时是这么想的:
React 的声明式更像 Vue 的组合式 API,只描述「状态是什么样就显示什么样」,而不是「怎么去改成那样」。

所以声明式编程更抽离。它让 UI 和逻辑的关系变得纯粹、直接,也带来了——


三、可预测性:一切从 state 出发

我写 React 的时候,最常用的调试手段就是打开 React DevTools 看 state。

因为在 React 里:

给定相同的 state,UI 永远是一致的。

出问题时,我会按这个逻辑排查:

  1. 看 state 是否是预期值;
  2. 看 props 是否传对;
  3. 对照 UI 节点,找出不一致的部分。

所以我后来体会到:
React 的可预测性不只是理念,而是实际开发中最管用的定位方法
出 bug 时,不用去查 DOM,而是查「状态 → 渲染」的映射链。


四、关注点分离:逻辑与 UI 的分层

当时我写的总结是:

“关注点分离就是不再关注底层 DOM API,只管理业务逻辑和状态。”

以前我用原生 JS,要管 DOM 更新;
现在用 React,我只管数据,UI 是它的自然映射。

这样带来的好处我当时列了三条:

  1. 逻辑和 UI 分层,更清晰;
  2. UI 改动不会影响业务逻辑;
  3. 纯函数组件可以单测,更容易测试。

五、副作用管理:为什么要用 useEffect?

我当时有个很大的困惑:

“既然 JSX 已经渲染了,那直接改 document.body 不就行了吗?”

后来我明白:

  • JSX 只是虚拟 DOM;
  • useEffect 才是处理真实 DOM 操作、副作用、异步任务的地方。

例子:

const [theme, setTheme] = useState('light');

useEffect(() => {
  document.body.className = theme;
  return () => { document.body.className = ''; };
}, [theme]);

return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>切换主题</button>;

我的理解是:

  • useState 管数据;
  • useEffect 管副作用;
  • 渲染函数本身应该保持纯净,不能直接改 DOM。

否则 React 的 diff、批量更新、甚至 SSR 都会出问题。

一句话总结:

JSX 描述虚拟 DOM,useEffect 更新真实 DOM。


六、虚拟 DOM 与真实 DOM

我一开始也以为 JSX 渲染出来的就是 DOM,后来才理清:

概念描述
虚拟 DOMReact 内存中的 UI 描述,用来 diff
真实 DOM浏览器实际显示的内容
useEffect操作真实 DOM 或执行副作用的入口

React DevTools 里能看到的是虚拟 DOM 的状态,而最终渲染的是真实 DOM。
所以 React 先「算出差异」(diff),再「批量更新真实 DOM」。


七、状态更新的链路(以 Counter 为例)

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>+1 ({count})</button>;
}

流程:

  1. 用户点击按钮;
  2. 触发 onClick;
  3. 调用 setCount;
  4. React 检测 state 改变;
  5. 重新执行组件函数;
  6. 生成新虚拟 DOM;
  7. diff 比较差异;
  8. 更新真实 DOM。

我当时记住的是一句话:

“React 的工作就是让状态变化和 UI 更新之间的这条链路自动化。”


八、全局状态管理(Redux 对比 Pinia)

我对 Redux 的理解是从 Vue 的 Pinia 类比开始的:

  • store 就像一个全局仓库;
  • 不能直接改数据,要通过 action(“操作说明书”);
  • reducer 是总管,收到 action 后返回新 state;
  • 组件通过 useSelector 订阅状态;
  • 一旦状态变化,组件会自动重新渲染。

我当时写的记忆口诀:

dispatch = 派发命令
action = 操作说明书(type + payload)
reducer = 更新规则
useSelector = 订阅状态


九、开发前的“三个灵魂问题”

我后来总结出 React 组件开发的三个关键自检问题:

1️⃣ State:最小完备状态是什么?
哪些数据会随时间或操作变化?由谁管理?如何保持单一数据源?

2️⃣ f(组件函数):渲染逻辑是否纯净?
是否只根据 state/props 渲染?有没有副作用?

3️⃣ 事件与状态变更:
哪些交互触发状态更新?事件如何调用 setState?新 UI 如何映射回来?

这三个问题,其实就是 React 哲学的三根支柱。


十、总结

React 的核心思维:

UI = f(state)

这不仅是一句公式,而是整个框架的设计哲学。

  • 声明式:只关心结果,不管过程
  • 可预测性:同一 state 对应同一 UI
  • 关注点分离:逻辑与渲染分层
  • 副作用管理:用 useEffect 管理真实 DOM 与异步行为
  • 虚拟 DOM:通过 diffing 实现高效更新

一句话回顾我当时的理解:

React 不是在帮我“写界面”,而是在帮我“管理变化”。