深入了解 rc-field-form

8,717 阅读5分钟

写在前面

对于经常使用 react 开发的朋友来说,antd 应该不陌生。开发中经常遇到的表单大多会使用 antd 中的 Form 系列组件完成,而 rc-field-form 又是 antd Form 的重要组成部分,或者说 antd Form 是对 rc-field-form 的简单封装。

本人也是在开发中遇到了极其复杂的表单结构,用寻常方式开发十分蛋疼,因此深入了解了一下 rc-field-form 的工作原理,希望另辟蹊径,结合 rc-field-form 的能力简化开发复杂度。这篇文章分享一下我对 rc-field-form 内部原理的认识。

rc-field-form 有几个比较重要的组成部分,FormStore、Form 组件 和 Field(FormItem) 组件,再利用 React Context 的能力,把这几个部分结合在一起,就组成了表单的基本功能。

FormStore

在说 FormStore 之前先说说 FormInstance。大家都知道在调用 useForm 钩子后会得到一个包含各种 form 操作函数的对象,我们把这个对象称为 Form 实例。

const [form] = Form.useForm();

而 FormStore 是一个类,存储了 Form 表单数据,并定义了各种对数据的操作。当我们调用 useForm 时,内部就会创建一个 FormStore 的实例,并通过 useRef 缓存起来,也就是说在调用 useForm 的组件创建直到销毁这段时间内,无论组件渲染多少次,FormStore 只会实例化一次。

/* useForm 源码 */
function useForm<Values = any>(form?: FormInstance<Values>): [FormInstance<Values>] {
  const formRef = React.useRef<FormInstance>();
  const [, forceUpdate] = React.useState();

  if (!formRef.current) {
    if (form) {
      formRef.current = form;
    } else {
      // Create a new FormStore if not provided
      const forceReRender = () => {
        forceUpdate({});
      };

      const formStore: FormStore = new FormStore(forceReRender); // 实例化 FormStore

      formRef.current = formStore.getForm(); // 获得 Form 实例
    }
  }

  return [formRef.current];
}

最终得到的 Form 实例实际上是 formStore.getForm() 的返回值,这里面只暴露了一些 public 方法,把 FormStore 的其他属性隐藏,这样外部在使用时就比较安全。当然,如果我们传入了一个 form,就会直接返回我们传入的这个 form。

/* FormStore.getForm 源码 */

class FormStore {
  // ... 其他属性和方法
  
  public getForm = (): InternalFormInstance => ({
    getFieldValue: this.getFieldValue,
    getFieldsValue: this.getFieldsValue,
    getFieldError: this.getFieldError,
    getFieldsError: this.getFieldsError,
    isFieldsTouched: this.isFieldsTouched,
    isFieldTouched: this.isFieldTouched,
    isFieldValidating: this.isFieldValidating,
    isFieldsValidating: this.isFieldsValidating,
    resetFields: this.resetFields,
    setFields: this.setFields,
    setFieldsValue: this.setFieldsValue,
    validateFields: this.validateFields,
    submit: this.submit,

    getInternalHooks: this.getInternalHooks,
  });
  
  private getInternalHooks = (key: string): InternalHooks | null => {
    if (key === HOOK_MARK) {
      this.formHooked = true;

      return {
        dispatch: this.dispatch,
        registerField: this.registerField,
        useSubscribe: this.useSubscribe,
        setInitialValues: this.setInitialValues,
        setCallbacks: this.setCallbacks,
        setValidateMessages: this.setValidateMessages,
        getFields: this.getFields,
        setPreserve: this.setPreserve,
      };
    }

    warning(false, '`getInternalHooks` is internal usage. Should not call directly.');
    return null;
  };
}

看到这里相信大家就比较熟悉了,这些方法就是我们日常使用 Form 实例的常见方法,但和 antd 中的 Form 实例稍有不同。

  1. getInternalHooks 在 antd Form 暴露的类型定义中是没有的,这是 rc-field-form 的 Form 组件和 Field 组件中需要用到的方法,antd 在类型中把它隐藏了,但实际上还是存在,只是我们日常开发中不应该去使用这个方法。

  2. scrollToField 方法没有,这个方法是 antd 实现的。

还有其他一些属性和方法

interface Store {
  [name: string]: any;
}

interface Callbacks<Values = any> {
  onValuesChange?: (changedValues: any, values: Values) => void;
  onFieldsChange?: (changedFields: FieldData[], allFields: FieldData[]) => void;
  onFinish?: (values: Values) => void;
  onFinishFailed?: (errorInfo: ValidateErrorEntity<Values>) => void;
}

type InternalNamePath = (string | number)[];

class FormStore {
  private store: Store = {}; // 存放表单中所有的数据
  
  private initialValues: Store = {}; // 表单初始值,resetFields 时使用此值

  private fieldEntities: FieldEntity[] = []; // 保存表单中所有 Field 组件实例
  
  private callbacks: Callbacks = {}; // 回调函数
  
  private setInitialValues = (initialValues: any, init: boolean) => {
  	// 设置初始值
  }
  
  // =========================== Observer ===========================
  private registerField = (entity: FieldEntity) => {
    // 注册 Field 组件实例,触发 fieldEntities 压栈
  }
  
  private dispatch = (action: ReducerAction) => {
    // Field 组件在需要修改表单值或触发校验时,通过 dispatch 一个 action,通知 FormStore 处理
  }
  
