探索 Ant Design Form 的实现原理——从 HOC 到 Hooks

2,713 阅读15分钟

作为前端开发者,表单几乎是我们日常工作中最频繁接触的组件。而 Ant Design Form 又是 React 开发者最常用的组件库。从简单的登录框到复杂的数据录入界面,它几乎无处不在。

你是否曾好奇过它的实现原理?或者注意到从 3.x 到 4.x 版本时的那次重大变化?从 Form.create() 高阶组件到 Form.useForm() Hooks API,这不仅是 API 设计的改变,更是整个表单状态管理思路的转变。

这种演进背后有着怎样的技术考量?旧版实现存在哪些局限?新版又是如何解决这些问题的?本文将带你深入理解 Ant Design Form 的实现原理,探索从高阶组件到 Hooks 的演进历程。

3.x 版本——HOC 模式

设计思想

在 3.x 版本中,Ant Design Form 的核心设计采用状态提升(State Lifting)策略。通过将表单控件(如 Input)和提交按钮的共有状态(values/errors)存储在它们最近的共同祖先组件中,实现了:

  1. 跨组件状态共享
  2. 统一校验逻辑
  3. 数据流集中管理

HOC 模式解析

高阶组件(Higher-Order Component,HOC)是该版本的核心实现模式,其本质是一个函数,接受一个组件返回一个新组件的函数式编程范式。核心优势包括:

  • 逻辑解耦:分离表单逻辑与UI呈现
  • 复用能力:通用表单逻辑可跨组件复用
  • 状态隔离:维护独立的状态管理上下文

实战演示:Form 表单 HOC 版本简化实现

使用示例

我们从一个简单的使用示例开始:

const nameRules = {required: true, message: "请输入用户名!"};
const passwordRules = {required: true, message: "请输入密码!"};

@Form.create()
class MyForm extends Component {
  componentDidMount() {
    this.props.form.setFieldsValue({username: "小明"});
  }

    submit = () => {
    const {getFieldsValue, validateFields} = this.props.form;
    console.log("submit", getFieldsValue()); 
    validateFields((err, val) => {
      if (err) {
        console.log("err", err); 
      } else {
        console.log("校验成功", val); 
      }
    });
  };

  render() {
    const {getFieldDecorator} = this.props.form;
    return (
      <div>
        <h3>MyForm</h3>
        {getFieldDecorator("username", {rules: [nameRules]})(
          <Input placeholder="Username" />
        )}
        {getFieldDecorator("password", {rules: [passwordRules]})(
          <Input placeholder="Password" />
        )}

        <button onClick={this.submit}>submit</button>
      </div>
    );
  }
}

export default MyForm;

手写 Form.create()

以这个 demo 为例,如果我们要自己实现一个高阶组件来替代 Form.create() API,该如何着手呢?

HOC 核心架构初始化

首先高阶组件是接收一个组件,返回一个组件:

import React, {Component} from "react";

