React生命周期
React 的"生命周期"指的是 组件从诞生到死亡的全过程,以及在这个过程中 React 自动调用的那些回调函数 / Hook。
函数组件用 Hook(useEffect 等)表达生命周期,类组件用固定的成员函数;核心都是"挂载 → 更新 → 卸载"三个阶段。
函数组件(主流)
阶段 | 对应 Hook | 触发时机 | 常见用途 |
---|---|---|---|
挂载 | useEffect(..., []) |
组件第一次渲染到屏幕后 | 发请求、开定时器、绑定事件 |
更新 | useEffect(..., [dep1, dep2]) |
指定依赖变化并渲染后 | 发请求、根据新 props 重新计算 |
卸载 | useEffect(() => { return () => { ... } }, []) |
组件即将被移除 | 清定时器、解绑事件、取消请求 |
每次渲染后 | useEffect(...) 不加依赖 |
每次渲染完成后 | 日志、埋点 |
基本模版
jsx
useEffect(() => {
// 1. 挂载 + 更新(依赖改变)时执行
console.log('did mount / did update');
return () => {
// 2. 下一次 effect 运行前 或 卸载时执行
console.log('cleanup: will unmount / prev cleanup');
};
}, [dep]);
类组件生命周期示例
jsx
function Demo({ id }) {
useEffect(() => {
// componentDidMount
console.log('挂载/更新 id=', id);
return () => {
// componentWillUnmount
console.log('卸载/清理 id=', id);
};
}, [id]);
}
类组件(老项目/遗留项目)
三大阶段&经典方法
阶段 | 方法 | 说明 |
---|---|---|
挂载 | constructor |
初始化 state、绑定事件 |
static getDerivedStateFromProps |
罕见,根据 props 计算 state | |
render |
返回 JSX | |
componentDidMount |
第一次渲染完成;发请求、开定时器 | |
更新 | static getDerivedStateFromProps |
同上 |
shouldComponentUpdate |
返回 false 可跳过渲染(性能优化) | |
render |
重新渲染 | |
getSnapshotBeforeUpdate |
在 DOM 更新前获取滚动位置等信息 | |
componentDidUpdate |
DOM 更新完成后;可发请求、比较 prevProps | |
卸载 | componentWillUnmount |
清理工作 |
过时 API(16.3 已废弃带UNSAFE_
)
componentWillMount
componentWillReceiveProps
componentWillUpdate
新项目绝对不要使用;旧项目迁移可加UNSAFE_
前缀兼容
函数组件 vs 类组件映射速查
类生命周期 | 函数组件等价写法 | 备注 |
---|---|---|
componentDidMount |
useEffect(()=>{...}, []) |
仅执行一次 |
componentDidUpdate |
useEffect(()=>{...}, [dep]) 或 useEffect(...) |
加依赖可模拟 |
componentWillUnmount |
useEffect(()=>{ return ()=>{...} }, []) |
cleanup 函数 |
shouldComponentUpdate |
React.memo + 自定义比较 / useMemo |
性能优化 |
常见误区
useEffect
不是生命周期"一对一":它同时覆盖"didMount + didUpdate + willUnmount"的任意组合,看依赖数组。cleanup
不只卸载才执行 :依赖变化导致effect
重新运行前,先运行上一次cleanup
。- 异步请求别直接
setState
:组件已卸载再setState
会报警告 → 在cleanup
里取消请求/标记废弃。
React的Diff算法
React 的 diff 算法 是 Virtual DOM 的 reconciliation(协调)引擎,用来以最小代价把旧 DOM 树变成新 DOM 树。
"用 O(n) 时间,把两次渲染的差异算出来,再批量打补丁。"
背景
- 每次
setState
会生成一棵新的Virtual DOM
树(JS 对象)。 - 直接按新树销毁再重建真实 DOM → 性能爆炸。
- 于是
React
在内存里比两棵树,只改真正变化的部分。
三个经典约束(Trade-off)
React 故意给自己加了 三条强假设,把 O(n³) 的完全对比降成 O(n):
- 只对同级节点比(不跨层移动)
- 不同类型元素 → 整棵子树销毁重建
- 兄弟节点用
key
标识身份,维持复用
算法流程(自顶向下,深度优先)
层级对比(Tree Diff)
- 同层节点按顺序比,不回头、不跨层。
- 发现标签名不同(如
<div>
→<p>
)→ 拆掉旧树,新建新树,子节点全部丢弃不复用。
组件对比(Component Diff)
- 同类型组件 → 继续比它的render结果;
- 不同类型 → 直接unmount旧组件,mount 新组件。
元素对比(Element Diff)------ 兄弟列表最核心
对同层兄弟进行三趟指针扫描:
场景 | 行为 |
---|---|
旧列表有,新列表也有 → key 相同且类型相同 | 复用旧节点,仅移动位置 |
旧列表有,新列表无 | 删除 |
旧列表无,新列表有 | 新增 |
移动判断:用最长递增子序列(LIS) 算法,找出最少 DOM 移动次数。
Key 的作用
示例代码:
jsx
// 旧
<ul>
<li key="a">A</li>
<li key="b">B</li>
</ul>
// 新
<ul>
<li key="b">B</li>
<li key="c">C</li>
</ul>
- 有 key → React 知道 b 只是顺序变,a 被删,c 是新增 → 只做一次插入 + 一次删除。
- 用索引当 key → 看起来"省内存",实则全部复用失败,可能出现输入框错位、动画异常等 bug。 key 必须稳定、唯一、可预测(数据库 id 最佳)。
伪代码
js
function diffChildren(oldVNodes, newVNodes, parentDom) {
let oldStart = 0, newStart = 0;
let oldEnd = oldVNodes.length - 1;
let newEnd = newVNodes.length - 1;
while (oldStart <= oldEnd && newStart <= newEnd) {
if (sameNode(oldVNodes[oldStart], newVNodes[newStart])) {
patch(oldVNodes[oldStart], newVNodes[newStart]); // 复用
oldStart++; newStart++;
} else if (sameNode(oldVNodes[oldEnd], newVNodes[newEnd])) {
patch(...);
oldEnd--; newEnd--;
} else {
// 乱序 → 建 map + LIS 移动
mapRemainingOldNodes();
moveOrInsert();
}
}
// 新增剩余 / 删除剩余
}
真实 DOM 打补丁(Commit 阶段)
diff 完后得到一串最小操作队列(insert、move、update、remove),进入不可中断的 Commit 阶段,一次性应用:
- 更新文本 / 属性
- 插入/移动/删除节点
- 触发 useEffect / componentDidUpdate
Fiber 之后的优化
React 16+ Fiber 架构把 diff 过程切成可中断的小任务:
- render 阶段(找 diff)可打断、可调度优先级;
- commit 阶段(应用 diff)同步执行,保证一致性。
常见误区
误区 | 正解 |
---|---|
"diff 对比真实 DOM" | 始终对比 Virtual DOM(JS 对象) |
"key 用 index 就行" | 会导致全部节点复用失败 |
"跨层移动也高效" | 超三层移动 React 直接销毁重建整棵子树 |
"diff 完立即看到页面" | 中间还有批量更新、调度、paint |