【翻译】React编译器及其原理:为何类对象可能阻碍备忘录法生效

原文链接:anita-app.com/blog/articl...

作者:ilDon

本文反映了作者的个人观点与思考。由于作者并非英语母语者,最终表述经人工智能编辑以确保清晰度与准确性。

React编译器现已稳定并可投入生产环境(React博客,2025年10月7日),它显著减少了手动使用useMemouseCallbackReact.memo的需求。

这对大多数 React 代码库而言是重大利好,尤其适用于采用纯净函数组件和不可变数据的架构。但存在一种模式正变得日益棘手:依赖类实例计算衍生值的类密集型对象模型。

若渲染时逻辑依赖类实例,编译器备忘录机制的精确度可能无法满足需求,开发者往往不得不重新引入手动备忘录机制以恢复控制权。

React编译器通过可观察依赖关系进行优化

官方文档说明React编译器会基于静态分析和启发式算法自动对组件和值进行备忘存储:

关键细节在于:备忘存储仍取决于React能观察到的输入内容。

在 React 中,对象的备忘比较基于引用(采用 Object.is 的语义)。memouseMemo 的文档都明确说明了这一点:

因此,如果有效值隐藏在对象实例内部,而该实例引用发生变化,React 就会认为值也发生了变化。

ElementClass 示例

假设你将元素建模如下:

JavaScript 复制代码
class ElementClass {
  constructor(private readonly isoDate: string) {}

  public getFormattedDate(): string {
    const date = new Date(this.isoDate);

    if (Number.isNaN(date.getTime())) {
      return 'Invalid date';
    }

    return date.toLocaleString('en-US', {
      year: 'numeric',
      month: 'short',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      timeZoneName: 'short',
    });
  }
}

而在一个组件中:

JavaScript 复制代码
export function Row({ elementInstance }: { elementInstance: ElementClass }) {
  const formattedDate = elementInstance.getFormattedDate();
  return <span>{formattedDate}</span>;
}

这段代码是可读的。但从外部来看,相关的响应式输入实际上是 elementInstance(对象引用)。

如果状态管理层返回了一个新的 ElementClass 实例,React/编译器会检测到新的依赖关系,并重新计算格式化后的值------即使底层的 isoDate 字符串并未改变。

手动逃生舱门功能正常,但噪音较大

你可以强制使用更窄的依赖项:

JavaScript 复制代码
class ElementClass {
  constructor(public readonly isoDate: string) {} // <-- expose isoDate as a public property
  // unchanged
}

export function Row({ elementInstance }: { elementInstance: ElementClass }) {
  const formattedDate = useMemo(
    () => elementInstance.getFormattedDate(),
    [elementInstance.isoDate],
  );

  return <span>{formattedDate}</span>;
}

这确实可行,React 明确将 useMemo/useCallback 作为编译器环境下的逃生通道:

但此时我们又陷入了手动处理依赖关系的困境,还不得不将内部逻辑暴露给 UI。

编译器友好的替代方案:纯数据 + 纯辅助函数

若 UI 接收纯粹的不可变数据,依赖关系将变得显式且低成本:

JavaScript 复制代码
type Element = {
  isoDate: string;
};

export function Row({ element }: { element: Element }) {
  const formattedDate = DateHelpers.formatDate(element.isoDate);
  return <span>{formattedDate}</span>;
}

现在,DateHelpers.formatDate 的相关输入是一个基本类型(isoDate),而非隐藏在类实例方法调用背后的状态。这样,编译器就能将formatDate的输出进行备忘存储,仅将 isoDate 作为唯一依赖项------这个基本值在发生变化时会正确触发备忘存储机制。

有人可能会提出异议:即便在这个简单的对象示例中,整个element仍会被传递给组件。因此Row组件终究会重新渲染,唯一实质区别在于formattedDate不再被重新计算。

这种说法没错:若传递整个对象且其引用发生变化,该组件就会重新渲染。我们稍后将详细探讨这个问题。

在探讨该问题的解决方案之前,我想强调:对于大型应用而言,即使仅考虑派生值的备忘录化,类实例与普通数据之间的差异依然显著。React编译器会注入备忘录单元和依赖项检查。若依赖项是不稳定的对象引用,缓存命中率将很低:

  • 你仍需为备忘录槽位支付额外内存成本,
  • 仍需执行依赖项检查,
  • 仍需因引用变更而重新计算。

换言之,当渲染路径中充斥着类实例且未进行手动备忘时,编译器的优化往往会变成额外开销而非性能提升

现在,让我们回到传递整个对象的问题。若传递对象后其引用发生变化,组件将重新渲染。无论对象是类实例还是普通对象,此特性均成立。若需避免因对象引用变更导致的冗余渲染,可仅传递子组件实际需要的原始值,而非完整对象。如此,组件仅在相关原始值变更时重新渲染,而非对象引用变更时:

JavaScript 复制代码
export function Row({ isoDate }: { isoDate: string }) {
  const formattedDate = DateHelpers.formatIsoDate(isoDate);
  return <span>{formattedDate}</span>;
}

现在依赖关系已显式化且采用原始类型(isoDate),而非隐藏在实例方法背后。

可能的反对意见是:即使采用面向对象的方法,仍可将element.getFormattedDate()的结果传递给子组件,而该结果本质上仍是字符串:

JavaScript 复制代码
function Parent({ element }: { element: ElementClass }) {
  return <Row formattedDate={element.getFormattedDate()} />;
}

function Row({ formattedDate }: { formattedDate: string }) {
  return <span>{formattedDate}</span>;
}

Row 组件现在接收原始属性,但耗时或重复的计算只是向上移了一层,转移到了 Parent 组件中。

如果 element 组件频繁通过引用发生变化,element.getFormattedDate() 方法仍会频繁重新执行。因此瓶颈并未消除,只是转移了位置。

采用数据优先的架构后,你可以直接跨边界传递 isoDate 数据,并将衍生计算作为纯函数保留在需求附近。

这更契合 React 的纯粹性与不可变性模型:

实用经验法则

在 React 渲染路径中,优先采用数据优先模型而非行为丰富的类实例。

仅在边界处使用类(如领域模型、解析器、适配器),但向组件传递可序列化的纯数据,并将渲染时推导保持为纯函数。

借助 React Compiler,这通常能带来:

  1. 更高的自动备忘录命中率
  2. 更少的手动 useMemo 逃逸机制
  3. 更清晰的依赖推理
  4. 更少因对象身份变化导致的意外重计算

React Compiler 消除了大量优化工作,但仍会奖励依赖关系明确的代码。在现代 React 的 UI 渲染中,普通对象加纯辅助函数往往是更具可扩展性的选择。

相关推荐
mldong29 分钟前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排1 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60612 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角3 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!3 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰3 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi5 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen5 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马6 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒6 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端