解析 rc-field-form,探索 zero-form

在中后台开发领域,表单是当之无愧的 "核心场景载体"------ 无论是数据录入、信息查询,还是流程审批、配置管理,绝大多数业务需求都需要通过表单来落地。可以说,表单的开发效率、性能表现与可维护性,直接决定了中后台项目的整体研发质量,其重要性不言而喻。

与此同时,中后台表单场景的复杂性也远超普通 UI 组件,实际开发中常面临多重痛点:

  • 字段层面:字段数量往往多达数十个,且字段间关联逻辑复杂(如 "选择 A 选项后显示 B 字段""C 字段值依赖 D 字段计算")。这既要求性能不随字段数量增加而下降,又需要简化联动逻辑的实现,避免因频繁触发更新导致的性能损耗;
  • 数据与状态管理层面:数据上需处理前后端格式差异(如前端布尔值与后端字符串 "0/1" 的转换)、跨表单数据通信(如弹窗表单与主表单的状态同步);状态上则要应对动态自增表单项(如多行联系人信息)、字段状态回滚等复杂需求,极易出现状态混乱;
  • 扩展性层面:随着业务迭代,表单常需支持动态配置化,以适配不同业务线的定制化需求,这对表单库的灵活度提出了更高要求。

在 React 社区中,开发者针对这些痛点提出了众多表单解决方案。我在日常工作中主要使用 Ant Design 组件库,其 Form 组件凭借易用性和完整性成为主流选择。今天,我准备从 Ant Design Form 组件的底层依赖入手,探索其背后的核心实现 ------rc-field-form。

不过在聊 rc-field-form 之前,有一个 "前辈" 不得不提,那就是 rc-form。这两个库同为 Ant Design Form 组件的底层支撑:rc-form 是 Ant Design 3 版本的依赖,支撑了早期类组件时代的表单开发;而 rc-field-form 则是 Ant Design 4 版本及以后的核心依赖,针对函数组件生态做了全面优化,解决了 rc-form 的诸多痛点。

rc-form:React 表单库的经典架构与设计启示

rc-form 曾是 React 类组件时代被广泛使用的基础库,也是 Ant Design 3 版本表单的核心依赖。它通过 getFieldDecorator 方法实现表单项组件与状态的绑定,支持字段校验、统一管理等基础功能。但随着前端应用复杂度提升,其设计短板逐渐暴露:

  1. 基于高阶组件(HOC)模式封装,与当前主流的函数组件适配性欠佳;
  2. 表单状态管理与 UI 渲染耦合较深,在处理动态增减表单项、跨字段联动等场景时,容易出现性能瓶颈和逻辑混乱;
  3. 存在全量渲染的性能问题:当某个表单字段状态变化时,会触发整个表单组件重新渲染,而非仅更新发生变化的字段。

那么,既然 rc-form 已是一个相对落后的库,为何我们还要在此提及它?

主要是因为它的许多设计堪称经典,值得细细品味。用 "以字段为核心的状态管理" 和 "声明式 API 与底层逻辑的解耦" 来描述其设计,再贴切不过。

rc-form 借助高阶组件、装饰器模式及单一职责等设计理念,既简化了表单开发的复杂度,又保留了足够的灵活性,成为早期 React 表单库的标杆实现。其设计思想也深刻影响了后续的 rc-field-form、Formily、React Hook Form 等同类库。

我们不妨打开其源码(github.com/react-compo...),简单品味它 "以字段为核心的状态管理" 与 "声明式 API 与底层逻辑解耦" 这两大经典设计。

上图为rc-form源码目录。

声明式 API 与底层逻辑的解耦

rc-form的源码十分精炼,总共才1200行。

合理的分层设计,使其实现了声明式 API 与底层逻辑的解耦。可分为这三个主要部分:核心层、扩展层、接入层。

核心层

核心层是整个库的根基,承载着表单运行的核心机制,它的实现不依赖特定框架,主要包含以下模块:

  • 表单工厂(createBaseForm.js):该文件暴露创建表单实例的方法,是所有表单能力的 "入口"。它通过高阶组件(HOC)模式,向用户传入的表单组件注入表单方法,创建状态仓库(store)实例,并管理组件生命周期。
  • 状态管理(createFieldsStore.js):该文件暴露的方法用于创建store实例 ------ 这是字段状态管理的核心 "容器",负责存储所有字段的关键信息(如值 value、元数据 meta 等),并提供 getField(获取字段)、setField(更新字段)、resetFields(重置字段)等原子化操作方法。
  • 校验引擎:基于 async-validator 库封装,支持处理同步、异步两种校验场景,为字段合法性校验提供底层能力。

扩展层

