React学习笔记(二)

164 阅读4分钟

React生命周期

React 的“生命周期”指的是 组件从诞生到死亡的全过程,以及在这个过程中 React 自动调用的那些回调函数 / Hook。

函数组件用 Hook(useEffect 等)表达生命周期,类组件用固定的成员函数;核心都是“挂载 → 更新 → 卸载”三个阶段。

函数组件(主流)

阶段对应 Hook触发时机常见用途
挂载useEffect(..., [])组件第一次渲染到屏幕后发请求、开定时器、绑定事件
更新useEffect(..., [dep1, dep2])指定依赖变化并渲染后发请求、根据新 props 重新计算
卸载useEffect(() => { return () => { ... } }, [])组件即将被移除清定时器、解绑事件、取消请求
每次渲染后useEffect(...) 不加依赖每次渲染完成后日志、埋点

基本模版

useEffect(() => {
  // 1. 挂载 + 更新(依赖改变)时执行
  console.log('did mount / did update');

  return () => {
    // 2. 下一次 effect 运行前 或 卸载时执行
    console.log('cleanup: will unmount / prev cleanup');
  };
}, [dep]);

类组件生命周期示例

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 更新前获取滚动位置等信息
componentDidUpdateDOM 更新完成后;可发请求、比较 prevProps
卸载componentWillUnmount清理工作

过时 API(16.3 已废弃带UNSAFE_

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

新项目绝对不要使用;旧项目迁移可加UNSAFE_前缀兼容

函数组件 vs 类组件映射速查

类生命周期函数组件等价写法备注
componentDidMountuseEffect(()=>{...}, [])仅执行一次
componentDidUpdateuseEffect(()=>{...}, [dep])useEffect(...)加依赖可模拟
componentWillUnmountuseEffect(()=>{ return ()=>{...} }, [])cleanup 函数
shouldComponentUpdateReact.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):

  1. 只对同级节点比(不跨层移动)
  2. 不同类型元素 → 整棵子树销毁重建
  3. 兄弟节点用key标识身份,维持复用

算法流程(自顶向下,深度优先)

层级对比(Tree Diff)

  • 同层节点按顺序比,不回头、不跨层。
  • 发现标签名不同(如<div><p>)→ 拆掉旧树,新建新树,子节点全部丢弃不复用。

组件对比(Component Diff)

  • 同类型组件 → 继续比它的render结果;
  • 不同类型 → 直接unmount旧组件,mount 新组件。

元素对比(Element Diff)—— 兄弟列表最核心

对同层兄弟进行三趟指针扫描:

场景行为
旧列表有,新列表也有 → key 相同且类型相同复用旧节点,仅移动位置
旧列表有,新列表无删除
旧列表无,新列表有新增

移动判断:用最长递增子序列(LIS) 算法,找出最少 DOM 移动次数。

Key 的作用

示例代码:

// 旧
<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 最佳)。

伪代码

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