  private notifyObservers = (
    prevStore: Store, // 原 Store
    namePathList: InternalNamePath[] | null, // 有变更的表单的路径集合
  ) => {
    // 用于通知 Field 组件 Store 有变化,并传入原来的 Store 和新的 Store
  }
}

相信大家已经对 FormStore 有了一个大致的认识,它掌管着表单的所有状态,是表单的核心枢纽。

Form

Form 主要做下面这几件事

  1. 初始化 Form 实例
  2. 注册回调函数,onFieldsChange、onValuesChange、onFinish
  3. 设置初始值(仅当第一次渲染时会设置 store,其他情况下仅设置 initialValues)
  4. 提供 Provider
  5. 渲染子节点
const Form = ({
  form,
  initialValues
}: FormProps) => {
  // 初始化 Form 实例
  const [formInstance] = useForm(form);
  const {
    useSubscribe,
    setInitialValues,
    setCallbacks,
    setValidateMessages,
    setPreserve,
  } = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK);
  
  // 配置回调函数
  setCallbacks({
    onValuesChange,
    onFieldsChange: (changedFields: FieldData[], ...rest) => {
      formContext.triggerFormChange(name, changedFields);

      if (onFieldsChange) {
        onFieldsChange(changedFields, ...rest);
      }
    },
    onFinish: (values: Store) => {
      formContext.triggerFormFinish(name, values);

      if (onFinish) {
        onFinish(values);
      }
    },
    onFinishFailed,
  });
  
  // 标志位,是否第一次挂载
  const mountRef = React.useRef(null);
  // 设置初始值
  setInitialValues(initialValues, !mountRef.current);
  // 标志位赋值
  if (!mountRef.current) {
    mountRef.current = true;
  }
  
  // 初始化 context value
  const formContextValue = React.useMemo(
    () => ({
      ...(formInstance as InternalFormInstance),
      validateTrigger,
    }),
    [formInstance, validateTrigger],
  );

  // 提供 Provider
  const wrapperNode = (
    <FieldContext.Provider value={formContextValue}>{childrenNode}</FieldContext.Provider>
  );

  return (
    <Component
      {...restProps}
      onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        event.stopPropagation();

        formInstance.submit();
      }}
    >
      {wrapperNode}
    </Component>
  );
}

Field

Field 组件主要有以下几个工作

  1. 通过 React Context 获取 Form 实例
  2. 向 FormStore 注册自身
  3. 向子组件注入 value 和 onChange(value 通过 Form 实例从 FormStore 中获取)
  4. 渲染子组件
class Field extends React.Component {
  public componentDidMount() {
    const { shouldUpdate } = this.props;
    
    // 获取 getInternalHooks 方法
    const { getInternalHooks }: InternalFormInstance = this.context;
    const { registerField } = getInternalHooks(HOOK_MARK);
    
    // 向 FormStore 注册自身
    this.cancelRegisterFunc = registerField(this);
  }
  
  public onStoreChange() {
    // 当 FormStore 里的值变化时(可能是 onChange 触发,也可能是 setFieldsValue 触发)
    // FormStore 会调用此函数(Field 已经把自身注册到 FormStore 中)
    // 在这里 Field 就会根据一些条件决定是否要进行更新
  }
  
  public getControlled() {
    const { getInternalHooks, getFieldsValue }: InternalFormInstance = this.context;
    const { dispatch } = getInternalHooks(HOOK_MARK);
    
  	...
    
    // trigger 默认为 onChange,可以通过 props 更改
    control[trigger] = (...args: EventArgs) => {
      // Mark as touched
      this.touched = true;
      this.dirty = true;

      let newValue: StoreValue;
      if (getValueFromEvent) {
        newValue = getValueFromEvent(...args);
      } else {
        newValue = defaultGetValueFromEvent(valuePropName, ...args);
      }

      if (normalize) {
        newValue = normalize(newValue, value, getFieldsValue(true));
      }

      // 通知 FormStore 有值变更
      dispatch({
        type: 'updateValue',
        namePath,
        value: newValue,
      });

      if (originTriggerFunc) {
        originTriggerFunc(...args);
      }
    };
    
    ...
    
    return control;
  }
  
  public render() {
    const { resetCount } = this.state;
    const { children } = this.props;

    // 判断 children 是什么类型
    const { child, isFunction } = this.getOnlyChild(children);

    let returnChildNode: React.ReactNode;
    if (isFunction) {
      returnChildNode = child;
    } else if (React.isValidElement(child)) {
      // 通过 cloneElement 注入 value 和 onChange
      returnChildNode = React.cloneElement(
        child as React.ReactElement,
        this.getControlled((child as React.ReactElement).props),
      );
    } else {
      warning(!child, '`children` of Field is not validate ReactElement.');
      returnChildNode = child;
    }

    return <React.Fragment key={resetCount}>{returnChildNode}</React.Fragment>;
  }
}

结语

rc-field-form 通过 React Context 把 FormStore、Form、Field 结合在一起,实现了一种类似状态管理库的机制,可以看成 Form 组件内置了一个状态管理,而 Field 组件则 connect 到了此状态管理库。对使用者来说,这一切都是透明的。

上述是 rc-field-form 的基本思想,有很多细节没有详细剖析,比如 FormList 是如何实现的,依赖校验是怎么回事,性能如何保证等等。有机会再写下来与大家探讨。