扩展层基于核心层能力进行功能延伸,按需补充特定场景的实用特性(如 DOM 交互相关能力),主要包括:

  • 基础功能扩展:实现了createForm 函数,该函数新增了批量操作方法(getFieldsValue / setFieldsValue)和表单提交状态管理( isSubmitting )等。
  • DOM 交互扩展(createDOMForm):提供与 DOM 相关的增强能力,其中最经典的功能是 "校验失败时自动滚动到错误字段",提升用户交互体验。

接入层

接入层为开发者直接提供使用的 API。

  • 字段绑定工具(getFieldDecorator):这是 Ant Design 3 时期最常用的 API,本质是一个高阶装饰器。它能将普通输入组件与表单库绑定,并自动注入 value / onChange 逻辑,实现字段值的双向同步。
  • 字段包装组件:负责处理字段的挂载 / 卸载生命周期,维护 DOM 元素引用与表单状态的关联,确保字段状态与 DOM 行为同步。

以字段为核心的状态管理

在表单场景中,字段数量往往较多。若这些字段的状态都由开发者手动控制,不仅代码冗余,还容易出现状态同步混乱的问题,体验会十分糟糕。

rc-form 则通过创建 FieldsStore,将所有字段的状态(包括值、校验信息、字段元数据等)集中管理,相当于为表单状态搭建了一个 "统一仓库"。当任意字段状态发生变化时(比如通过 setFields 主动修改,或用户操作触发 onChange),rc-form 会启动更新流程,调用 forceUpdate 强制整个表单组件重新渲染,以此实现字段状态与 UI 的同步。

字段状态的变化可分为两个阶段 "初始化阶段" 和 "交互阶段" ,下图清晰展示了这两个阶段中 rc-form 的核心处理逻辑:

初始化阶段

rc-form 在创建 form 实例后,会立即初始化 fieldsStore 作为字段状态的集中管理容器。所有通过 getFieldDecorator 声明的表单项,都会被注册到这个 fieldsStore 中;同时,这些表单项会被 FieldElemWrapper 组件包裹,自动注入 value、onChange 等属性以实现双向绑定 ------ 其中 onChange 事件经过特殊处理,会成为触发 rc-form 渲染流程的入口。

交互阶段

当用户输入触发表单字段的 onChange 事件时(因已被特殊处理),rc-form 的渲染流程会被启动:首先 fieldsStore 调用 setFields 方法更新对应字段的值,随后触发一系列内部处理逻辑,最终调用表单组件的 forceUpdate 方法强制渲染。 这一更新机制正是其性能问题的根源:任何字段的状态变化,都会触发 fieldsStore 更新并通过 forceUpdate 导致整个表单组件重新渲染,即使其他未变化的字段也会随之重新渲染。

小结

尽管目前 rc-form 因技术迭代逐渐退出主流,但它的设计思想深刻影响了后续表单库的发展:

  1. 集中式状态管理的合理性:以 FieldsStore 作为表单状态 "单一数据源" 的设计,被 Formily、React Hook Form、rc-field-form 等众多主流表单库继承,成为表单状态管理的经典范式;
  2. 声明式 API 降低使用成本:getFieldDecorator 通过参数配置实现声明式开发的设计,大幅简化了表单字段绑定逻辑,如今已成为现代表单库的标配能力;
  3. 分层架构的扩展性:核心逻辑层(如状态管理、校验)与接入层(如 getFieldDecorator)分离的架构,为现代库的 "插件化扩展" 提供了关键思路,让功能迭代更灵活。

同时,rc-form 暴露的短板,也为后续表单库指明了明确的优化方向:

  • 适配函数组件:用 React Hooks 替代高阶组件(HOC),贴合当前函数组件为主的开发模式,降低使用门槛;
  • 优化渲染性能:实现字段级渲染控制,仅让状态变化的字段及依赖组件重新渲染,彻底解决全量渲染导致的性能问题;
  • 简化核心 API:减少对装饰器模式的依赖,设计更直观的 API(如 useForm),降低开发者的理解和使用成本。

作为 React 类组件时代的经典表单库,rc-form 曾成功解决了当时表单开发的核心痛点 ------ 状态分散、同步繁琐、逻辑耦合等问题。它的 "功成身退" 并非设计失败,而是前端技术迭代的必然结果;其 "以字段为核心" 的状态管理理念、"解耦底层逻辑与上层使用" 的架构思维,至今仍是前端组件库设计的重要参考,堪称 "旧时代技术留给新时代的启示"。

rc-field-form : 基于 Hooks 重构的现代化表单库

rc-form 存在的短板(如依赖高阶组件 HOC、全量渲染导致性能损耗、API 设计偏繁琐等),直接推动了 rc-field-form 向更优方向迭代 ------ 它针对性地强化了精细化状态管理、实现了高性能渲染、简化了核心 API,支持 React Hooks。这些改进让 rc-field-form 更适配现代 React 应用的开发需求,尤其在大型表单场景中,性能表现和开发体验都有了明显提升。

