Skip to content

React 18 ​

导航目录 ​

什么是 React? ​

React 是一个用于构建用户界面的 JavaScript 库,核心思想是通过组件化和声明式编程描述 UI。

它具有以下特点:

  • 组件化:将页面拆分为独立、可复用的组件,便于开发、测试和维护。
  • 声明式:开发者只需描述状态对应的界面,React 负责完成视图更新。
  • 高效更新:通过虚拟 DOM、协调过程和 Diff 策略,尽量减少不必要的真实 DOM 操作。
  • 单向数据流:数据通常由父组件传递给子组件,使状态变化更加清晰、可预测。
  • 生态丰富:可结合路由、状态管理和服务端渲染方案构建完整应用。
  • 跨平台:除 Web 外,还可借助 React Native 等技术开发移动端应用。

需要注意:React 主要负责视图层,并不是一个开箱即用的完整前端框架。

React 能干什么? ​

React 主要用于构建交互式用户界面,常见应用场景包括:

  • 构建大型 Web 应用:通过组件化拆分复杂页面,提高代码的复用性和可维护性。
  • 开发单页应用:配合 React Router 等工具实现页面路由与无刷新切换。
  • 实现复杂交互界面:利用状态驱动视图,处理表单、列表、弹窗和实时数据等场景。
  • 服务端渲染与全栈开发:结合 Next.js 等框架实现 SSR、SSG 和全栈应用。
  • 跨平台应用开发:通过 React Native 构建 Android 和 iOS 应用。

组件化 ​

  • 组件化是将复杂页面拆分为多个独立、可复用组件的开发方式。每个组件负责特定的界面、状态和交互逻辑,再通过组合形成完整应用。

组件化具有以下优势:

  • 职责清晰:每个组件只关注自身功能,有利于实现高内聚。
  • 降低耦合:组件通过 Props、事件或 Context 等明确接口协作,减少内部实现之间的依赖。
  • 提高复用性:通用组件可以在不同页面和业务场景中重复使用。
  • 便于维护:修改某个组件时,通常不会影响其他无关模块。
  • 易于测试:可以单独测试组件的渲染结果和交互行为。
  • 支持组合:通过组合小型组件逐步构建复杂界面。

diff 算法时间复杂度 ​

  • 传统 diff 算法:

    • 通用树 Diff 需要比较两棵树中节点之间的多种对应关系,并计算插入、删除、移动等操作。
    • 时间复杂度可达到 O(n³);当节点数量较多时,计算成本很高,不适合频繁更新的 UI 场景。
  • React diff:

    • React 只对同层节点进行比较,不进行跨层级匹配。
    • 通过同层比较、元素类型判断和 key 匹配,将常见场景下的 Diff 时间复杂度降低到O(n)。
    • 如果同一位置的新旧元素类型相同,React 会复用已有节点,并继续比较属性和子节点。
    • 如果元素类型不同,React 会卸载旧子树并创建新子树,不再继续比较两棵不同类型的子树。
    • 在列表比较中,React 使用 key 判断节点能否复用,并识别新增、删除和移动的节点。

什么是 JSX ​

TIP

在 React17 以前,babel 是调用的 React.createElement,并且页面需要引入

在 React17 之后,babel 是调用的(react/jsx-runtimejsx)函数,并且页面无需引入

  • Jsx(JavaScript XML)是一个语法糖,是对 JavaScript 的扩展,即可以写 html 也可以写 js
  • 浏览器支持的是 ECMAScript 标准,所以代码在编译阶段会通过babel-preset-react转换成 ReactElement
  • ReactElement 就是我们所说的虚拟节点

createPortal ​

  • 将组件渲染到对应的容器中ReactDOM.createPortal(child, container)
  • 常用于弹窗、抽屉、全局提示和 Tooltip 等需要脱离父元素布局或层叠上下文的场景。

虚拟的 dom 优缺点 ​

优点 ​

  • 简化开发:通过声明式方式描述 UI,减少手动操作真实 DOM,并由 React 统一处理属性、事件及部分浏览器兼容问题。
  • 差异化更新:状态变化后通过 Diff 找出差异,只提交必要的真实 DOM 更新。
  • 跨平台:虚拟 DOM 不直接依赖浏览器,可通过不同渲染器支持 Web、Android、iOS 等平台。

缺点 ​

  • 不利于 SEO:虚拟 DOM 是在浏览器中运行的,无法直接用于服务端渲染,影响首屏加载速度和 SEO。
  • 首屏加载速度慢:首次渲染时需要创建完整的虚拟 DOM 树,再生成真实 DOM,增加了加载时间。

函数组件和类组件的区别 ​

特性函数组件类组件
定义方式JavaScript 函数ES6 类继承 React.Component
状态管理useState/useReducer Hooksthis.state + this.setState
生命周期useEffect Hook生命周期方法(componentDidMount等)
this 绑定无 this,避免绑定问题需要处理 this 绑定
代码复用自定义 Hooks高阶组件(HOC)或 Render Props
性能优化React.memo + useMemo/useCallbackPureComponent/shouldComponentUpdate
错误边界❌ 不支持(需配合类组件)✅ 支持(componentDidCatch)
实例引用useRef + useImperativeHandle直接通过 ref 获取实例
心智模型函数式编程(无副作用)面向对象编程(实例化)

状态管理机制 ​

jsx
// 函数组件(Hooks)
const Counter = () => {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c+1)}>{count}</button>;
}

// 类组件
class Counter extends React.Component {
  state = { count: 0 };
  render() {
    return <button onClick={() => this.setState({count: this.state.count+1})>
      {this.state.count}
    </button>
  }
}
  • 核心差异:函数组件状态是闭包环境中的独立值,类组件状态是实例的 this.state 属性

生命周期实现 ​

jsx
// 函数组件(useEffect)
useEffect(() => {
  // componentDidMount + componentDidUpdate
  fetchData();
  return () => { /* componentWillUnmount */ }
}, [dependencies]);

// 类组件
componentDidMount() { fetchData(); }
componentDidUpdate(prevProps) {
  if (prevProps.id !== this.props.id) fetchData();
}
componentWillUnmount() { cleanup(); }
  • 核心差异:函数组件通过声明式依赖数组控制副作用,类组件需要手动比较 prevProps

this 绑定问题 ​

jsx
// 类组件典型问题
class Button extends React.Component {
  handleClick() {
    // this 可能为 undefined
    console.log(this.props);
  }

  // 解决方案1: 构造函数绑定
  constructor() {
    this.handleClick = this.handleClick.bind(this);
  }

  // 解决方案2: 箭头函数
  handleClick = () => { ... }
}
  • 函数组件优势:天然规避 this 绑定问题,所有值通过闭包获取

逻辑复用方式 ​

jsx
// 函数组件(自定义 Hook)
const useLogger = (value) => {
  useEffect(() => {
    console.log("Value changed:", value);
  }, [value]);
};

// 类组件(HOC)
const withLogger = (Comp) => {
  return class extends React.Component {
    componentDidUpdate(prevProps) {
      console.log("Props changed:", this.props);
    }
    render() {
      return <Comp {...this.props} />;
    }
  };
};

React 错误边界处理 ​

