React 相关

返回 相关私有笔记 · 相关私有笔记

📑 本目录笔记

渲染流程

  1. React Fiber 架构新增了一个 Scheduler 调度器 主要用于调度 Fiber 节点的生成和更新任务
  2. 当组件更新时,Reconciler 协调器 执行组件的 render 方法生成一个 Fiber 节点 之后再递归的去生成 Fiber 节点 的子节点
  3. 每一个 Fiber 节点 的生成都是一个单独的任务,会以回调的形式交给 Scheduler 进行调度处理,在 Scheduler 里会根据任务的优先级去执行任务
  4. 任务的优先级
    • React 17 引入了 车道模型 Lane 作为基础(Concurrent 特性此时仍是实验性的,到 React 18 才正式对外),将任务进行分类,每一类拥有不同的优先级,所有的分类和优先级都在 React 中进行了枚举
    • React 16 中使用的是 expirationTime 模型 来决定优先级
  5. Scheduler 按照优先级执行任务时,会异步的执行,同时每一个任务执行完成之后,都会判断下一个任务是否能在当前渲染帧的剩余时间内完成(早期方案是 requestIdleCallback,但因兼容性和触发频率问题被弃用,最终改用 MessageChannel 实现时间切片)
  6. 如果不能完成就发生中断,把线程的控制权交给浏览器,剩下的任务则在下一个渲染帧内执行
  7. diff(reconcile)发生在 render 阶段:ReconcilerScheduler 在 render 阶段完成 diff 计算后,会生成一个新的 workInProgressFiber 节点树并打好变更标记;之后 Reconciler 触发 Commit 阶段,通知 Render 渲染器 把已算好的变更 patch 到真实 DOM 上

❓ Fiber

有 current Fiber 和 workingProgress Fiber 两棵树,通过 alternate 相连

源码解析

React 源码大致可以分为三块:

  • 🆕 Scheduler(调度器):排序优先级,让优先级高的任务先进行 reconcile
  • Reconciler(协调器):找出哪些节点发生了改变,并打上不同的 Tag
  • Renderer(渲染器):将 Reconciler 中打好标签的节点渲染到视图上

整个生命周期的话又大致分为两个阶段:render 和 commit

render 阶段

Reconciler(协调器)是在 render 阶段工作的,简单来说就是会创建、更新 Fiber 节点。通过 Diff 算法给 Fiber 树打上 tag

  • 首次渲染时根据 JSX 构建 workingProgress Fiber -> current Fiber
  • 每次更新的时候根据变更后的 JSX 形成 workingProgress Fiber

将 workingProgress Fiber 替换为 current Fiber 后渲染到 DOM 中就完成了组件的更新

Fiber 树的一切操作都是在内存中进行的,从而减少对 DOM 的操作,提高性能

遍历 Fiber 树

  1. beginWork 主要负责创建 Fiber 节点、计算 state、diff 算法
  2. completeWork 则处理节点 props、创建 DOM、创建 effectList 链表(被标了更新的节点形成的链表)等

commit 阶段