若要深入理解 rc-field-form 的实现原理,我们仍需从其源码切入,可参考官方仓库:github.com/react-compo... 。

上图为 rc-filed-form 的代码目录。

rc-field-form 在继承 rc-form 经典设计的同时,又解决了诸多 rc-form 的短板。以下我将从分层设计、主要模块和更新流程三个方面进行探索:

一、分层设计

作为 rc-form 的升级版,rc-filed-form 依然沿用了"解耦底层逻辑与上层使用" 的架构思想。

1、核心层

与 rc-form 逻辑相同,rc-field-form 的核心层是整个库的根基,负责管理表单核心状态(如字段值、元信息等)与核心逻辑(如值的读写操作)。它不依赖任何 UI 框架,仅专注于数据处理与逻辑管控,核心能力包括:

  • 状态管理:通过 FormStore 单例统一管理整个表单的关键状态,如字段值(value)、已注册的字段实例(fieldEntities)等,确保状态来源唯一;
  • 值操作 API:提供 getFieldValue(获取单个字段值)、setFieldValue(更新单个字段值)、getFieldsValue(批量获取字段值)、setFieldsValue(批量更新字段值)等基础方法,覆盖字段值的常用操作;
  • 字段注册与通知:初始化阶段通过 registerField 方法收集并注册所有字段;当字段值发生变化时,调用 notifyObservers 方法通知所有已注册字段,触发字段自身的 onStoreChange 逻辑,由字段自主判断是否需要重新渲染(re-render)。

2、扩展层:丰富表单能力的 "插件"

扩展层基于核心层能力构建,旨在补充表单场景所需的扩展功能(如校验、字段联动、格式处理等)。它不修改核心逻辑,仅通过 "功能增强" 或 "场景适配" 的方式扩展能力,核心包括:

  • 校验扩展:内置必填、格式(如手机号、邮箱)等基础验证规则,同时支持自定义验证函数(validator),满足复杂场景下的字段合法性校验需求;

  • 依赖管理:rc-form 因全量渲染无需考虑字段依赖,而 rc-field-form 为实现 "字段级精准更新",必须完善依赖管理 ------ 例如 A 字段值依赖 B 字段,当 B 变化时 A 需同步更新:

    • 通过 dependencies 配置项定义字段依赖关系;
    • 核心层的 notifyObservers 方法会根据依赖关系,精准通知关联字段触发重渲染与重新校验,避免无关字段更新。

3、接入层:连接 React 组件的 "桥梁"

接入层承担着核心层底层逻辑与 React 生态的 "衔接" 职责,它对外提供开发者可直接调用的 React 组件与 Hook,将复杂的底层状态管理细节完全屏蔽,大幅降低了使用门槛。其中,核心 Hook 为 useForm,核心组件则包括 Form(表单容器)与 Field(字段包装组件)。

二、主要模块

在分析完 rc-field-form 的各层设计后,我们来看一个实际使用案例。该案例中用到了 useForm、Form 组件与 Field 组件 ------ 这三者正是 rc-field-form 接入层的核心模块,能直观体现其使用逻辑。

ini 复制代码
import Form, { Field } from 'rc-field-form';
import React from 'react';
import Input from './components/Input';


export default () => {
const [form] = Form.useForm();

return (
  <Form
    form={form}
    preserve={false}
    onFieldsChange={fields => {
      console.error('fields:', fields);
    }}
  >
    <Field name="name">
      <Input placeholder="Username" />
    </Field>

    <Field<FormData> dependencies={['name']}>
      {() => {
        return form.getFieldValue('name') === '1' ? (
          <Field name="password">
            <Input placeholder="Password" />
          </Field>
        ) : null;
      }}
    </Field>

    <button type="submit">Submit</button>
  </Form>
  );
};

来看各个的核心模块的具体实现

FormStore 模块(useForm.ts)

该模块负责创建表单的数据仓库 store。我对源码做了部分精简并添加注释,使其更易读。从代码结构可看出,该模块主要包含两部分:useForm 和 FormStore。

  • FormStore 类: 其内部属性(如 store、fieldEntities 等)用于存储字段相关信息;提供 getFieldValue(获取字段值)、setFieldValue(设置字段值)等方法操作 store 中的数据。核心的 notifyObservers 方法承担通知职责 ------ 每当通过 setFieldsValue 等方法修改 store 后,会触发该方法遍历所有已注册的字段实体,调用其 onStoreChange 方法以同步状态。
  • useForm Hook: 作为创建入口,用于初始化 Form 实例与 FormStore 实例,并通过 React 机制确保 FormStore 实例为单例(全表单共享同一状态仓库)。
js 复制代码
import * as React from 'react';
import { getNamePath, getValue, setValue } from './utils/valueUtil';
/**
 * 表单数据仓库类
 * 负责管理表单的状态、字段注册、值的更新等核心逻辑
 */