export default function createForm(WrappedComponent) {
  return class extends Component {
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}
状态提升集中管理

文章开头我们提到过,它使用状态提升实现了对表单数据的统一管理,结合我们的示例,数据流向示意图如下:

Form.create()返回的HOC组件
  ├── 存储表单状态
  └── 渲染 MyForm 组件 (对应WrappedComponent)
       ├── 渲染 Input 控件 (username)
       ├── 渲染 Input 控件 (password)
       └── 渲染 submit 按钮

那么如何在 HOC 中收集这些状态呢? 首先给它创建一个 state 用于存储所有表单项的值:

return class extends Component {
  constructor(props) {
    super(props);
    this.state = {}; // 用于存储所有表单项的值
  }
  // ...
}

至于收集,那就要依靠每个表单项外面包裹的 getFieldDecorator 了, 它是整个实现的关键:

getFieldDecorator = (field, options) => (InputComponent) => {
  return React.cloneElement(InputComponent, {
    name: field,
    value: this.state[field],
    onChange: this.handleChange,
  });
};

这里采用了双层函数的设计:

  • 第一层接收字段名和配置选项

  • 第二层接收表单控件组件

  • 最终返回注入了特定属性的克隆组件

通过 cloneElement,它为原始表单控件注入了三个关键属性:

  1. name:字段标识,用于追踪状态
  2. value:从 HOC 的 state 中读取值
  3. onChange:统一的事件处理函数

当用户输入时,handleChange 方法被调用:

handleChange = (e) => {
  this.setState({
    [e.target.name]: e.target.value,
  });
};

这里使用了计算属性名,根据事件对象的 name 属性更新对应的状态。这样一来,所有表单控件的状态变更都会统一流向 HOC 的 state。最后,通过 getForm 方法将 getFieldDecorator 传递给被包装组件,完整代码如下:

import React, { Component } from "react";

export default function createForm(WrappedComponent) {
  return class extends Component {
    constructor(props) {
      super(props);
      this.state = {};
    }
    handleChange = (e) => {
      this.setState({
        [e.target.name]: e.target.value,
      });
    };
    getFieldDecorator = (field, options) => (InputComponent) => {
      return React.cloneElement(InputComponent, {
        name: field,
        value: this.state[field],
        onChange: this.handleChange,
      });
    };
    validateFields = () => {};
    getFieldsValue = () => {};
    setFieldsValue = () => {};
    getForm = () => {
      return {
        form: {
          getFieldDecorator: this.getFieldDecorator,
          validateFields: this.validateFields,
          getFieldsValue: this.getFieldsValue,
          setFieldsValue: this.setFieldsValue,
        },
      };
    };
    render() {
      return <WrappedComponent {...this.props} {...this.getForm()} />;
    }
  };
}

这里为了防止代码报错,定义了 demo 中引用到的其他方法,正好大家也可以在这里暂停一下,拿这段代码作为起点,尝试自己实现 Form.create() 提供的其他的方法。

表单 API 封装 (get、set、校验)

getFieldsValuesetFieldsValue 实现起来是比较简单的:

getFieldsValue = () => {
  return { ...this.state };
};
setFieldsValue = (newState) => {
  this.setState(newState);
};

这里我们重点看一下校验的实现:

export default function createForm(WrappedComponent) {
  return class extends Component {
    constructor(props) {
      super(props);
      this.state = {};
      this.options = {};
    }
    // ...
    getFieldDecorator = (field, options) => (InputComponent) => {
      this.options[field] = options;
      // ...
    };
    validateFields = (callback) => {
      let error = {};
      for (let field in this.options) {
        if (!this.state[field]) {
          error[field] = this.options[field].required;
        }
      }
      if (Object.keys(error).length > 0) {
        callback(error, this.state);
      } else {
        callback(null, this.state);
      }
    };
    // ...
  };
}

整体工作流程:

  1. 在表单初始化时,this.options 为空对象
  2. 当使用 getFieldDecorator 装饰字段时,将校验规则存入 this.options
  3. 用户点击提交按钮时,调用 validateFields 方法
  4. validateFields 遍历 this.options 中的所有字段进行校验
  5. 将校验结果通过回调函数返回给调用者
最终代码
import React, { Component } from "react";

export default function createForm(WrappedComponent) {
  return class extends Component {
    constructor(props) {
      super(props);
      this.state = {};
      this.options = {};
    }
    handleChange = (e) => {
      this.setState({
        [e.target.name]: e.target.value,
      });
    };
    getFieldDecorator = (field, options) => (InputComponent) => {
      this.options[field] = options;
      return React.cloneElement(InputComponent, {
        name: field,
        value: this.state[field],
        onChange: this.handleChange,
      });
    };
    validateFields = (callback) => {
      let error = {};
      for (let field in this.options) {
        if (!this.state[field]) {
          error[field] = this.options[field].required;
        }
      }
      if (Object.keys(error).length > 0) {
        callback(error, this.state);
      } else {
        callback(null, this.state);
      }
    };
    getFieldsValue = () => {
      return { ...this.state };
    };
    setFieldsValue = (newState) => {
      this.setState(newState);
    };
    getForm = () => {
      return {
        form: {
          getFieldDecorator: this.getFieldDecorator,
          validateFields: this.validateFields,
          getFieldsValue: this.getFieldsValue,
          setFieldsValue: this.setFieldsValue,
        },
      };
    };
    render() {
      return <WrappedComponent {...this.props} {...this.getForm()} />;
    }
  };
}

思考

HOC 实现的性能缺陷

HOC 模式虽然设计巧妙,但其核心缺陷在于状态提升导致的渲染效率问题。由于表单状态集中存储在HOC组件中,任何字段的变化都会触发整个组件树的重新计算:每当用户在输入框中输入内容时,会引发以下级联反应:

  1. 触发 handleChange 事件处理
  2. 执行 this.setState() 更新HOC状态树
  3. HOC组件因状态变化而重新渲染
  4. 作为子组件的 MyForm 接收新的props引用,触发完整重渲染
  5. 所有表单字段组件(包括未修改的)一同重新渲染

大家可以在 MyForm 的 render 函数中打个 console 验证一下。

这意味着即使用户仅修改"用户名"输入框,"密码"输入框以及整个表单结构都会不必要地重新计算和渲染,在表单项较多或结构复杂时会造成明显的性能损耗。

HOC 优化的局限性

在HOC架构下,这类性能问题的解决面临多重障碍:

  1. 组件优化手段失效:即使在被包装组件上应用 React.memo 或使用 PureComponent,由于每次都会接收新的props引用,浅比较机制无法阻止重渲染
  2. 状态粒度问题:所有字段状态被合并在同一对象中(this.state),缺乏独立更新的机制,难以实现字段级的渲染优化
  3. 组件层级复杂化:HOC模式增加了组件嵌套深度,使性能优化需要在多个层级协同处理,增加了维护难度
  4. 调试与追踪困难:由于props来源不直观,排查渲染性能问题时难以确定变更的确切来源

这些问题正是React Hooks设计所要解决的关键痛点,也解释了为什么表单实现会从HOC模式转向Hooks模式。

4.x/5.x 版本——Hooks API

设计思想

HOC 的状态提升存在一些固有的缺陷,比如组件嵌套层级过深、状态更新可能触发不必要的重渲染、状态管理逻辑与 UI 组件耦合等问题。那么,还有什么更好的方式来统一管理 Form 中的状态值呢?

我们可以借鉴 Redux 的思想,创建一个独立的数据管理仓库(FormStore),通过发布订阅模式来实现状态管理。这种方式的核心是:将状态管理从组件中抽离出来,统一由 FormStore 来管理,并规定好这个数据仓库的 get、set 方法。当状态发生变化时,FormStore 会通知相关的订阅者(表单项组件)进行更新,而不是触发整个表单树的重渲染。

这种实现方式不仅解决了 HOC 的固有问题,还为表单功能的扩展(如表单验证、依赖关系等)提供了更大的灵活性。通过发布订阅模式,我们可以实现更细粒度的状态更新,支持更复杂的表单联动,同时保持代码的可维护性和可测试性。

博客素材.png

实战演示:Form 表单 Hooks 版本简化实现

使用示例

还是从一个简单的使用示例开始:

import React, { Component } from "react";
import { Form, Button, Input } from "antd";

const FormItem = Form.Item;

const nameRules = { required: true, message: "请输入姓名!" };
const passworRules = { required: true, message: "请输入密码!" };

export default function MyForm(props) {
  const [form] = Form.useForm();

  const onFinish = (val) => {
    console.log("onFinish", val);
  };

  // 表单校验失败执行
  const onFinishFailed = (val) => {
    console.log("onFinishFailed", val);
  };

  useEffect(() => {
    console.log("form", form); 
  }, []);
  
  return (
    <div>
      <h3>AntdFormPage</h3>
      <Form
        ref={formRef}
        onFinish={onFinish}
        onFinishFailed={onFinishFailed}
      >
        <FormItem name="username" label="姓名" rules={[nameRules]}>
          <Input placeholder="username placeholder" />
        </FormItem>
        <FormItem name="password" label="密码" rules={[passworRules]}>
          <Input placeholder="password placeholder" />
        </FormItem>
        <FormItem>
          <Button type="primary" size="large" htmlType="submit">
            Submit
          </Button>
        </FormItem>
      </Form>
    </div>
  );
}

架构设计

在开始编码之前,我们需要先规划好整体架构。从使用示例来看,我们的简化版 Form 需要以下部分:

  1. Form 容器组件
  2. Form.Item 表单项组件
  3. FormStore 状态管理
  4. Context 通信机制
/my-form
├── FormContext.js    - 用于传递表单实例
├── Form.js           - 表单容器组件
├── index.js          - 导出入口文件
├── Item.js           - 表单项组件
└── useForm.js        - 表单状态管理逻辑

入口文件

// index.js
import React from "react";
import _Form from "./Form";
import Item from "./Item";

const Form = React.forwardRef(_Form); //_Form;

Form.Item = Item;

export { Item };
export default Form;

这里 React.forwardRef() 的作用是使 Form 组件能够接收并转发 ref 到其内部元素或组件。

  • 使用 forwardRef 后:可以直接 <Form ref={myRef}>...</Form>,ref 会被正确传递
  • 不使用 forwardRef:<Form ref={myRef}>...</Form> 中的 ref 会被忽略,无法传递到组件内部

在 React 19 中,所有函数组件都默认接收 ref 作为第二个参数。这意味着不再需要显式地使用 forwardRef 来包装组件,组件可以直接访问并使用 ref。

Form.item 表单项组件

第一步,把 Form.Item 包裹的组件变成受控组件。这个过程与旧版 antd 中的 getFieldDecorator HOC 原理类似。实现受控组件的关键技巧在于使用 React.cloneElement '劫持'原始输入框,注入自定义的 value 和 onChange 属性,从而接管组件的数据流向。

import React from "react";

export default function Item(props) {
  const { children } = props;
  const getControlled = () => {
    return {
      value: "初始值",
      onChange: (e) => {
        const newValue = e.target.value;
        console.log("新的值", newValue);
      },
    };
  };

  const returnChildNode = React.cloneElement(children, getControlled());
  return returnChildNode;
}

初始化状态管理库

// useForm.js
class FormStore {
  constructor() {
    this.store = {}; 
  }
  
  getFieldsValue = () => {
    return { ...this.store };
  };

  getFieldValue = (name) => {
    return this.store[name];
  };
  
  setFieldsValue = (newStore) => {
    this.store = {
      ...this.store,
      ...newStore,
    };
  };
  
  getForm = () => {
    return {
      getFieldsValue: this.getFieldsValue,
      getFieldValue: this.getFieldValue,
      setFieldsValue: this.setFieldsValue,
    };
  };
}

那么当我们要使用 Form 的时候,应该在哪创建这个对象的实例呢?为避免表单状态丢失和重置,FormStore 实例应在组件首次渲染时创建一次,在多次渲染和状态更新时复用同一实例,只有在组件完全卸载后重新挂载时才创建新实例。这个场景我们应该想到 useRef

使用 ref 可以确保:

  • 可以在重新渲染之间 存储信息(普通对象存储的值每次渲染都会重置)。
  • 改变它 不会触发重新渲染(状态变量会触发重新渲染)。
  • 对于组件的每个副本而言,这些信息都是本地的(外部变量则是共享的)。 改变 ref 不会触发重新渲染,所以 ref 不适合用于存储期望显示在屏幕上的信息。如有需要,使用 state 代替。
// useForm.js
import { useRef } from "react";

// ...

export default function useForm(form) {
  const formRef = useRef();

  if (!formRef.current) {
    const formStore = new FormStore();
    formRef.current = formStore.getForm();
  }
  return [formRef.current];
}

此时已经可以通过 useForm 钩子创建数据仓库实例了,但通过 useForm 钩子创建的表单数据仓库需要在 Form 组件树的各个层级间共享。由于表单组件(如 Form.Item、Input、Button 等)可能位于不同嵌套层级且需要统一访问表单状态,使用 props 逐层传递显然不够优雅。此时,React 的 Context API 提供了理想解决方案,允许我们在 Form 根组件提供数据仓库实例,并让任意层级的子组件直接访问,实现高效的跨层级状态共享。

表单组件间通信

首先,创建一个 Context 来共享表单实例:

// FormContext.js
import { createContext } from "react";

const FormContext = createContext();

export default FormContext;

接下来,在 Form 组件中,我们使用 Provider 将表单实例提供给子组件树。Form 组件是整个表单的容器,负责创建表单实例并通过 Context 分发给子组件:

// Form.js
import FormContext from "./FormContext";
import useForm from "./useForm";

export default function Form({ children, form }) {
  const [formInstance] = useForm(form);
  return (
    <form>
      <FormContext.Provider value={formInstance}>
        {children}
      </FormContext.Provider>
    </form>
  );
}

最后,在 Item 组件中,我们通过 useContext 访问表单实例,从而实现对输入控件的"劫持",将其转变为受控组件。这里的关键是 getControlled 方法,它为原始输入组件注入了 value 和 onChange 属性:

// Item.js
import React, { useContext } from "react";
import FormContext from "./FormContext";

export default function Item(props) {
  const { children } = props;
  const formInstance = useContext(FormContext);
  const { getFieldValue, setFieldsValue } = formInstance;

  const getControlled = () => {
    return {
      value: getFieldValue(props.name),
      onChange: (e) => {
        const newValue = e.target.value;
        setFieldsValue({ [props.name]: newValue });
        console.log("新的值", newValue);
      },
    };
  };

  const returnChildNode = React.cloneElement(children, getControlled());
  return returnChildNode;
}

不过,需要注意的是,目前的实现还缺少响应式更新机制。虽然我们能够修改表单状态,但当 value 被修改后,它只存在于 React 的数据结构中(即虚拟 DOM),只有当 React 执行渲染过程时,这个修改才会被应用到实际的 DOM 元素上。

即 value 是 input 上的属性,input 作为一个 HTML 元素,不重新渲染 React 组件是无法更新这个属性的。

没有渲染,变化就停留在虚拟 DOM 层面,用户界面上的 input 元素不会更新。要实现真正的响应式表单,我们还需要添加订阅发布机制,让 FormStore 在状态变化时能够通知相关的表单项组件进行更新。

通过发布订阅实现响应式更新

实现订阅

首先,我们需要建立一个中央存储系统来管理表单状态和订阅者:

// useForm.js
class FormStore {
  constructor() {
    this.store = {}; // 存储表单值的仓库
    this.itemEntities = []; // 存储订阅者(表单项)的数组
  }
  
  registerItemEntities = (entity) => {
    this.itemEntities.push(entity); // 添加订阅者

    return () => {
      // 返回取消订阅的函数
      this.itemEntities = this.itemEntities.filter((item) => item !== entity);
      delete this.store[entity.props.name];
    };
  };
  
  getForm = () => {
    return {
      // ...
      registerItemEntities: this.registerItemEntities,
    };
  };
}
  1. 使用数组存储订阅者, itemEntities 数组存储了所有需要响应数据变化的表单项组件。当表单数据发生变化时,我们可以遍历这个数组,通知每个订阅者进行更新。

  2. 为什么返回取消订阅函数? 这是一个经典的订阅模式实现。返回的函数用于组件卸载时清理订阅关系,防止内存泄漏。这种设计让订阅和取消订阅的逻辑保持一致性。

  3. 为什么同时删除store中的数据? 当组件卸载时,对应的表单字段也应该从数据存储中移除,保持数据的一致性和内存的有效利用。

Item 组件中的订阅处理
// Item.js - 函数组件实现
export default function Item(props) {
  const {children, name} = props;
  
  const {
    getFieldValue,
    setFieldsValue,
    registerItemEntities,
  } = React.useContext(FormContext);
  
  // 使用useReducer实现forceUpdate功能
  const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
  
  React.useLayoutEffect(() => {
    // 向FormStore注册此Item组件
    const unregister = registerItemEntities({
      props,
      onStoreChange: forceUpdate, // 注册更新函数
    });
    
    // 组件卸载时通过清理函数取消订阅
    return unregister;
  }, []);
  
  // 组件渲染逻辑...
}

这里为什么不能用 useEffect 呢?这是因为执行时机的差异:

  • useLayoutEffect 会在浏览器执行绘制之前同步调用,确保在用户看到页面之前就完成了订阅
  • useEffect 是异步执行的,会导致页面先渲染一次后再订阅,造成闪烁或初始值不正确的问题
实现发布

发布机制负责在数据变化时通知所有相关的订阅者:

// useForm.js
setFieldsValue = (newStore) => {
  // 1. update store
  this.store = {
    ...this.store,
    ...newStore,
  };
  // 2. update Item
  this.itemEntities.forEach((entity) => {
    Object.keys(newStore).forEach((k) => {
      if (k === entity.props.name) {
        entity.onStoreChange();
      }
    });
  });
};

表单提交

表单提交需要一个统一的入口来处理成功和失败的回调:

// Form.js
import React from "react";
import FormContext from "./FormContext";
import useForm from "./useForm";

export default function Form(
  { children, form, onFinish, onFinishFailed },
  ref
) {
  const [formInstance] = useForm(form);

  formInstance.setCallbacks({
    onFinish,
    onFinishFailed,
  });
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        formInstance.submit();
      }}
    >
      <FormContext.Provider value={formInstance}>
        {children}
      </FormContext.Provider>
    </form>
  );
}