  • 目前还没有办法将错误边界编写为函数式组件。但是你不必自己编写错误边界类。例如,你可以使用 react-error-boundary 包来代替。
js
import React, { Component } from "react";

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error, info) {
    // 这里可以添加错误日志上报逻辑
    console.error("Error caught by ErrorBoundary:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 显示备用 UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

export default ErrorBoundary;

function App() {
  return (
    <div>
      <ErrorBoundary>
        <h1>Hello, world!</h1>
      </ErrorBoundary>
    </div>
  );
}

React 单向数据流 ​

  • React 单向数据流是 React 的核心原则之一,它描述了数据的流动方向:从父组件通过 props 流向子组件,子组件不能直接修改 props,只能通过父组件传递的回调函数来请求父组件修改数据。

    • 优点:
      • 1、可预测性:数据流动方向单一,容易跟踪和调试。
      • 2、易于维护:组件的状态变化有明确的来源和改变方式。
      • 3、组件隔离:每个组件只关心自己的状态和接收的 props,降低了组件之间的耦合。

受控组件与非受控组件 ​

受控组件与非受控组件的核心区别是:表单数据由谁管理。

  • 受控组件:表单数据由 React 的状态管理。
  • 非受控组件:表单数据由 DOM 自身,React 通过 ref 在需要时读取。

在大多数业务表单中,建议优先使用受控组件。文件上传、接入第三方 DOM 库等需要直接访问 DOM 的场景,可以使用非受控组件。

受控组件 ​

受控组件是指表单元素的当前值由 React 状态决定。

React 将状态通过 value 或 checked 传递给表单元素;用户操作表单时 onChange 更新状态,再由新的状态更新页面。

它的数据流如下:

text
用户输入 → 触发 onChange → 更新 state → React 重新渲染 → 更新表单值
tsx
import { useState } from "react";

const Controlled = () => {
  const [value, setValue] = useState<string>("hulei");

  const getValue = () => {
    console.log(value);
  };

  return (
    <>
      <div>受控组件</div>

      <input
        type="text"
        value={value}
        onChange={(event) => setValue(event.target.value)}
      />

      <p>当前值:{value}</p>
      <button onClick={getValue}>获取值</button>
    </>
  );
};

受控组件具有以下特点:

  1. 表单元素的值保存在 React 状态中。
  2. React 状态是表单数据的唯一数据源。
  3. 用户输入时,需要通过 onChange 更新状态。
  4. 状态更新后,React 会重新渲组件并更新表单值。
  5. 便于实现实时校验、输入格式化和表单联动。

常见使用场景:

  • 登录、注册等普通业务表单。
  • 实时校验用户输入。
  • 根据输入内容控制其他组件。
  • 根据表单状态决定是否允许提交。
  • 对输入内容进行格式化。

非受控组件 ​

非受控组件是指表单元素的当前值由 DOM 自身维护,React 不使用状态控制它的每一次变化。

React 通常通过 ref 获取表单元素,并在提交或其他特定时机读取它的当前值。

它的数据流如下:

text
用户输入 → DOM 保存数据 → React 通过 ref 读取数据
tsx
import { useRef } from "react";

const Uncontrolled = () => {
  const inputRef = useRef<HTMLInputElement>(null);

  const getValue = () => {
    console.log(inputRef.current?.value);
  };

  return (
    <>
      <div>非受控组件</div>

      <input
        ref={inputRef}
        type="text"
        defaultValue="hulei"
      />

      <button onClick={getValue}>获取值</button>
    </>
  );

非受控组件具有以下特点:

  1. 表单元素的值由 DOM 自身维护。
  2. React 不会同步保存用户的每一次输入。
  3. 通常使用 ref 获取表单元素的当前值。
  4. 使用 defaultValue 或 defaultChecked 设置初始值。
  5. 修改 defaultValue 不会更新已经挂载的表单元素。

常见使用场景:

  • 文件上传。
  • 接入需要直接操作 DOM 的第三方库。
  • 只需要在提交时获取表单数据。
  • 将已有的非 React 表单迁移到 React。

文件上传 ​

在 React 中,文件输入框始终属于非受控组件。

这是因为文件输入框的值只能由用户选择,不能通过 React 状态直接设置。React 需要通过 ref 读取用户选择的文件。

tsx
import { useRef } from "react";

const FileUpload = () => {
  const fileRef = useRef<HTMLInputElement>(null);

  const upload = () => {
    const file = fileRef.current?.files?.[0];

    if (file) {
      console.log(file);
    }
  };

  return (
    <>
      <input ref={fileRef} type="file" />

      <button onClick={upload}>获取文件</button>
    </>
  );
};

React 时间分片的大概原理 ​

  • React 时间分片的核心,是把一次耗时的渲染任务拆分成多个小的工作单元,分批执行,避免长时间占用浏览器主线程。 以 60Hz 屏幕为例,每帧大约只有 16.6ms。在这段时间内,浏览器不仅要执行 JavaScript,还要完成样式计算、布局和绘制。如果 JavaScript 长时间占用主线程,浏览器就无法及时渲染和响应用户操作,页面会出现卡顿。

  • React 在Fiber 架构下把渲染任务拆成多个工作单元,并通过 Scheduler 进行调度。执行过程中,React 会判断是否需要让出主线程;如果有更高优先级的任务,例如用户输入,就可以暂停当前渲染,优先处理紧急任务,之后再继续或重新执行。 需要注意:时间分片不会减少总计算量,只是把连续执行改成分段执行;而且只有 Render 阶段可以中断,操作真实 DOM 的 Commit 阶段仍然是同步且不可中断的。

lifeofframe

为什么会出现 fiber ​

Fiber 出现的主要原因,是解决旧版 React 同步递归渲染无法中断的问题。

  • React 16 以前采用 Stack Reconciler。组件树一旦开始更新,就会通过递归持续执行,直到整棵树计算完成。如果组件树很大,JavaScript 会长时间占用主线程,浏览器无法及时处理用户输入、动画和页面绘制,从而出现卡顿。

  • React 16 引入 Fiber 架构,将每个节点的渲染工作拆成独立的 Fiber 工作单元,并通过 child、sibling、return 等指针组成可遍历的数据结构。React 因此可以用循环逐个处理工作单元,并保存执行进度,使 Render 阶段能够暂停、恢复、放弃或重新执行。

  • 结合 Scheduler,React 还可以根据任务优先级进行调度:高优先级任务可以优先执行,低优先级渲染可以延后,从而提高页面响应性。

一句话总结:Fiber 将不可中断的递归渲染改造成可调度的工作单元,为时间分片、优先级调度和并发渲染提供了基础。

fiber 数据结构 ​

  • fiber 是一个链表的数据结构,他的 child 指向第一个子节点,sibling 指向最近弟弟,return 指向父 fiber,即使是执行被打断,也很容易恢复

Fiber结构

优先级 ​

scheduler 优先级 ​

  • scheduler 优先级有 5 种,每一个优先级对应一个过期时间,过期时间越短,对应优先级越高
js
// 无优先级
export const NoPriority = 0;
// 立即执行(如点击按钮触发的同步操作)
export const ImmediatePriority = 1;
//阻塞用户的操作(如输入框输入、拖拽) ,用户输入(等待时间250ms)
export const UserBlockingPriority = 2;
// 普通操作(如网络请求完成后的回调)(等待时间5000ms)
export const NormalPriority = 3;
// 低优先级(如非紧急的后台计算)(等待时间10000ms)
export const LowPriority = 4;
// 空闲时执行(如日志上报、非关键的统计)(没有过期时间)
export const IdlePriority = 5;

Lane ​

  • Lane 一共有 31 条车道,对应 5 种事件优先级
js
/**
 * 一共有31条车道
 */
const TotalLanes = 31;
//没有车道,所有位都为0
const NoLanes = 0b0000000000000000000000000000000;
const NoLane = 0b0000000000000000000000000000000;
//同步车道,优先级最高
const SyncLane = 0b0000000000000000000000000000001;
const SyncBatchedLane = 0b0000000000000000000000000000010;
//离散用户交互车道 click
const InputDiscreteHydrationLane = 0b0000000000000000000000000000100;
const InputDiscreteLanes = 0b0000000000000000000000000011000;
//连续交互车道 mouseMove
const InputContinuousHydrationLane = 0b0000000000000000000000000100000;
const InputContinuousLanes = 0b0000000000000000000000011000000;
//默认车道
const DefaultHydrationLane = 0b0000000000000000000000100000000;
const DefaultLanes = 0b0000000000000000000111000000000;
//渐变车道
const TransitionHydrationLane = 0b0000000000000000001000000000000;
const TransitionLanes = 0b0000000001111111110000000000000;
//重试车道
const RetryLanes = 0b0000011110000000000000000000000;
const SomeRetryLane = 0b0000010000000000000000000000000;
//选择性水合车道
const SelectiveHydrationLane = 0b0000100000000000000000000000000;
//非空闲车道
const NonIdleLanes = 0b0000111111111111111111111111111;
const IdleHydrationLane = 0b0001000000000000000000000000000;
//空闲车道
const IdleLanes = 0b0110000000000000000000000000000;
//离屏车道
const OffscreenLane = 0b1000000000000000000000000000000;

事件优先级 ​

js
//离散事件优先级 click onchange
export const DiscreteEventPriority = SyncLane; //1
//连续事件的优先级 mousemove
export const ContinuousEventPriority = InputContinuousLane; //4
//默认事件车道
export const DefaultEventPriority = DefaultLane; //16
//空闲事件优先级
export const IdleEventPriority = IdleLane;

React 18 渲染流程核心思想 ​

React 18 渲染流程的核心是基于 Fiber 架构,将一次更新拆分为 Render(协调)和 Commit(提交)两个阶段。

在并发渲染模式下,React 可以根据更新优先级调度 Render 工作,在浏览器需要处理更高优先级任务时暂停、恢复或放弃当前渲染结果。Commit 阶段则始终同步,以避免用户看到不完整的界面。

一句话速记:调度更新(Lane 定优先级)→ Render(beginWork 向下、completeWork 向上,可打断)→ Commit(Before Mutation → Mutation → Layout,同步不可中断)→ Passive Effects(useEffect,绘制后异步执行)。

一、调度更新 ​

状态更新产生后,React 会执行以下工作:

  1. 创建 Update,并通过 Lane 记录更新优先级;
  2. 将更新加入对应 Fiber 的更新队列;
  3. 从当前节点向根节点标记待处理的 Lane;
  4. 根据根节点中待处理任务的优先级安排同步或并发渲染。

高优先级更新可以打断低优先级的并发渲染。被打断的工作可能稍后继续,也可能由于数据发生变化而被放弃并重新计算。

二、Render 阶段:计算下一棵 Fiber 树 ​

Render 阶段的目标是基于当前 Fiber 树和最新状态,构建 workInProgress Fiber 树,并记录需要在 Commit 阶段执行的副作用。

双缓存(Double Buffering)机制

React 在内存中同时维护两棵 Fiber 树,通过 alternate 指针互相引用:

  • current 树:当前已渲染到屏幕上的树,root.current 指向它;
  • workInProgress 树:正在内存中构建的下一棵树。

Render 阶段所有的计算、diff、打 flag 都发生在 workInProgress 树上,不影响屏幕上的 current 树。构建时会尽量复用 current 树对应节点的 alternate,避免重复创建对象。

当 workInProgress 树构建完成并在 Commit 阶段应用后,React 只需一步将 root.current 指向它,即完成「树的切换」——旧的 current 树则作为下一次更新的 workInProgress 缓存复用。这种「两棵树来回切换」的方式就是双缓存,好处是在内存中完成构建、一次性切换,避免用户看到中间的不完整状态。

该阶段主要包含两个过程:

1. beginWork:递阶段,向下遍历 ​

React 从根节点开始深度优先遍历,负责构建 Fiber、生成子 Fiber,并执行 diff:

  1. 执行函数组件或类组件的 render,得到新的 React Element;
  2. 将新 Element 与旧 Fiber 进行 diff,根据节点类型和 key 判断复用、新建或删除;
  3. 给 Fiber 打上 Placement(新增 / 移动)、Deletion(删除)这类节点结构相关的 flags;
  4. 生成子 Fiber,继续向下处理。

注意:beginWork 只关心节点结构,不处理原生节点自身的属性(属性变更留到 completeWork)。

JSX 通常已在编译阶段转换为创建 React Element 的代码,并不是在 Render 阶段临时解析。

2. completeWork:归阶段,向上归并 ​

当一个节点的子节点全部处理完后,React 回头「完成」这个 Fiber:

  1. 碰到原生的 HostComponent / HostText 节点时,对比新旧 props、文本;
  2. 若属性或文本发生变化 → 给 Fiber 打上 Update flag;
  3. 执行 bubbleProperties,把子树所有 flags 聚合,向上冒泡更新父节点的 subtreeFlags;
  4. 逐层返回,直到完成整棵 workInProgress 树。

Flags 表示当前 Fiber 自身需要执行的副作用,subtreeFlags 表示其子树是否包含副作用,Commit 阶段可以据此跳过没有副作用的子树。

Render 阶段只进行计算,不应该产生可观察的外部副作用。并发渲染可能暂停、重新执行或放弃尚未提交的结果,因此组件渲染逻辑必须保持纯粹。

三、Commit 阶段:提交更新 ​

当 workInProgress 树构建后,React 进入 Commit 阶段。该阶段同步执行且不可中断,以保证界面不会处于部分更新状态。

Commit 阶段主要包含以下三个子阶段。

1. Before Mutation ​

在修改 DOM 之前执行:

  • 调用类组件的 getSnapshotBeforeUpdate;
  • 读取 DOM 更新前的状态;
  • 处理部分与焦点、选区相关的准备工作。

该阶段适合读取修改前的 DOM 信息,但不执行主要的 DOM 变更。

2. Mutation ​

执行实际的宿主环境变更:

  • 插入、更新和删除 DOM;
  • 处理 ref 的解绑;
  • 执行 useInsertionEffect;
  • 执行上一轮 useLayoutEffect 的清理函数。

Mutation 完成后,新的 DOM 结构已经生成,但浏览器通常还没有机会绘制。

随后 React 将根节点的 current 指向 finishedWork,使刚完成的 workInProgress 树成为新的 current 树。该切换发生在 Mutation 之后、Layout Effects 之前,因此 Layout 阶段可以访问最新 Fiber 树和 DOM。

3. Layout ​

DOM 更新完成后 React 同步执行:

  • 设置新的 ref;
  • 执行 useLayoutEffect 的创建函数;
  • 调用 componentDidMount;
  • 调用 componentDidUpdate;
  • 执行部分同步回调。

useLayoutEffect 会在浏览器获得绘制机会前执行。如果其中包含耗时操作,将直接延迟页面绘制。

Commit 阶段结束后,主线程才有机会进行样式计算、布局和绘制。React 本身不会直接调用浏览器的重绘流程。

四、Passive Effects ​

useEffect 属于 Passive Effects,不在主要 DOM Mutation 和 Layout Effects 中执行。

通常情况下,React 会在提交完成后调度 Passive Effects:

  1. 执行上一次 useEffect 的清理函数;
  2. 执行本次 useEffect 的创建函数。

Passive Effects 通常不会阻塞当前提交的页面绘制,适合处理数据请求、事件订阅、日志上报等不需要在绘制前完成的副作用。

但“useEffect 一定在浏览器绘制后执行”并不是严格保证。在离散交互、同步刷新或后续更新触发提前清理时,React 可能在绘制前处理 Passive Effects。因此,不应使用 useEffect 执行必须严格等待绘制完成的逻辑。

React 的 Diff 原理 ​

React 的 Diff 算法主要分为以下几部分:

1、同层比较:仅比较同一层级的节点,不跨层级移动

2、复用优先:当 key 和 type 相同时复用老 Fiber 节点,否则创建新的 Fiber 节点

3、多场景优化:针对单节点和多节点不同处理策略

diff_1

一句话速记:只比同层,key + type 相同就复用;单节点找到即复用、其余兄弟标删;多节点走「两轮遍历 + lastPlacedIndex 判断是否右移」。

一、单节点 Diff 流程 ​

diff2

核心思想:新节点是「单个」,所以只要在老 Fiber 兄弟链中找到一个 key + type 都相同的即可复用,其余兄弟全部标删;找不到就新建。

  • 1、循环老的 Fiber 兄弟节点,如果某个老 Fiber 的key和类型(type)都与新虚拟节点一致,复用该老 Fiber,并把它剩下的所有兄弟节点标记为删除
  • 2、如果当前老 Fiber 的key与新节点不同,说明肯定不是同一个,把当前这个老 Fiber 标记删除,继续检查下一个兄弟
  • 3、特殊情况:如果key相同但type不同,因为 key 唯一,后面不可能再匹配,此时直接把当前及剩余所有兄弟全部标删并跳出循环
  • 4、如果兄弟节点全部循环完毕仍未找到可复用的 Fiber,则根据新的虚拟 DOM 创建新的 Fiber 节点

记忆要点:单节点比较先比 key,key 相同再比 type。key 不同 → 删当前、看下一个;key 相同 type 不同 → 直接放弃剩余全部。

js
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
  let child = currentFirstChild;

  while (child !== null) {
    // 1. key 相同才有复用可能
    if (child.key === element.key) {
      if (child.type === element.type) {
        // 1.1 key + type 都相同:复用,并删除它剩下的所有兄弟
        let nextChild = child.sibling;
        while (nextChild) {
          deleteChild(returnFiber, nextChild);
          nextChild = nextChild.sibling;
        }
        const existing = useFiber(child, element.props);
        existing.return = returnFiber;
        return existing;
      }
      // 1.2 key 相同但 type 不同:后面不可能再匹配,删掉当前及所有剩余兄弟并跳出
      deleteRemainingChildren(returnFiber, child);
      break;
    }

    // 2. key 不同:删除当前节点,继续找下一个兄弟
    deleteChild(returnFiber, child);
    child = child.sibling;
  }

  // 3. 未找到可复用节点:创建新节点
  const created = createFiberFromElement(element);
  created.return = returnFiber;
  return created;
}

二、多节点 Diff 流程(多节点分为三种情况) ​

diff3

核心思路:两轮遍历 React 认为「同位置更新」比「移动」更常见,所以先用第一轮遍历处理顺序不变的节点(逐个按位置对比,key/type 相同就复用,一旦不匹配立即跳出),再用第二轮遍历借助 map 处理剩下乱序的节点。

第一轮遍历结束后,会出现三种情况:

  • 1、老的 Fiber 全部能被复用,新的节点还没有遍历完成(新的节点比老的多)
  • 2、老的 Fiber 复用了部分,新的节点已经遍历完了(新的节点比老的少)
  • 3、老的 Fiber 和新的虚拟 DOM 都没有循环完毕(根据老的 Fiber 生成 map 对象,遍历还未完成的虚拟 DOM,最大程度的复用)

diff2

  • 第一轮比较 A 和 A,相同可以复用,更新,然后比较 B 和 C,key 不同直接跳出第一个循环
  • 把剩下 oldFiber 的放入 existingChildren 这个 map 中
  • 然后声明一个 lastPlacedIndex 变量,表示不需要移动的老节点的索引,默认为 0
  • 继续循环剩下的虚拟 DOM 节点,从 C 开始
  • 如果能在 map 中找到相同 key 相同 type 的节点则可以复用老 fiber,并把此老 fiber 从 map 中删除
  • 如果能在 map 中找不到相同 key 相同 type 的节点则创建新的 fiber 节点
  • 如果是复用老的 fiber,则判断老 fiber 的索引是否小于 lastPlacedIndex
  • 如果小于 lastPlacedIndex 则需要移动老 fiber,lastPlacedIndex 不变
  • 如果大于 lastPlacedIndex 则不需要移动老 fiber,更新 lastPlacedIndex 为老 fiber 的 index
  • 虚拟 DOM 循环结束后把 map 中所有的剩下的 fiber 全部标记为删除

lastPlacedIndex 一句话记忆:它记录「已确定不用动的老节点的最大 index」。新序列从左往右扫,复用的老节点 index 比它大 → 相对位置没变(不动,更新 lastPlacedIndex);比它小 → 该节点在老树里靠前、却要排到后面,必须右移(打 Placement)。这也是 React「节点尽量往右移、避免整体前移」策略的由来。

为什么 key 很重要 / 别用 index 当 key 上面所有复用判断都依赖 key 定位老 fiber。若用数组 index 当 key,一旦列表发生插入、删除或排序,同一个 index 会对应到不同的数据,导致 React「错误复用」——状态错位、输入框内容串行、无谓的重新渲染。稳定唯一的 key 是 Diff 高效且正确复用的前提。

循环链表 ​

  • 实现思路:

    • 1、当 fiber 更新队列为空时,让新的队列queue1.next指向头节点,让头节点指向queue1
    • 2、当queue2进入队列时,让queue2.next指向头节点的next(queue1),让queue1.next指向queue2(形成循环链表结构),让头节点指向 queue2
    • 3、当queue3进入队列时,让queue3.next指向头节点(queue2)的next(queue1),让头节点(queue2.next)指向queue3,让头节点指向 queue3
  • 目的:即可以保证链表的顺序也可以减少放入队列的循环次数,在更新状态的时候,需要从第一个进入的队列开始遍历,这里在添加队列的时候只需要改变 2 个指针,在遍历的时候找最后一个队列的下一个就是头节点,并且让最后一个 next 指向 null

循环链表

合成事件 ​

合成事件(SyntheticEvent)是 React 对原生 DOM 事件的一层跨浏览器封装。设计目的主要有四点:

  • 1、跨浏览器一致性:统一不同浏览器的事件处理接口
  • 2、性能优化:通过事件委托减少内存消耗
  • 3、便捷开发:提供更符合 React 理念的事件处理方式
  • 4、安全控制:防止事件滥用导致的 XSS 攻击

一句话速记:所有事件委托到根容器(16 是 document、17+ 是 #root)→ 触发时按 target 向上收集路径上的处理函数 → 包装成 SyntheticEvent → 依次模拟捕获、冒泡执行。

合成事件的实现原理 ​

1. 事件注册 ​

  • React 采用事件委托:并不给每个 DOM 节点单独绑事件,而是把事件统一代理到一个根容器上。
  • **注意委托目标的变化 **:React 16 及以前统一委托到 document;React 17 起改为委托到 ReactDOM.render 的根容器节点(#root)。这样做便于多个 React 版本共存、也更适配微前端等嵌套场景。
  • React 并非一开始就注册所有事件,而是惰性注册:当组件中第一次用到某个事件(如 onClick)时,才在根容器上注册对应的原生事件类型。

2. 事件存储 ​

  • React 会维护一个映射,将事件类型与组件的事件处理函数关联起来。这个映射存储在组件的 fiber 节点上(具体是在 fiber 的memoizedProps和pendingProps中)。

3. 事件触发 ​

当事件在 DOM 上触发时(如用户点击),事件会冒泡到 root 节点。

  • root 节点上绑定的事件监听器(由 React 注册)会被触发。
  • React 通过事件对象(原生事件)的event.target找到实际触发事件的 DOM 节点。
  • 然后,React 从这个 DOM 节点向上遍历,收集所有与事件类型相关的处理函数(例如,在onClick事件中,收集所有节点的onClick处理函数)。这个遍历过程会考虑事件是否被阻止冒泡(stopPropagation)。
  • 接着,React 构造一个合成事件对象(SyntheticEvent),并依次调用收集到的事件处理函数

4. 事件对象池(React 17 起已移除) ​

  • 旧版本(≤ React 16):为减少 GC,React 使用事件池复用合成事件对象,回调执行完后会把其属性置空。因此想在异步(如 setTimeout、Promise)里访问事件属性,必须先调用 event.persist() 保留它。
  • 新版本(≥ React 17):事件池已被移除,合成事件对象不再被复用、属性也不会被清空,可以直接在异步中访问。event.persist() 仍保留但已成为空操作(no-op),无需再调用。

记忆点:「为什么 setTimeout 里拿不到 event」——那是 React 16 及以前的事件池行为;17 之后已不存在这个坑。

合成事件与原生事件的区别: ​

  • 合成事件的命名采用小驼峰(camelCase),而不是纯小写。
  • 在合成事件中,返回false不会阻止默认行为,必须显式调用preventDefault。
  • 由于事件委托,在组件卸载后,React 会自动移除事件处理,避免内存泄漏
js
// 支持的事件类型
const allNativeEvents = ["click"];
const elementEventPropsKey = "__props";

const listeningMarker = `_reactListening` + Math.random().toString(36).slice(2);

// 事件映射
function getEventCallbackNameFromtEventType(eventType) {
  return {
    click: ["onClickCapture", "onClick"],
  }[eventType];
}

// 收集从目标元素到HostRoot之间所有目标回调函数
const collectPaths = (targetElement, container, eventType) => {
  const paths = {
    capture: [],
    bubble: [],
  };
  // 收集事件回调是冒泡的顺序
  while (targetElement && targetElement !== container) {
    // div.__props = {onClickCapture:fn,onClick:fn....}
    const eventProps = targetElement[elementEventPropsKey];
    if (eventProps) {
      const callbackNameList = getEventCallbackNameFromtEventType(eventType);
      // 取出映射的事件
      if (callbackNameList) {
        callbackNameList.forEach((callbackName, i) => {
          // react事件
          const eventCallback = eventProps[callbackName];
          if (eventCallback) {
            if (i === 0) {
              // 由于捕获的执行顺序是从上往下,所以是反向收集
              // 反向插入捕获阶段的事件回调
              paths.capture.unshift(eventCallback);
            } else {
              // 由于冒泡的执行顺序是从下往上,所以是正向收集
              // 正向插入冒泡阶段的事件回调
              paths.bubble.push(eventCallback);
            }
          }
        });
      }
    }
    targetElement = targetElement.parentNode;
  }
  return paths;
};

const dispatchEvent = (rootContainerElement, eventType, e) => {
  const targetElement = e.target; // 事件源

  if (targetElement === null) {
    console.error("事件不存在target", e);
    return;
  }
  // 从当前的事件源向上查找所有的,扑捉、冒泡事件,用于模拟原生的事件
  const { capture, bubble } = collectPaths(
    targetElement,
    rootContainerElement,
    eventType
  );
  // 合成原生事件
  const se = createSyntheticEvent(e);
  triggerEventFlow(capture, se);
  // 如果事件被阻止不需要在执行
  if (!se.__stopPropagation) {
    triggerEventFlow(bubble, se);
  }
};

export function listenToAllSupportedEvents(rootContainerElement) {
  //监听根容器,也就是div#root只监听一次
  if (!rootContainerElement[listeningMarker]) {
    rootContainerElement[listeningMarker] = true;
    allNativeEvents.forEach((eventType) => {
      // 注册代理事件
      rootContainerElement.addEventListener(eventType, (e) => {
        dispatchEvent(rootContainerElement, eventType, e);
      });
    });
  }
}

function createSyntheticEvent(e) {
  const syntheticEvent = e;
  syntheticEvent.__stopPropagation = false;
  // 阻止事件传递
  const originStopPropagation = e.stopPropagation;

  syntheticEvent.stopPropagation = () => {
    syntheticEvent.__stopPropagation = true;
    if (originStopPropagation) {
      originStopPropagation();
    }
  };

  return syntheticEvent;
}

// 执行react事件
const triggerEventFlow = (paths, se) => {
  for (let i = 0; i < paths.length; i++) {
    const callback = paths[i];
    callback.call(null, se);
    if (se.__stopPropagation) {
      break;
    }
  }
};

// 将支持的事件回调保存在DOM中
export const updateEventProps = (node, props) => {
  node[elementEventPropsKey] = node[elementEventPropsKey] || {};
  allNativeEvents.forEach((eventType) => {
    // 获取支持的事件类型
    const callbackNameList = getEventCallbackNameFromtEventType(eventType);
    if (!callbackNameList) {
      return;
    }
    // 事件映射
    callbackNameList.forEach((callbackName) => {
      if (Object.hasOwnProperty.call(props, callbackName)) {
        node[elementEventPropsKey][callbackName] = props[callbackName];
      }
    });
  });
  return node;
};

react16 版本事件 bug ​

  • 比如需要一个事件来控制一个元素的展示,在当前组件注册一个原生的 document 冒泡事件,在处理合成事件让元素展示的时候,如果原生事件处理的是隐藏,其结果就是元素无法展示

  • 出现的原因是 react 冒泡事件先注册,注册在 document,所以 react 冒泡事件先执行,后执行原生的 document 冒泡事件,所以无法展示,通过阻止默认行为(冒泡),无效,因为是同级节点,只能阻止上级

js
// react16版本
function App() {
  let [visibility, setVisibility] = useState(false);
  // 注册冒泡事件
  document.addEventListener("click", () => {
    setVisibility(false);
  });
  let handlerClick = (e: any) => {
    setVisibility(true);
    // 阻止默认行为(冒泡),只能阻止上级,不能阻止同级
    e.stopPropagation();
    // 阻止所有
    e.nativeEvent.stopImmediatePropagation();
  };

  return (
    <div onClick={handlerClick}>
      {visibility && "visibility"}
      点击
    </div>
  );
}

context 用作原理 ​

  • 用作:数据共享,可以避免 props 一层成传递,可以实现跨组件数据共享

  • 原理

    • 1、在创建 createContext 时,会返回一个 context 对象,包含内容提供者(Provider)和消费者(Consumer),他们都有一个_context 属性,都指向同一个引用地址
    • 2、react 在渲染时,如果是 Provider 组件,会给组件_context._currentValue进行赋值,在渲染当前的 children 组件
    • 3、react 在渲染时,如果是 Consumer 组件,会取出_context._currentValue的值,传递给子组件(因为指向同一个引用地址,所以值是一样的)
    • 4、如果是类组件,有 contextType 属性,会对类上的 context 进行赋值
js
// 渲染provider
function mountProviderComponent(vdom) {
  let { type, props } = vdom; // {$$typeof: REACT_PROVIDER,_context: context,};
  // 引用赋值
  type._context._currentValue = props.value;
  // 用于下次新老做对比(渲染的其实是子元素)
  vdom.oldRenderVdom = props.children;
  if (!vdom.oldRenderVdom) return null;
  return createDOM(props.children);
}
js
// 处理函数类件
function mountClassComponent(vdom) {
  let { type: ClassComponent, props, ref } = vdom;
  let classInstance = new ClassComponent(props);
  vdom.classInstance = classInstance;
  // 如果有contextType,把context绑定在实例上
  if (ClassComponent.contextType) {
    classInstance.context = ClassComponent.contextType._currentValue;
  }
  // .....
  return dom;
}
js
function createContext() {
  let context = { $$typeof: REACT_CONTEXT };
  // 提供者
  context.Provider = {
    $$typeof: REACT_PROVIDER,
    _context: context,
  };
  // 消费者
  context.Consumer = {
    $$typeof: REACT_CONTEXT,
    _context: context,
  };
  return context;
}

this.state = {
  color: "red",
};
const Context = React.createContext();
<Context.Provider value={{red: this.state.color}}>
  <div>{this.state.color}</div>
</Context.Provider>

 <Context.Consumer>
    {(props) => {
      return (
        <div>
          <span style={{ color: props.red }}>main</span>
        </div>
      );
    }}
</Context.Consumer>

React 优化方案( shouldComponentUpdate,PureComponent,memo,useMemo,useCallback) ​

React 的性能优化核心是减少不必要的重新渲染和重复计算,手段分为两类:

  • 拦截渲染:在组件更新前判断 props/state 是否变化,未变化则跳过(shouldComponentUpdate / PureComponent / memo)
  • 缓存结果:把「值」或「函数」缓存下来,依赖不变就复用旧引用(useMemo / useCallback)

一句话速记:类组件用 shouldComponentUpdate / PureComponent 拦截更新;函数组件用 memo 拦截更新,再用 useMemo 缓存值、useCallback 缓存函数(memo 需与 useMemo/useCallback 配套,才能保证传下去的 props 引用稳定)。

类组件 ​

  • shouldComponentUpdate(nextProps, nextState)
    • 组件更新前触发的生命周期,接收即将生效的新 props、新 state,可与当前的 this.props / this.state 对比。
    • 返回 true 执行更新,返回 false 跳过本次渲染(默认返回 true)。
  • PureComponent
    • 内部自动实现了 shouldComponentUpdate,对 props 和 state 做浅比较(第一层引用比较)。
    • 陷阱:若传入的是新创建的对象 / 数组 / 函数(引用每次都变),浅比较必然失败,优化会失效。

函数组件 ​

  • memo
    • 高阶组件,包裹函数组件后,更新前对比新旧 props,未变化则跳过重新渲染。
    • 未提供自定义对比函数时,走 memo 默认的 arePropsEqual,对 props 做浅层对比:父组件重渲染但传入相同的 props 引用时,子组件不会重渲染。
    • 陷阱:父组件每次渲染都会重新创建内联对象 / 函数(引用变化),会让 memo 失效——因此常需配合 useMemo / useCallback 稳定这些引用。
  • useMemo:缓存计算结果的值,依赖不变则跳过重新计算(适合昂贵计算)。
  • useCallback:缓存函数引用,依赖不变则复用同一个函数(常配合 memo 子组件,避免子组件因函数引用变化而重渲染)。

React.memo 默认比较策略 ​

React.memo 默认的比较函数就是 shallowEqual(浅比较),对新旧 props 对象做逐键对比:

  • 先判断新旧 props 是否为同一引用,相同直接判定相等 → 跳过更新;
  • 引用不同时,再比较 key 的数量,数量不同 → 更新;
  • 数量相同则遍历第一层每个 key,用 === 比较对应的 value(即比较引用/基本值):
    • 所有 key 的 value 都相等 → 判定相等,跳过更新;
    • 只要有一个不等 → 更新组件。

一句话速记:memo 只比 props 的第一层。第一层是基本类型时比值,是对象/数组/函数时比引用地址——所以深层数据变化(如 {b:{}} → {b:{c:10}} 但 b 引用没变)不会触发更新;反之每次传新对象(引用变)必触发更新。

js
function shallowEqual(objA, objB) {
  // 1. 相同引用检查 不更新
  if (objA === objB) return true;

  // 2. 如果有一个是 null,更新
  if (
    typeof objA !== "object" ||
    objA === null ||
    typeof objB !== "object" ||
    objB === null
  ) {
    return false;
  }

  // 3. 键数量检查
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  if (keysA.length !== keysB.length) return false;

  // 4. 键值对检查(仅第一层)
  for (let i = 0; i < keysA.length; i++) {
    const key = keysA[i];

    // 检查键是否存在
    if (!Object.prototype.hasOwnProperty.call(objB, key)) {
      return false;
    }
    // {a:10,b:{}}
    // {a:10,b:{c:10}} // 不会触发更新,因为浅比较(第一层),比较的是引用地址
    if (objA[key] !== objB[key]) {
      return false;
    }
  }

  return true;
}

setState ​

核心概念

setState 是 React 中用于更新组件状态的方法,具有批量更新和异步执行的特性。

  • 在 React 事件(合成事件、生命周期)中调用 setState,会走批量更新:同一次事件里的多个 setState 会被合并,只触发一次重新渲染。
  • React 17 及以前:只有 React 事件里才批处理;在原生事件、setTimeout、Promise 等异步任务中调用 setState 不批处理,相当于每次都同步更新并立即重渲染。
  • React 18 起:引入自动批处理(Automatic Batching),无论在合成事件、原生事件还是异步任务中,多个 setState 都会自动合并;如需强制同步更新,可用 flushSync 包裹。

一句话速记:批处理 = 合并多次 setState、只渲染一次。React 17 只在合成事件里批处理,异步/原生事件里「同步生效」;React 18 全场景自动批处理,想跳出批处理用 flushSync。

注意:setState 的「异步」指的是不能同步拿到更新后的 this.state(批处理期间状态还没提交),并非真正的异步 API。想拿更新后的值,用第二个参数的回调,或改用函数式更新 setState(prev => ...)。

js
// 初始状态:count = 0
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });

// 最终结果:count = 1 (不是3!)
js
// 使用函数形式确保基于最新状态
this.setState((prevState) => ({ count: prevState.count + 1 }));
this.setState((prevState) => ({ count: prevState.count + 1 }));
this.setState((prevState) => ({ count: prevState.count + 1 }));

// 最终结果:count = 3 ✅
js
handleClick = () => {
  this.setState({ count: 42 });
  console.log(this.state.count); // 输出旧值,不是42!

  // 使用回调获取更新后状态
  this.setState({ count: 43 }, () => {
    console.log(this.state.count); // 输出43 ✅
  });
};

为什么会出现 hooks ​

核心概念

Hooks 是 React 16.8 引入的特性,使得函数组件可以拥有类组件的特性,如状态管理、生命周期方法等。

Hooks 的出现是为了解决类组件在逻辑复用、代码组织和心智负担上的痛点,让函数组件也能拥有状态和生命周期能力。

一句话速记:Hooks 让函数组件也能用 state 和生命周期,主要解决类组件的三大痛点——逻辑复用难(HOC/mixin 嵌套)、逻辑分散(同一逻辑被拆到不同生命周期)、this 指向易错。

  • 让函数组件拥有状态与副作用能力:通过 useState、useEffect 等,函数组件也能管理状态、处理生命周期,更灵活强大。
  • 解决逻辑复用难题:用自定义 Hook 复用状态逻辑,替代类组件中层层嵌套的高阶组件(HOC)和已废弃的 mixin,避免「嵌套地狱」。
  • 让相关逻辑聚合:类组件里同一功能的逻辑常被拆散在 componentDidMount / componentDidUpdate / componentWillUnmount 中;Hooks 可按关注点把相关逻辑写在一起,更易读易维护。
  • 规避 this 问题:函数组件没有 this,天然避免类组件中 this 绑定错误、事件回调需 bind 等繁琐问题,也更易于测试。

hooks 原理 ​

核心概念

Hooks 原理基于链表结构和 dispatcher 机制,通过维护 hooks 链表和更新队列实现状态管理和生命周期功能。

TIP

类组件中的 setState 是状态合并策略 hooks 队列中有多个时,状态不会进行合并,是直接覆盖更新,后者覆盖前者

一句话速记:函数组件的所有 hook 按调用顺序串成一条链表挂在 fiber 上;渲染时通过全局 dispatcher(Mount / Update 两套)分发;每个 hook 自带更新队列,调用 setState 就是往队列里塞数据并触发重渲染;重渲染时按链表顺序依次读取——所以 hook 必须顶层、顺序稳定调用。

  • 1、React 构建 Fiber 时,若是函数组件,会先调用 renderWithHooks 做初始化准备。
  • 2、根据渲染阶段设置全局 dispatcher(ReactCurrentDispatcher):首次渲染(Mount)用 HooksDispatcherOnMount,更新(Update)用 HooksDispatcherOnUpdate。dispatcher 就是组件里实际调用的那套 hook 实现(如 useState、useEffect…)。
  • 3、执行函数组件时,若是初次渲染,会按 hook 的调用顺序依次创建 hook 对象,串成一条 hooks 链表。
  • 4、每个 hook 都持有一个更新队列和一个 dispatchSetState 函数(该函数已预先绑定好当前 fiber 和自身 hook 队列),并通过 next 指向下一个 hook。
  • 5、调用 setState 本质是执行 dispatchSetState:把新的 state 放入该 hook 的更新队列,并触发组件重新渲染。
  • 6、重新渲染时,React 依据 hooks 链表按原顺序依次执行,从队列中取出并计算最新状态,从而复原各 hook 的值。

hooks 注意事项 ​

核心概念

Hooks 必须在函数组件的顶层调用,不能在条件、循环或嵌套函数中使用,否则会导致 hook 链表错乱。

  • 因为函数组件所有的 hook,以链表的形式存储的 fiber 中,当函数组件在更新的时候,会按照链表 next 一个个进行查找
  • 如果在条件中使用 hooks、或者在循环中使用 hooks,会导致无法获取到正确的 hook 链表,从而出现 bug

useMemo 和 useCallback 区别 ​

核心概念

useMemo 和 useCallback 都是 React 中用于性能优化的 Hooks,用于缓存值和函数,减少不必要的计算和渲染。

一句话速记:两者都靠依赖数组做缓存——依赖没变就复用上次结果,变了才更新。useMemo 缓存值(factory 的返回值),useCallback 缓存函数本身。等价关系:useCallback(fn, deps) === useMemo(() => fn, deps)。

为什么需要它们:函数组件里任何一个 state 变化都会导致整个组件重新执行,函数、计算结果每次都被重新创建。把不必变的「值」或「函数」缓存起来,可避免重复计算、并保持引用稳定。

注意:useCallback 通常要和 React.memo 配套使用——用 useCallback 稳定住传给子组件的函数引用,memo 才能靠浅比较跳过子组件渲染;只用其中一个往往无效,甚至因额外缓存开销导致性能不升反降。

  • 相同点

    • 1、都是 React 提供的性能优化 Hook,用于缓存、减少不必要的计算与渲染;
    • 2、参数一致:第一个是回调函数,第二个是依赖数组 deps;
    • 3、只有依赖发生变化时才更新缓存,否则复用上一次的结果。
  • 区别

    • 1、useMemo 缓存的是回调函数 return 出来的值(会执行回调拿返回值),适合缓存昂贵的计算结果;
    • 2、useCallback 缓存的是回调函数本身(不执行它,只是返回同一个函数引用),适合把稳定的函数传给子组件或作为其他 Hook 的依赖。
js
function useMemo(factory, deps) {
  let hook =
    workInProgressFiber.alternate &&
    workInProgressFiber.alternate.hooks &&
    workInProgressFiber.alternate.hooks[hookIndex];
  let currentMemo = null;
  if (hook) {
    let [lastMemo, lastDeps] = hook;
    // 检测依赖的属性是否发生变化
    let same = deps.every((item, index) => item === lastDeps[index]);
    if (same) {
      // 依赖的属性未发生变化返回之前的值
      currentMemo = lastMemo;
    } else {
      // 返回新的值
      currentMemo = factory();
    }
  } else {
    // 创建新的
    currentMemo = factory();
  }
  workInProgressFiber.hooks[hookIndex++] = [currentMemo, deps];
  return currentMemo;
}
js
function useCallback(callback, deps) {
  let hook =
    workInProgressFiber.alternate &&
    workInProgressFiber.alternate.hooks &&
    workInProgressFiber.alternate.hooks[hookIndex];
  let currentCallback = null;
  if (hook) {
    let [lastCallback, lastDeps] = hook;
    let same = deps.every((item, index) => item === lastDeps[index]);
    if (same) {
      currentCallback = lastCallback;
    } else {
      currentCallback = callback;
    }
  } else {
    currentCallback = callback;
  }
  workInProgressFiber.hooks[hookIndex++] = [currentCallback, deps];
  return currentCallback;
}

useEffect 和 useLayoutEffect ​

一句话速记:两者 API 用法一致,只是执行时机不同。useEffect 在浏览器绘制后异步执行,不阻塞渲染;useLayoutEffect 在 DOM 变更后、浏览器绘制前同步执行,会阻塞绘制。默认优先用 useEffect;只有当「读取/修改 DOM 布局又不能让用户看到闪烁」时才用 useLayoutEffect。

时机顺序:DOM 变更 → useLayoutEffect(同步)→ 浏览器绘制 → useEffect(异步)。

1. useEffect(异步执行) ​

  • 触发时机:在浏览器完成 绘制(Paint)后异步执行
  • 使用场景:
    • 数据获取(API 请求)
    • 事件监听/订阅
    • 非紧急的 DOM 操作
    • 第三方库集成
    • 性能分析/埋点
  • 特点:
    • 不会阻塞浏览器渲染
    • 用户可能先看到变化前的 UI,再看到更新后的 UI(可能产生闪烁)
jsx
// 典型用例:数据获取
useEffect(() => {
  fetchData().then((data) => setData(data));
}, []);

2. useLayoutEffect(同步执行) ​

  • 触发时机:在 DOM 更新后、浏览器 绘制(Paint)前同步执行
  • 使用场景:
    • 读取/修改 DOM 布局(如元素尺寸、位置)
    • 同步更新 UI 避免视觉闪烁
    • 需要基于 DOM 计算的动画
  • 特点:
    • 阻塞浏览器绘制
    • 保证用户看到的是最终 UI(避免中间状态)
jsx
// 典型用例:测量 DOM 元素
useLayoutEffect(() => {
  const { width } = divRef.current.getBoundingClientRect();
  setWidth(width); // 在绘制前更新尺寸
}, []);

错误使用示例 ​

jsx
// ❌ 错误:在 useEffect 中同步修改布局
// useEffect 在绘制后执行,用户会先看到旧布局、再看到新布局,产生闪烁
useEffect(() => {
  buttonRef.current.style.padding = "10px 20px";
}, []);

// ✅ 正确:改用 useLayoutEffect,在绘制前同步改好布局,用户直接看到最终结果
useLayoutEffect(() => {
  buttonRef.current.style.padding = "10px 20px";
}, []);
js
function useEffect(callback, deps) {
  let hook =
    workInProgressFiber.alternate &&
    workInProgressFiber.alternate.hooks &&
    workInProgressFiber.alternate.hooks[hookIndex];

  if (hook) {
    let [lastCallback, lastDeps] = hook;
    let same = deps && deps.every((item, index) => item === lastDeps[index]);
    // 如果关联的deps未发生变化不做任何操作
    if (same) {
      workInProgressFiber.hooks[hookIndex++] = [lastCallback, deps];
    } else {
      // 如果发生变化,执行销毁的回调
      lastCallback && lastCallback();
      setTimeout(() => {
        // 存储新的
        workInProgressFiber.hooks[hookIndex++] = [callback(), deps];
      });
    }
  } else {
    setTimeout(() => {
      // callback 返回的函数,在下次渲染前执行
      workInProgressFiber.hooks[hookIndex++] = [callback(), deps];
    });
  }
}
js
function useLayoutEffect(callback, dependencies) {
  let hook =
    workInProgressFiber.alternate &&
    workInProgressFiber.alternate.hooks &&
    workInProgressFiber.alternate.hooks[hookIndex];
  if (hook) {
    let [lastCallback, lastDeps] = hook;
    let same =
      dependencies &&
      dependencies.every((item, index) => item === lastDeps[index]);
    if (same) {
      workInProgressFiber.hooks[hookIndex++] = [lastCallback, dependencies];
    } else {
      lastCallback && lastCallback();
      // DOM 更新完成后,浏览器绘制之前
      queueMicrotask(() => {
        lastCallback = callback();
        workInProgressFiber.hooks[hookIndex++] = [lastCallback, dependencies];
      });
    }
  } else {
    // DOM 更新完成后,浏览器绘制之前
    queueMicrotask(() => {
      workInProgressFiber.hooks[hookIndex++] = [callback(), dependencies];
    });
  }
}

useRef&createRef 区别 ​

一句话速记:useRef 引用稳定(跨渲染保持同一个对象),createRef 在函数组件里每次渲染都会新建。

  • 1、两者都会创建一个 ref 对象(形如 { current: xxx })。区别在于:createRef 在类组件和函数组件中都能使用,useRef 只能在函数组件中使用。
  • 2、在函数组件里使用 createRef 时,每次组件更新都会重新初始化,得到一个全新的 ref 对象(指向不同的引用地址),无法在多次渲染间保存数据。
  • 3、useRef 把 ref 对象 存储在对应 hook 的 memoizedState 上(挂在 fiber 的 hook 链表中),组件更新时直接复用之前的对象,不会重新初始化(始终指向同一个引用地址)。

补充:useRef 常用于两类场景 —— ① 保存 DOM 节点引用;② 保存一个「跨渲染保持、但修改时不触发重新渲染」的可变值。类组件中的 createRef 应写在 constructor 里,而不是 render 中,原因同上(避免每次渲染重建)。

js
// mount 阶段
function mountRef<T>(initialValue: T): {| current: T |} {
  // 获取 hook 对象
  const hook = mountWorkInProgressHook();
  const ref = { current: initialValue };
  if (__DEV__) {
    Object.seal(ref);
  }
  // 存储
  hook.memoizedState = ref;
  return ref;
}

// update 阶段
function updateRef<T>(initialValue: T): {| current: T |} {
  const hook = updateWorkInProgressHook();
  // 返回之前的引用
  return hook.memoizedState;
}

useState 和 useReducer 使用场景 ​

一句话速记:状态简单、独立、更新直接用 useState;状态之间相互关联、更新逻辑复杂、需要集中管理时用 useReducer(本质是把「怎么更新」收敛到 reducer 里)。

1. useState - 简单状态管理 ​

使用场景:

  • 管理单个简单值(字符串、数字、布尔值)
  • 处理独立状态(无复杂依赖关系)
  • 组件内轻量级状态(少量状态变量)
  • UI 元素状态(输入框值、开关状态等)

2. useReducer - 复杂状态逻辑 ​

使用场景:

  • 管理关联状态组(多个相互影响的状态)
  • 处理复杂状态更新逻辑
  • 需要状态历史追溯的场景
  • 购物车、分步表单

使用原则指南 ​

优先选择 useState 当:

  1. 状态是独立的基本类型(string/number/boolean)
  2. 状态更新逻辑简单(直接赋值)
  3. 组件内状态数量较少(≤3 个)

必须使用 useReducer 当:

  1. 状态是相互关联的对象(如用户资料{name, age, avatar})
  2. 下一个状态依赖前一个状态(如计数器队列)
  3. 需要处理复杂状态转换(如购物车、表单多步骤)
  4. 需要跨组件/钩子共享状态逻辑(可复用 reducer)
  5. 大型组件需要收敛更新逻辑(把分散的 setState 集中到 reducer,便于维护,并可把 dispatch 透传给子组件而无需重建回调)

总结选择策略 ​

场景推荐 Hook
表单输入控制useState
开关/复选框状态useState
简单计数器useState
购物车/复杂业务对象useReducer
多步骤表单useReducer
需要状态历史记录的功能useReducer
全局状态共享(中小型应用)useReducer + Context

useRef 和 useState 区别 ​

一句话速记:useState 更新会触发重新渲染、且每次渲染拿到的是当次快照值;useRef 更新不触发渲染、且始终读到最新值(同一引用)。

  • useState 在更新的时候会导致组件重新渲染
  • useRef 在更新的时候不会导致组件重新渲染

useRef 可以解决闭包陷阱问题 ​

下面两段演示同一个操作序列的差异:useState 版本因闭包捕获的是「点击那一刻的快照值」,useRef 版本因始终指向同一引用而读到最新值。

js
// 1、先点击第一个按钮6次
// 2、在点击第二个按钮1次
// 3、立马在点击第一个按钮4次
import { useState } from "react";
const App = () => {
  // setState更新会导致组件更新
  const [state, setState] = useState(0);
  function handleClick() {
    setTimeout(() => {
      // 4、由于闭包的特性,当前的值是6,所以打印6
      console.log(state);
    }, 3000);
  }
  return (
    <>
      {/* setState更新会导致组件更新所以是10 */}
      <button onClick={() => setState(state + 1)}>{state}</button>
      <button onClick={handleClick}>print</button>
    </>
  );
};
export default App;
js
// 1、先点击第一个按钮6次
// 2、在点击第二个按钮1次
// 3、立马在点击第一个按钮4次
import { useRef } from "react";
const App = () => {
  const state = useRef(0);
  function handleClick() {
    setTimeout(() => {
      // 4、由于都是指向同一个引用地址所以是10
      console.log(state.current);
    }, 3000);
  }
  return (
    <>
      <button
        onClick={() => {
          state.current++;
        }}
      >
        {/* useRef值的改变不会导致组件更新,所以一直是0 */}
        {state.current}
      </button>
      <button onClick={handleClick}>print</button>
    </>
  );
};
export default App;

如何拿到 useState 更新的值 ​

  • 1、可通过useEffect,将更新值作为依赖条件
  • 2、在更新 useState 之前,先计算值在更新 useState

forwardRef(组件转发) ​

一句话速记:函数组件没有实例,不能直接接收 ref;用 forwardRef 把父组件传来的 ref 转发到子组件内部的 DOM 或(配合 useImperativeHandle)暴露的方法上。

WARNING

  • 如果对函数组件使用 ref,会抛出如下警告。因为函数组件和类组件不同,没有实例,所以对函数组件直接使用 ref 是毫无意义的。
  • Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
  • forwardRef 可以让父组件的ref获取到子组件的 DOM,同时也可以配合useImperativeHandle,将子组件的函数暴露给父组件ref
js
import React, { useEffect, useRef, forwardRef } from "react";
// 子组件:普通函数组件本身无法接收 ref,需要用 forwardRef 包裹
// 包裹后 forwardRef 会把父组件传来的 ref 作为第二个参数 ref 传进来
const InputText = (props: any, ref: any) => {
  return (
    <>
      <div>
        {/* 把转发进来的 ref 绑定到真实 DOM 上,父组件即可拿到这个 input */}
        <input ref={ref} />
      </div>
    </>
  );
};
// 用 forwardRef 包裹子组件,得到一个可以接收 ref 的新组件
const ForwardInput = forwardRef(InputText);
const App: React.FC = () => {
  // 父组件创建 ref,用来指向子组件内部的 input
  const inputRef = useRef < HTMLInputElement > null;
  useEffect(() => {
    if (inputRef.current) {
      // 获取到子组件的input,并让其聚焦
      inputRef.current.focus();
    }
  });
  return (
    <>
      {/* 把父组件的 ref 传给转发后的子组件 */}
      <ForwardInput ref={inputRef}></ForwardInput>
    </>
  );
};
export default App;
  • 1、forwardRef 接收一个组件,返回一个对象,包含 $$typeof(类型标记)和 render(即外部传入的组件函数)。
  • 2、渲染时,React 检测到该元素的 $$typeof 是 REACT_FORWARD_REF,就走转发逻辑,内部会保留父组件传递的 ref 引用。
  • 3、调用 render 渲染组件,并把 props 和 ref 一起传给对应组件,从而让父组件的 ref 拿到子组件内部的 DOM。
js
// forwardRef 只是把传入的组件包成一个带类型标记的对象
function forwardRef(render) {
  return {
    $$typeof: REACT_FORWARD_REF, // 类型标记:标识这是一个转发 ref 的组件
    render, // 外部传入的真正组件函数
  };
}

// 渲染阶段:识别到转发 ref 类型,走专门的挂载逻辑
if (type && type.$$typeof === REACT_FORWARD_REF) {
  return mountForwardComponent(vdom);
}

function mountForwardComponent(vdom) {
  // 从虚拟 DOM 上取出组件类型、props 和父组件传来的 ref
  const { type, props, ref } = vdom;
  // 关键:调用 render 时把 ref 作为第二个参数一起传进去,实现转发
  const renderVdom = type.render(props, ref);
  if (!renderVdom) return null;
  // 把返回的虚拟 DOM 转成真实 DOM
  return createDOM(renderVdom);
}

useImperativeHandle ​

一句话速记:配合 forwardRef 使用,让子组件自定义暴露给父组件 ref 的内容(通常是一组方法),而不是直接把整个 DOM 暴露出去。

  • 1、useImperativeHandle 是一个 Hook,接受 2 个参数(可选第 3 个):第一个是父组件转发进来的 ref,第二个是一个工厂函数,返回要暴露的对象;第三个是依赖数组,依赖变化时才重新生成该对象。
  • 2、当 useImperativeHandle 被调用时,内部会执行第二个参数(工厂函数),把它返回的对象存储到父组件的 ref.current 上。
  • 3、因为父子指向同一个引用地址,所以父组件通过 ref.current 就能调用到子组件暴露出来的函数。
js
import React, {
  useEffect,
  useRef,
  forwardRef,
  useImperativeHandle,
} from "react";

// 定义子组件要暴露给父组件的方法类型
interface IInputText {
  onClick: () => void;
}

// 子组件:通过 forwardRef 接收父组件传来的 ref
const InputText = (props: any, ref: any) => {
  // useImperativeHandle 自定义暴露给父组件 ref 的内容
  // 第一个参数:转发进来的 ref;第二个参数:返回要暴露对象的工厂函数
  useImperativeHandle(ref, () => {
    return {
      // 只暴露 onClick 方法给父组件,而不是整个 DOM
      onClick: () => {
        console.log("子组件的函数");
      },
    };
  });
  return (
    <>
      <div></div>
    </>
  );
};
// 用 forwardRef 包裹子组件,使其能接收 ref
const ForwardInput = forwardRef(InputText);
const App: React.FC = () => {
  // 父组件创建 ref,类型为子组件暴露的方法集合
  const inputRef = useRef < IInputText > null;
  useEffect(() => {
    if (inputRef.current) {
      // 通过 ref.current 调用子组件暴露出来的 onClick 方法
      inputRef.current.onClick();
    }
  });
  return (
    <>
      {/* 把 ref 传给子组件 */}
      <ForwardInput ref={inputRef}></ForwardInput>
    </>
  );
};
export default App;
js
// 简化实现:执行工厂函数 handler,把返回的对象挂到 ref.current 上
export function useImperativeHandle(ref, handler) {
  // handler() 返回要暴露的对象,赋值给 ref.current
  // 父子指向同一引用,父组件即可通过 ref.current 调用这些方法
  ref.current = handler();
}

react 新增了什么生命周期和删除了什么生命周期,为什么要删除 ​

一句话速记:废弃 3 个 will 开头的钩子(componentWillMount/componentWillReceiveProps/componentWillUpdate),新增 2 个更安全的钩子(getDerivedStateFromProps/getSnapshotBeforeUpdate)。根因:Fiber 异步可中断渲染下,render 前的阶段可能被打断并重复执行,will 钩子里的副作用会被多次调用。

被废弃的三个(render 前、可能被重复执行):

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

新增的两个(用来替代上面的能力):

  • getDerivedStateFromProps:静态方法,在 render 前调用,根据 props 派生 state(替代 componentWillReceiveProps),因是纯函数、无副作用,重复调用也安全。
  • getSnapshotBeforeUpdate:在真实 DOM 更新前调用,捕获更新前的信息(如滚动位置),返回值传给 componentDidUpdate。

为什么废弃:React 引入 Fiber 后,render 阶段(生成虚拟 DOM)变成异步可中断的——当有更高优先级的任务到来时,当前 render 会被打断,之后从头重来。这些 will 钩子正处于 render 之前的阶段,一旦里面写了副作用(如请求、订阅、setState),就可能被重复调用导致 bug,因此被废弃。

Vue vs React ​

一句话速记:Vue 用模板 + 响应式,组件级精确更新、渲染递归不可中断;React 用 JSX + 手动更新,从根节点调度、任务可拆分可中断(Fiber)。

  • Vue 使用模板,拥抱 HTML

  • Vue 真正做到了组件级更新:每个组件对应一个渲染 effect,由响应式数据去收集这个 effect,数据变化时精确通知对应组件更新

  • Vue 采用递归的方式渲染页面,过程不可中断

  • React 使用 JSX,拥抱 JS

  • React 更新都从根节点开始调度,会将一个大任务拆分成多个小的任务单元

  • React 更新策略是循环的方式,有任务优先级的概念,可中断执行(Fiber 架构)