一句话
React 不是"DOM 操作库"。它是:把 UI 当成 state 的纯函数——UI = f(state)。你改 state,它重新算一遍 f,再把差异写回 DOM。
三个阶段
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Trigger │ → │ Render │ → │ Commit │ → Effects
└─────────┘ └─────────┘ └─────────┘
触发 渲染 提交 副作用
- Trigger:用户点击 / 定时器 / 父组件 re-render → React 知道要重算
- Render:调用组件函数 → 返回 JSX(其实是 React Element 对象树)
- Commit:把新旧元素 diff,操作真实 DOM(这一步才碰浏览器)
- Effects:commit 完成后跑
useEffect回调
Render 是纯计算,不能有副作用(不能 setState、不能改 DOM、不能 fetch)。
为什么组件函数会跑很多次
每次 state 变化,整个组件函数从头执行一遍。这不是 bug,是设计:
function Counter() {
const [n, setN] = useState(0);
console.log('render', n); // 每次点都打一行
return <button onClick={() => setN(n + 1)}>{n}</button>;
}
新手最容易踩的坑:在组件函数里写 let x = 0; x++——下次 render x 又是 0。任何要跨 render 存活的值,必须放 state 或 ref。
Re-render 不等于 DOM 更新
function App() {
const [n, setN] = useState(0);
return <div>固定文本</div>; // 不依赖 n
}
setN(1) 会让 App 重新执行函数(render),但 diff 后发现 DOM 没变,所以不写 DOM(commit 是空操作)。这就是 React 比手动 DOM 快的原因之一:vdom diff 在内存里做,比 DOM 操作便宜。
什么时候触发 re-render
只有 4 种:
- 组件自己的 state 变化(
setState) - 组件订阅的 Context 值变化
- 父组件 re-render(哪怕 props 没变)
useReducerdispatch
props 改变本身不会触发——是父组件 re-render 顺带把子也 re-render 了。
StrictMode 为什么"渲染两次"
开发模式 + <StrictMode> 下,React 故意把组件函数和 effect 跑两遍,就是为了暴露:
- 副作用没正确清理
- 组件函数依赖外部可变状态
生产环境只跑一次。看到 console.log 出两条不要慌,关掉 StrictMode 就一次。
坑
- 不要在 render 里 setState:会死循环。要在事件回调或 effect 里。
- 闭包陷阱:effect / 事件回调里的变量是那次 render 的快照。要拿最新值用
useRef或 functional setState:setN(prev => prev + 1) - 对象 / 数组的引用:state 是
{...obj, x: 1}而不是obj.x = 1; setObj(obj)——后者引用没变,React 跳过 re-render
心智锚点
写 React 时反问自己:
- "现在是 render 阶段还是 effect 阶段?"
- "这个值需要 React 知道吗?需要 → state;不需要 → 普通变量"
- "我是不是在 render 里做了有副作用的事?"
这三问能解决新手 80% 的迷惑。
→ 下一篇 JSX 到底是什么