Renderer(渲染器)是在 commit 阶段工作的,commit 阶段会遍历 render 阶段生成的 effectList,并执行真实 DOM 节点的操作和一些生命周期(不同平台对应的 Renderer 不同,浏览器对应的是 react-dom

hooks

为函数组件引入状态管理和生命周期方法,提高代码的可复用性和抽象性

React 为每个组件维护了一个 hooks 的单链表,组件每次更新时都会检查链表中的 hook 是否发生变化,并根据变化来更新状态链中的对应状态

mount 阶段(初始化)

来看两个比较重要的函数

export function renderWithHooks(
  current,
  workInProgress,
  Component,
  props,
  secondArg,
  nextRenderExpirationTime,
) {
  renderExpirationTime = nextRenderExpirationTime;
  currentlyRenderingFiber = workInProgress;
 
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.expirationTime = NoWork;
 
  ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
 
  let children = Component(props, secondArg);
 
  if (workInProgress.expirationTime === renderExpirationTime) { 
      // 如果当前函数组件执行后,当前函数组件的还是处于渲染优先级
      // 说明函数组件又有了新的更新任务,那么循环执行函数组件
  }
 
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;
 
  renderExpirationTime = NoWork;
  currentlyRenderingFiber = null;
 
  currentHook = null
  workInProgressHook = null;
 
  didScheduleRenderPhaseUpdate = false;
 
  return children;
}
// ReactFiberHooks.new.js
function mountWorkInProgressHook() {
  const hook: Hook = {
    memoizedState: null,  // useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };
  if (workInProgressHook === null) { // 例子中的第一个`hooks`-> useState(0) 走的就是这样。
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

Fiber 的 memoizedState 指向的是当前函数形成的 hooks 链表

Hook 对象中的 memoizedState 存储的是 hooks 相关的数据, next 指向的是链表中下一个 hook 节点

因为是链表的原因,从下图就可以很简单的看出来,如果在条件语句中执行的话,可能会导致 hook 执行顺序出错

useState

有两个关键的点:mountStatedispatchAction

  • mountState:首先会得到初始化的 state,将它赋值给 mountWorkInProgressHook 产生的 hook 对象的 memoizedStatebaseState 属性,然后创建一个 queue 对象,里面保存了负责更新的信息
  • dispatchAction:无论是类组件还是函数组件,都会生成一个 update 对象,这个对象上记录了这次更新的信息,并被放入 pending 队列中。如果当前 Fiber 节点正在渲染阶段,则更新 expirationTime;如果不处于更新阶段,那么将新的 statecurrentState,进行浅比较,值不相等时才调用 scheduleUpdateOnFiber 更新渲染当前 Fiber
useEffect
  • mountEffect:初始化 hook 对象并将 hook 相关信息存入 hook 的 memoizedState 字段中
  • pushEffect:创建一个 effect ,判断组件如果第一次渲染,那么创建 componentUpdateQueue ,就是 workInProgress Fiber 的 updateQueue。然后将 effect 放入 updateQueue

update 阶段

  • 从 current Fiber 上取出 memoizedState,拿到旧的 hooks 链表
  • 声明并赋值 nextWorkInProgressHook
    • 正常情况下,应该为 null
    • 但是如果当前 Fiber 的渲染优先级在下次渲染时还是高的,那么就会赋值为 workInProgressHook.next
  • 最后,用 currentHook 生成一个新的 Hook 对象,赋值给 workInProgressHook,用于更新新一轮 hooks 状态
useState
  • 将 hook 产生的 update 对象放入 pendingQueue。下次函数组件被执行时,将这三个 update 合并到 hook 对象的 baseQueue
  • baseQueuebaseState 更新到最新;
  • 循环 baseQueue,执行每个 action,得到最新的 state;更新 expirationTime

总结

Diff 算法

React 的 Diff 算法 可以分为 单节点 Diff多节点 Diff

  • 单节点 Diff 相对简单,包含以下流程:

    1. 首先会判断老的 Fiber 树上有没有对应的 Fiber 节点,若没有则说明是新增操作,直接在老 Fiber 树上新增节点并更新 DOM
    2. 若老 Fiber 节点也存在,则判断节点上的 key 值是否相同,若不同则删除老节点并新增新节点
    3. key 值相同,则判断节点的 type 是否相同,若不同则删除老节点并新增节点
    4. type 值也相同,则认为是一个可复用的节点,直接返回老节点就行
  • 多节点 Diff:主要用于 map 返回多个相同节点的情况下,可以分为三种情况:新增节点、删除节点以及节点移动,React 采用双重遍历的方式来进行三种情况的判断,流程如下:

    1. children[i]currentFiber 比较,如果可复用,则比较 children[i++]currentFiber.sibling,如果可复用,则继续重复此步骤
    2. 当上面的第一轮遍历无提前结束时,说明所有节点都可以复用,直接返回老节点
    3. children 遍历完成,currentFiber 未完成,则说明是删除操作,需要对未完成的 currentFiber 兄弟节点标记删除
    4. children 遍历未完成,currentFiber 完成,则说明是新增操作,需要生成新的 workInProgressFiber 节点
    5. childrencurrentFiber 都未完成,则说明是节点位置发生了变更,那就对剩余的 currentFiber 进行遍历,并通过 key 值找到每一个节点在 children 中对应的老节点,并将老节点中的位置替换为新节点的

Scheduler

为了实现异步可中断更新,React 使用 MessageChannel 实现了 Scheduler

React 17 之前,Scheduler 中每个任务的优先级是按距离过期时间来排的,离过期时间越近越优先。但是这个模型还存在着一些问题,比如交互操作可能会晚于拉取数据的操作,优先级反转了

Lane

Lane 用二进制位表示优先级,二进制中的 1 表示位置,同一个二进制数可以有多个相同优先级的位,这就可以表示 ” 批次 ” 的概念,而且二进制方便计算

事件机制

合成事件

React 合成事件的优势:

  1. 抹平不同浏览器直接的差异,提供统一的 API 使用体验
  2. 通过事件委托的方式统一绑定和分发事件,有利于提升性能,减少内存消耗

合成事件的绑定及分发流程

  • React 应用启动时,会在页面渲染的根元素上绑定原生的 DOM 事件,将该根元素作为委托对象
  • 在组件渲染时,会通过 JSX 解析出元素上绑定的事件,并将这些事件与原生事件进行一一映射
  • 当用户点击页面元素时,事件会冒泡到根元素,之后根元素监听的事件通过 dispatchEvent 方法进行事件派发
  • dispatchEvent 会根据事件的映射关系以及 DOM 元素找到 React 中与之对应的 fiber 节点
  • 找到 fiber 节点后,将其绑定的合成事件函数加到一个函数执行队列中
  • 最后则依次执行队列中的函数完成事件的触发流程

React 17 的事件机制

❗️React 17 中,React 不会再将事件处理添加到 document 上,而是将事件处理添加到渲染 React 树的根 DOM 容器中

React Router

路由懒加载的原理

<Suspense> 配合 React.lazy 实现的路由懒加载。<Suspense> 提供了 加载 -> 过渡 -> 完成后切换 这样一个标准的业务流程

React 捕获到异常之后,会判断这个异常是不是一个 thenable (或者说是不是一个 Promise),如果是的话就会去找父级的 <Suspense>

<Suspense> 通过 Promise 异步加载代码,一开始会先进入到 fallback,代码加载完成之后再展示对应的组件

<Link> 是 React-Router 里实现路由跳转的链接,一般配合 <Route> 使用,React-Router 接管了其默认的链接跳转行为,区别于传统的页面跳转,<Link> 的 ” 跳转 ” 行为只会触发相匹配的 <Route> 对应的页面内容更新,而不会刷新整个页面

// handleClick 部分源码
      if (_this.props.onClick) _this.props.onClick(event);
    
      if (!event.defaultPrevented && // onClick prevented default
      event.button === 0 && // ignore everything but left clicks
      !_this.props.target && // let browser handle "target=_blank" etc.
      !isModifiedEvent(event) // ignore clicks with modifier keys
      ) {
          event.preventDefault();
    
          var history = _this.context.router.history;
          var _this$props = _this.props,
              replace = _this$props.replace,
              to = _this$props.to;
    
    
          if (replace) {
            history.replace(to);
          } else {
            history.push(to);
          }
        }

Link 做了 3 件事情:

  1. onClick 那就执行 onClick
  2. click 的时候阻止 a 标签默认事件(这样子点击 <a href="/abc">123</a> 就不会跳转和刷新页面)
  3. 再取得跳转 href(即是 to ),用 history(前端路由两种方式 history & hash 之一)跳转(也就意味着会触发 hashchange 事件)。此时只是链接变了,并没有刷新页面

hash 和 history 的异同点和原理

hash

history

新版本 React

React 18 核心新特性

1. 并发模式(Concurrent Mode)

  • 可中断渲染:允许 React 在渲染过程中暂停并处理更高优先级的任务(如用户输入),提升交互流畅性。
  • API 支持useTransitionuseDeferredValue 用于控制非紧急更新的优先级。
const [isPending, startTransition] = useTransition();
startTransition(() => {
  // 非紧急更新(如数据加载)
  setNonCriticalState(newValue);
});

2. 自动批处理(Automatic Batching)

  • 统一批量更新:无论事件类型(如 setTimeoutPromise 或原生事件),React 18 默认合并多个 setState 更新,减少渲染次数。
// React 18 前:异步回调中的 setState 是同步更新
setTimeout(() => {
  setCount(1);
  setName('A'); // 触发两次渲染
}, 0);
 
// React 18:自动批处理,仅一次渲染

3. 服务端 Suspense 流式渲染

  • 流式 HTML:服务端渲染(SSR)支持分段传输 HTML,配合 Suspense 实现渐进式加载。
  • 选择性注水(Selective Hydration):优先为交互区域注水,缩短可交互时间。
<Suspense fallback={<Spinner />}>
  <Comments /> // 延迟加载的组件
</Suspense>

4. 新 Hooks

  • useId:生成跨服务端/客户端的唯一 ID,避免 SSR 与客户端 ID 不匹配。
  • useSyncExternalStore:简化外部状态库(如 Redux)的集成。
  • useInsertionEffect:为 CSS-in-JS 库优化样式注入时机。

React 19(已正式发布)

React 19 已于 2024 年底正式发布,React Compiler、Actions、文档元数据、use 等特性均已落地。主要改进如下:

1. React Server Components 正式支持

  • 服务端组件:在服务端渲染静态内容,减少客户端 JS 体积。
  • 零捆绑大小:服务端组件不发送 JS 到客户端,提升加载速度。
// ServerComponent.server.js(服务端组件)
export default function ServerComponent() {
  return <div>Rendered on Server</div>;
}
 
// ClientComponent.client.js(客户端组件)
import ServerComponent from './ServerComponent.server';
export default function ClientComponent() {
  return <ServerComponent />;
}

2. 资源加载优化

  • 预加载资源:通过新 API(如 preload)提前加载脚本、样式或数据。
const data = preload('/api/data'); // 提前发起请求

3. React Compiler(React Forget)

  • 自动记忆化:编译器自动生成 useMemouseCallback,减少手动优化成本。
// 开发者无需手动优化
const list = items.filter(x => x.active); // 自动记忆化

4. Actions API(统一数据处理)

  • 简化异步操作:封装数据提交、加载状态和错误处理。
const { submit, isPending } = useAction(async () => {
  await saveData(data);
});
 
return <button onClick={submit} disabled={isPending}>Save</button>;

5. 文档元数据支持

  • 内置 <title><meta> 管理:在组件树中直接设置文档头信息。
function Page() {
  return (
    <>
      <title>Home</title>
      <meta name="description" content="Welcome" />
      <Content />
    </>
  );
}

React 18 vs React 19 对比

特性React 18React 19(预期)
并发模式✅ 基础支持✅ 优化扩展(如更细粒度优先级控制)
服务端组件❌ 实验性✅ 正式支持
自动记忆化❌ 需手动 useMemo✅ 编译器自动生成
资源预加载❌ 需自定义实现✅ 原生 API 支持
文档元数据管理❌ 需第三方库(如 React Helmet)✅ 内置组件支持

迁移与适配建议

  1. React 18 升级

    • 使用 createRoot 替代 ReactDOM.render
    • 利用 startTransition 优化用户体验。
    // React 17
    ReactDOM.render(<App />, document.getElementById('root'));
     
    // React 18
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(<App />);
  2. React 19 前瞻

    • 关注服务端组件和编译器进展,逐步适配新范式。
    • 使用 use 提案处理异步数据(当前需实验版本)。
    // 提案中的 use Hook(异步数据读取)
    const data = use(fetch('/api/data'));

总结

React 18 通过并发模式和流式渲染显著提升了性能与用户体验,而 React 19 将进一步推动服务端组件和自动化优化,降低开发者心智负担。建议优先升级至 React 18,并持续关注 React 19 的动态以把握未来趋势。