为什么每次渲染都调用setCallbacks? 由于函数组件每次渲染都会重新执行,onFinishonFinishFailed 可能是新的函数引用。通过每次都调用 setCallbacks,确保FormStore中保存的始终是最新的回调函数。

表单校验

提交和校验需要 Form.Item 中的数据,所以也得放在 FormStore 中统一处理。

// useForm.js
validate = () => {
  let err = [];

  this.itemEntities.forEach((entity) => {
    const { name, rules } = entity.props;

    const value = this.getFieldValue(name);
    let rule = rules[0];

    if (rule && rule.required && (value === undefined || value === "")) {
      err.push({ [name]: rule.message, value });
    }
  });

  return err;
};

总结

无论是HOC还是Hooks的实现方式,都蕴含着丰富的设计智慧值得我们深入学习。虽然HOC在现代React开发中逐渐被Hooks所取代,但其装饰器模式的核心思想依然具有重要的借鉴价值。

或许一直使用antd Form这样的成熟组件库并不会遇到什么问题,但当我们亲自动手实现FormStore时,就能更深刻地理解独立状态管理仓库的设计哲学。这种理解是可以举一反三的——我们能够更好地掌握Redux、Zustand等状态管理库的核心原理,也能深刻理解为什么React 18要推出useSyncExternalStore这样的Hook来解决并发渲染场景下的状态同步问题。

技术的每一次演进都是为了解决实际开发中遇到的具体问题。作为开发者,我们不应该仅仅满足于掌握API的使用方法,更重要的是要透过源码看到作者的技术理念和设计范式。代码本身只是这些抽象思想的具象表达,真正有价值的是隐藏在代码背后的设计哲学和解决问题的思维方式。