class FormStore {
  // 表单当前值存储(键值对形式,如 { username: 'xxx', age: 20 })
  private store = {};

  // 表单初始值存储
  private initialValues = {};

  // 已注册的字段实体列表(每个字段对应一个Field组件实例)
  private fieldEntities = [];

  // 强制重新渲染的函数(由React状态更新触发)
  private forceUpdate: () => void;

  constructor(forceUpdate: () => void) {
    // 用于触发表单重新渲染的函数
    this.forceUpdate = forceUpdate;
  }

  
   //对外暴露表单表单实例方法,就是我们经常使用的formRef
  getForm = () => ({
    getFieldValue: this.getFieldValue,    // 获取单个字段值
    setFieldValue: this.setFieldValue,    // 设置单个字段值
    getFieldsValue: this.getFieldsValue,  // 获取所有字段值
    setFieldsValue: this.setFieldsValue,  // 批量设置字段值
    resetFields: this.resetFields,        // 重置字段值
    _init: true,                          // 标记表单已初始化
    getInternalHooks: this.getInternalHooks, // 获取内部钩子(供组件内部使用)
  });

   // 内部钩子方法提供给表单内部组件(如Field)注册和初始化的接口
  private getInternalHooks = () => ({
    registerField: this.registerField,      // 注册字段实体
    setInitialValues: this.setInitialValues, // 设置初始值
  });


  //注册字段实体
  private registerField = (entity) => {
    // 将字段实体添加到列表中
    this.fieldEntities.push(entity);
    // 返回取消注册的函数(用于组件卸载时清理)
    return () => {
      this.fieldEntities = this.fieldEntities.filter(item => item !== entity);
    };
  };


  //设置初始值
  private setInitialValues = (value) => {
    this.initialValues = { ...values }; // 保存初始值
    this.store = { ...this.store, ...values }; // 初始化当前值
  };


  //获取单个字段值
  private getFieldValue = (name) => {
    const namePath = getNamePath(name); // 统一转换为内部路径格式(数组)
    return getValue(this.store, namePath); // 从存储中获取值
  };


  //获取所有字段值
  private getFieldsValue = () => ({ ...this.store }); // 返回当前存储的拷贝

 
  // 设置单个字段值
  private setFieldValue = (name, value: any) => {
    const namePath = getNamePath(name); // 转换为内部路径
    const prevStore = { ...this.store }; // 保存更新前的状态
    this.store = setValue(this.store, namePath, value); // 更新存储中的值
    
    // 通知所有字段实体存储已更新
    this.notifyObservers(prevStore, [namePath], { type: 'valueUpdate' });
    
  };


  //批量设置字段值
  private setFieldsValue = (values) => {
    const prevStore = { ...this.store }; // 保存更新前的状态
    // 遍历所有值并更新存储
    Object.entries(values).forEach(([key, value]) => {
      this.store = setValue(this.store, getNamePath(key), value);
    });
    
    // 通知所有字段实体存储已更新
    this.notifyObservers(prevStore, null, { type: 'valueUpdate' });

  };

  //通知所有字段
  private notifyObservers = (prevStore, namePathList, info) => {
    // 遍历所有字段实体,调用其onStoreChange方法
    this.fieldEntities.forEach(entity => {
      entity.onStoreChange(prevStore, namePathList, { ...info, store: this.store });
    });
  };
}


export default function useForm(form?: any) {
  // 创建一个用于强制更新的React状态
  const [, forceUpdate] = React.useState({});

  // 用ref保存表单实例(避免每次渲染重新创建)
  const formRef = React.useRef<any>();

  // 初始化表单实例(只在首次渲染时执行)
  if (!formRef.current) {
    if (form) {
      // 如果传入了外部表单实例,直接使用
      formRef.current = form;
    } else {
         // 否则创建新的表单仓库实例
      const formStore = new FormStore(() => {
        // 强制更新的回调(通过更新React状态触发重渲染)
        forceUpdate({});
      });
      formRef.current = formStore.getForm();
    }
  }

  // 返回表单实例
  return [formRef.current];
}

Form 模块(Form.tsx )

就是是我们经常使用的Ant Design的Form组件,它是表单容器组件,负责初始化表单实例、管理上下文等。

  • 它接收 initialValues(表单初始值)、form(外部传入的表单实例)等配置,通过 useForm 钩子创建新的表单实例或复用外部实例,并借助 FieldContext(上下文)将实例传递给所有子组件,确保子表单项能共享表单状态;
  • 组件的 children 属性即开发者传入的具体表单项(如输入框、选择器等),这些子项通过上下文获取表单实例,实现与全局状态的绑定。
