解析 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 组件包裹,自动注入 valueonChange 等属性以实现双向绑定 ------ 其中 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 的各层设计后,我们来看一个实际使用案例。该案例中用到了 useFormForm 组件与 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。我对源码做了部分精简并添加注释,使其更易读。从代码结构可看出,该模块主要包含两部分:useFormFormStore

  • 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 方法,进而触发 FormStoresetFieldValue 方法,更新 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 已经处于内测阶段,敬请期待!

相关推荐
钱学敏5 小时前
Webpack 与 Gradle:构建工具的类比之旅
前端
用户4015442952615 小时前
vue 设置代理后,get请求正常,post请求报403
前端
李大玄5 小时前
ClipboardApp —— Mac 专属轻量级剪切板助手(开源)
前端·javascript·electron
bitbitDown5 小时前
如何优雅忽略 components.d.ts的更新
前端·javascript·vue.js
我是若尘5 小时前
event.currentTarget 、event.target 傻傻分不清楚?
前端
Dontla5 小时前
前端埋点(tracking)技术介绍(记录用户行为和页面性能数据)(埋点代码)ajax埋点、img埋点、navigator.sendBeacon埋点
前端·javascript·ajax
533_6 小时前
[css] flex 布局设置文字不自动换行
前端·css
guojb8246 小时前
元数据驱动:打造动态灵活的Vue键值对表格组件
前端·vue.js·element
学Linux的语莫6 小时前
langchain输出解析器
java·前端·langchain