js 复制代码
import * as React from 'react';
import useForm from './useForm';
import FieldContext, { HOOK_MARK } from './FieldContext';
import FormContext from './FormContext';
import { isSimilar } from './utils/valueUtil';
import ListContext from './ListContext';


/**
* 表单组件核心
* 负责管理表单整体状态、上下文传递和事件处理
*/
const Form = (
 {
   name,
   initialValues,
   fields,
   form: userForm,
   children,
   validateTrigger = 'onChange',
   clearOnDestroy,
   ...restProps
 },
 ref,
) => {
 // 原生表单元素引用
 const nativeElementRef = React.useRef<HTMLFormElement>(null);
 
 // 获取全局表单上下文
 const formContext = React.useContext(FormContext);

 // 初始化表单实例(复用用户传入的或创建新的)
 const [formInstance] = useForm(userForm);

 // 获取内部钩子(用于表单内部状态管理)
 const {
   useSubscribe,
   setInitialValues,
   setCallbacks,
   setValidateMessages,
   setPreserve,
   destroyForm,
 } = formInstance.getInternalHooks(HOOK_MARK);

 // 暴露表单实例到ref
 React.useImperativeHandle(ref, () => ({
   ...formInstance,
   nativeElement: nativeElementRef.current,
 }));

 // 注册表单到全局上下文
 React.useEffect(() => {
   if (name) {
     formContext.registerForm(name, formInstance);
   }
   return () => {
     if (name) {
       formContext.unregisterForm(name);
     }
   };
 }, [formContext, formInstance, name]);


 // 初始化表单值(仅首次挂载时)
 const mountRef = React.useRef(false);
 if (!mountRef.current) {
   setInitialValues(initialValues || {}, true);
   mountRef.current = true;
 }

 // 组件卸载时清理
 React.useEffect(
   () => () => {
     destroyForm(clearOnDestroy);
   },
   [destroyForm, clearOnDestroy],
 );

 // 处理子元素渲染 (使用者传入的组件)
 let childrenNode: React.ReactNode;
 if (typeof children === 'function') {
   // 渲染函数模式:传入当前表单值和实例
   const values = formInstance.getFieldsValue(true);
   childrenNode = (children )(values, formInstance);
 } else {
   // 普通子元素模式
   childrenNode = children;
 }


 // 监听fields变化并更新表单
 const prevFieldsRef = React.useRef([]);
 React.useEffect(() => {
   if (!isSimilar(prevFieldsRef.current, fields || [])) {
     formInstance.setFields(fields || []);
     prevFieldsRef.current = fields || [];
   }
 }, [fields, formInstance]);

 // FieldContext上下文
 const formContextValue = React.useMemo(
   () => ({
     ...(formInstance),
     validateTrigger,
   }),
   [formInstance, validateTrigger],
 );

 // 包裹字段上下文和列表上下文的节点
 const wrapperNode = (
   <ListContext.Provider value={null}>
     <FieldContext.Provider value={formContextValue}>
       {childrenNode}
     </FieldContext.Provider>
   </ListContext.Provider>
 );


 // 使用指定组件作为包裹容器
 return (
   <Component
     {...restProps}
     ref={nativeElementRef}
     // 处理表单提交
     onSubmit={(event) => {
       event.preventDefault(); // 阻止默认提交
       event.stopPropagation();
       formInstance.submit(); // 触发表单提交逻辑
     }}
     // 处理表单重置
     onReset={(event) => {
       event.preventDefault();
       formInstance.resetFields(); // 触发表单重置
       restProps.onReset?.(event); // 调用用户重置回调
     }}
   >
     {wrapperNode}
   </Component>
 );
};

export default Form;

Field 模块(Field.tsx)

作为表单项的基础组件,Field 是实现表单控件(如输入框、选择器等)与表单状态双向绑定的核心 ------ 所有表单组件都需要被 Field 包裹才能接入表单系统。

  • 初始化阶段(componentDidMount),Field 会将当前字段注册到 FormStore 中,同时建立数据变化的监听机制,确保表单状态变更时能同步到 UI;
  • 核心方法 onStoreChange 负责响应状态更新:当字段关联的状态变化时,它会调用 forceUpdate 强制当前 Field 组件重新渲染,实现 UI 与状态的同步;
  • getControlled 方法则专门处理控件的交互逻辑,其中对 onChange 事件做了特殊封装,使其触发时能接入 rc-field-form 的更新流程(如更新 FormStore 状态、通知关联字段等),形成完整的双向绑定闭环。
js 复制代码
import React from 'react';


/**
 * 表单字段组件
 * 负责管理单个表单字段的状态、验证和与表单仓库的交互
 */
class Field extends React.Component {
  static contextType = FieldContext;

  state = {
    resetCount: 0
  };

  private mounted = false;             // 组件是否已挂载
  private cancelRegisterFunc: (() => void) | null = null; // 取消注册的函数
  private touched = false;             // 字段是否被触碰过
  private dirty = false;               // 字段值是否被修改过

  constructor(props) {
    super(props);
    // 初始化时将字段初始值注册到表单仓库
    if (props.fieldContext) {
      const { initEntityValue } = props.fieldContext.getInternalHooks();
      initEntityValue(this);
    }
  }

  componentDidMount() {
    this.mounted = true;
    // 注册字段实例到表单仓库
    if (this.props.fieldContext) {
      const { registerField } = this.props.fieldContext.getInternalHooks();
      this.cancelRegisterFunc = registerField(this);
    }
  }

  componentWillUnmount() {
    this.mounted = false;
    // 取消注册
    if (this.cancelRegisterFunc) {
      this.cancelRegisterFunc();
    }
  }

  /**
   * 获取字段的完整路径(包含前缀)
   */
  getNamePath = () => {
    const { name, fieldContext } = this.props;
    const { prefixName = [] } = fieldContext;
    return name ? [...prefixName, ...name] : [];
  };

  /**
   * 获取当前字段的值
   */
  getValue = (store?: Store): StoreValue => {
    const { getFieldsValue } = this.props.fieldContext;
    const namePath = this.getNamePath();
    return getValue(store || getFieldsValue(true), namePath);
  };

  /**
   * 处理表单仓库变化的回调
   * 当仓库数据变化时更新字段状态
   */
  onStoreChange = (prevStore, namePathList, info) => {
    const prevValue = this.getValue(prevStore);
    const curValue = this.getValue(info.store);

    // 处理重置事件
    if (info.type === 'reset') {
      this.touched = false;
      this.dirty = false;
      this.forceUpdate();
      return;
    }

    // 当值变化时更新状态
    if (prevValue !== curValue) {
      this.forceUpdate();
    }
  };


  /**
   * 获取受控组件属性(绑定value和onChange)
   */
  getControlled = () => {
    const {
      trigger = 'onChange',
      valuePropName = 'value',
      getValueFromEvent,
      fieldContext
    } = this.props;
    
    const namePath = this.getNamePath();
    const value = this.getValue();
    const { dispatch } = fieldContext.getInternalHooks(HOOK_MARK);

    return {
      [valuePropName]: value,
      [trigger]: (...args: any[]) => {
        // 标记为已触碰
        this.touched = true;
        this.dirty = true;

        // 从事件中获取新值
        const newValue = getValueFromEvent 
          ? getValueFromEvent(...args) 
          : defaultGetValueFromEvent(valuePropName, ...args);

        // 分发更新值的动作
        if (newValue !== value) {
          dispatch({
            type: 'updateValue',
            namePath,
            value: newValue
          });
        }
      }
    };
  };

  render() {
    const { children } = this.props;
    const { resetCount } = this.state;
    const meta = this.getMeta();
    const controlledProps = this.getControlled();

    // 处理渲染函数形式的子元素
    if (typeof children === 'function') {
      return (
        <React.Fragment key={resetCount}>
          {children(controlledProps, meta)}
        </React.Fragment>
      );
    }

    // 处理普通子元素(仅支持单个子元素)
    const child = React.Children.only(children as React.ReactElement);
    return React.cloneElement(child, {
      ...child.props,
      ...controlledProps
    });
  }

}
export default Field;

三、字段更新流程

和 rc-form 一样,rc-field-form 也通过创建状态仓库(FieldsStore)对所有字段的状态(包括值、校验信息等)进行集中管理。当任意字段状态发生变化时(比如通过 setFields 主动修改,或用户操作触发 onChange),表单项组件会调用 forceUpdate 强制更新。

二者的核心区别在于:rc-form 调用的是整个表单组件的 forceUpdate,导致全量渲染;而 rc-field-form 调用的是发生变化的表单项自身的 forceUpdate,实现了精准的字段级更新,从根本上优化了渲染性能。

1、初始化阶段

  1. 表单实例与状态仓库创建:通过 useForm Hook 创建新的表单实例(或复用外部传入的实例),同时初始化 filedStore 实例,作为所有字段状态的集中管理容器。
  2. 上下文注册与数据共享:创建 FormContext 和 FieldContext 两个上下文,用于向下传递表单实例、状态及核心方法,为内部 Field 组件提供跨层级数据访问能力,避免 props 透传冗余。
  3. 字段注册与属性注入:表单项首次挂载时,会自动注册到表单系统并记录到 filedStore 中;同时,所有表单项会被 WrapperField 组件包裹,自动注入 value、onChange 等关键属性 ------ 其中 onChange 经过特殊封装,确保触发时能接入 rc-field-form 的统一更新流程。

2、交互阶段

当通过 setFieldValue 主动修改字段值,或用户操作触发表单项的 onChange 事件时,会触发以下流程:

  1. 状态更新:表单项调用经过特殊处理的 onChange 方法,进而触发 FormStore 的 setFieldValue 方法,更新 filedStore 中对应字段的状态;
  2. 订阅通知触发:FormStore 完成状态更新后,立即调用 notifyWatch 方法,通知所有订阅了该字段变化的组件;
  3. 依赖字段联动更新:根据 dependencies 配置的依赖关系,精准通知依赖当前字段的其他 Field 组件,触发这些组件重新渲染与状态同步,实现字段间的联动效果。

小结

作为 rc-form 的迭代升级版本,rc-field-form 既继承了前者的核心设计思想,又针对函数组件时代的开发需求做了全面优化,最终成为 React 生态中轻量且灵活的表单解决方案。其核心优势可概括为三点:

  1. 分层架构的极致解耦:采用 "核心层(FormStore 管理状态与逻辑)- 扩展层(校验规则 / 字段联动)- 接入层(Field/Form 组件对接 UI)" 的三层设计,实现 "数据 - 逻辑 - UI" 的彻底分离 ------ 既保证核心逻辑可复用,又能为动态列表、跨表单联动等扩展场景提供灵活支撑;
  2. "字段实例化" 的状态管理:每个 Field 组件作为独立实例注册到 FormStore,通过 getNamePath 实现嵌套字段的精准定位。这一设计既解决了 rc-form 中 "装饰器模式" 的耦合问题,又能确保动态表单项的状态不丢失,目前已被 Ant Design Form 等主流组件库采用;
  3. 轻量化的性能优化思路:摒弃 rc-form 中 "全量字段监听" 的模式,通过 "依赖字段更新"(dependencies 配置)、"值对比触发渲染"(shouldUpdate 控制)等设计,让字段更新仅触发自身及关联组件重渲染。在保持轻量体积(无冗余依赖)的同时,完美适配复杂中后台表单场景。

rc-field-form 并非对 rc-form 的颠覆,而是 "继承式创新"------ 它保留了前者 "以字段为核心" 的状态管理思想,又通过分层解耦、性能优化、API 轻量化,解决了函数组件时代表单开发的效率与性能痛点。作为 Ant Design Form v4+ 的底层依赖,它不仅是一款优秀的表单库,更成为中后台表单 "分层设计""实例化状态管理" 的标杆,其 "轻量不简单、灵活不冗余" 的设计理念,至今仍是前端组件库设计的重要参考。

zero-form:业务模型驱动的现代化表单库

不过,即便 rc-field-form 已足够优秀,在日常工作中,尤其是中后台项目的重业务表单场景下,仅通过编写组件配置渲染表单的方式仍存局限。核心问题指向 "分离":rc-field-form 虽已实现内部 "组件渲染与状态管理的分离",但实际开发中,我们常将 "表单渲染逻辑" 与 "业务处理逻辑" 混在一起。随着需求迭代,代码容易逐渐 "腐化",最终演变为难以维护的 "屎山代码"。

日常需求多以业务为核心驱动,因此构建表单时,我们不应止步于编写表单项配置,而应进一步搭建专门的 "业务逻辑层";再结合业务驱动特性,我们应当构建的是"业务模型"------ 让表单渲染与业务逻辑彻底解耦,以此应对复杂多变的业务场景。

即便在开发过程中,开发者有意识地做分层设计,但在 "如何构建业务模型" 上仍存在认知差异:前后端解耦如何实现?逻辑与视图如何彻底分离?数据转换逻辑该写在哪、怎么写、写成什么形式?

我们认为,表单库应当主动引导开发者实现这种分层。这一分层思路与 Formily 有相似之处,但 Formily 并未规范业务逻辑层的实现方式;而我们希望提供一套标准,帮助用户落地业务逻辑层。正是基于上述思考,我们政采事业部前端团队正在开发一款 "业务模型驱动" 的现代化表单库,并将其命名为 zero-form,该表单库提供了基于分层架构的开发模式,隔离变化,提供稳定的业务模型。

基于上面的思考,我们需要解决如下几个核心问题 :

  1. 逻辑渲染隔离:如何实现业务逻辑与渲染逻辑的完全解耦
  2. 表单结构范式:如何规范表单结构的定义与书写标准
  3. 高性能动态渲染:如何解决大表单初始化与运行时的性能瓶颈,核心是实现字段级精准更新
  4. 依赖管理:如何围绕字段本身做管理,从 "主动修改联动字段" 转为 "监听联动字段、仅更新自身",让联动逻辑收拢到被联动字段,实现逻辑可测试、可迭代、可维护,并能快速分析字段依赖关系

1. 分层设计

zero-form 不仅延续 "UI 与状态分离" 的优势,更突破现有表单库局限,实现 UI 与业务逻辑的深度分离,最终构建稳定的前端视图模型 ------ 业务逻辑与 UI 完全解耦。其价值在于:API 接口层变更时仅需修改业务逻辑层,无需调整 UI;UI 重构时仅需更新组件,不影响业务逻辑。具体分层如下:

  • UI 层:纯展示组件,仅接收字段配置和状态,不包含任何业务逻辑。提供了组件注册方法,可无缝切换 Ant Design、Element 等主流组件库。
  • 状态层:承担数据存储、更新、依赖管理的核心职责,与主流表单库逻辑类似,通过单例 store 统一管理所有字段状态,并暴露修改 store 的方法,确保状态来源唯一;
  • 业务模型层:业务逻辑的 "载体",与 UI 完全无关,本质是定义业务相关的数据结构与处理方法。zero-form 会暴露标准化的业务模型类,开发者需按规范实例化专属业务模型 ------ 包含模块实例化、数据初始化、表单结构生成、具体业务处理方法等核心能力。实例化后的业务模型将以 ref 形式暴露,供开发者调用。尽管这会增加部分初始代码量,但从长期迭代与稳定性来看,明确的业务模型能大幅降低需求变更成本,带来不可替代的灵活性。

表单结构范式

在 UI 层,zero-form 采用统一的表单结构描述协议(Schema),规范字段配置的书写方式,避免 "配置分散、格式混乱":

  • Schema 以数组形式描述表单项,每个字段需包含标准化属性:id(唯一标识)、type(字段类型,如输入框、下拉框)、props(UI 配置,如占位符、样式)、rules(校验规则,如必填、格式校验)。
  • 支持复杂结构:适配嵌套结构(如 object 嵌套子字段、array 动态列表)与动态字段(通过 dependencies 声明依赖关系),确保复杂表单的结构一致性。
  • 生成逻辑统一:业务模型层通过 generateSchema 方法输出标准化 Schema,UI 层基于 Schema 自动渲染,无需手动拼接配置。

双开发模式

构建业务模型虽能应对复杂场景,但在简单需求下难免冗余。为此,zero-form 设计两种开发模式:

  • Form 开发模式:面向简单场景,用法与 Ant Design Form 等库一致 ------ 直接通过配置项快速渲染表单,无需定义复杂业务模型,大幅降低轻量需求的开发成本。
  • 业务模型开发模式:面向复杂业务场景,需先实例化业务模型搭建业务逻辑层,再通过配置表单项构建 UI 层,实现 "业务 - UI" 彻底解耦,支撑长期迭代与需求变更。

2. 高性能动态渲染

针对百级以上字段的大表单,zero-form 采用 "字段级按需更新" 策略,攻克初始化与运行时的性能瓶颈:

  • 状态分片存储:按字段 id 分片存储状态,每个字段的状态(值、校验结果、UI 状态)独立管理,避免单状态对象过大导致的更新性能损耗。
  • 精确依赖追踪:通过字段 dependencies 声明依赖关系,当某个字段更新时,仅重新计算其关联字段的状态(如是否可见、是否禁用),而非全表单重渲染,减少无效计算。
  • 虚拟列表与延迟加载:对 array 类型的长列表表单,结合虚拟滚动技术,仅渲染可视区域内的表单项;非首屏字段采用延迟初始化,降低初始加载时间,提升首屏渲染速度。

3. 依赖管理

zero-form 以 "字段自治" 为核心,重构字段联动逻辑,解决传统联动 "逻辑分散、难以维护" 的问题:

  • 被动联动:每个字段仅关注自身依赖的字段,通过监听依赖字段的变化更新自身状态(如 visible、value),而非主动修改其他字段。例如:字段 B 依赖字段 A 时,字段 B 需定义 dependencies: 'A',并在 A 变化时触发自身的 onDepsChange 方法更新状态 ------ 彻底消除 "主动修改其他字段" 导致的逻辑混乱。
  • 依赖图可视化:基于图论构建字段依赖图,以顶点表示字段、边表示依赖关系,结合拓扑排序确保联动逻辑按正确顺序执行,同时支持可视化展示依赖关系,便于调试与维护。
  • 高可测试性:联动逻辑收拢在字段自身,可针对单个字段编写单元测试(如模拟依赖字段变化,验证自身状态是否符合预期),大幅提升测试覆盖率,保障迭代安全性。

目前 zero-form 已经处于内测阶段,敬请期待!

相关推荐
miss6 分钟前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan1 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真1 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment1 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan1 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么1 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技2 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue
西安小哥2 小时前
端侧跑大模型,真正的门槛不在参数量:一次 596 秒到 10 秒的压缩路径复盘
前端·人工智能
陈随易2 小时前
Cloudflare 出钱,Vite 团队挑人,44 个维护者分 30 万美元
前端·后端·程序员
恋猫de小郭3 小时前
Flutter THLC 的 WebView 滚动终于从 70 FPS 提升到 120 FPS
android·前端·flutter