antd form底层实现原理解析

         在以 React 作为技术栈的中后台系统中,Ant Design 组件库的使用频率还是很高的。而 Form 组件,是 Ant Design 中设计复杂且比较常用的一个组件。Antd4及以上的版本对 Form 组件做了很多优化,在性能方面有很大提升。因为 antd Form 底层依赖 rc-field-form,所以本文主要讲的其实是 rc-field-form 的实现原理。

先来看个例子:

<Form name="basic">
  <Form.Item label="Username" name="username">
    <Input />
  </Form.Item>
</Form>

         上面的代码是Antd Form的常规用法,如下图所示Input组件除了被Form、Form.Item组件包裹外,还有两层对外不可见的嵌套,每一层都各司其职。

一、如何创建 Form

const [form] = Form.useForm();

         在项目中,通常通过挂载在 Form 上的 useForm hook 来获取 form。

function useForm(form) {
  const formRef = React.useRef();
  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);

      formRef.current = formStore.getForm();
    }
  }

  return [formRef.current];
}

         useForm的源码比较简单,通过 new FormStore 得到 formStore 实例化对象,然后执行 formStore.getForm 并赋值给 formRef.current,最后 return [formRef.current],也就是返回我们所需要的 form。
         AntdForm 底层最核心的逻辑主要在 FormStore 类中,先看下 FormStore 的部分源码:

class FormStore {
  private store = {};
  private fieldEntities = [];
  // 向外暴露一些私有方法 getFieldsValue、setFieldsValue等
  public getForm = (): InternalFormInstance => ({
    getFieldValue: this.getFieldValue,
    getFieldsValue: this.getFieldsValue,
    resetFields: this.resetFields,
    setFields: this.setFields,
    setFieldValue: this.setFieldValue,
    setFieldsValue: this.setFieldsValue,
    _init: true,
    getInternalHooks: this.getInternalHooks,
  });

  private registerField = (entity) => {
    this.fieldEntities.push(entity);
    return () => {
      // un-register
    }
  }
  private registerWatch = () => { }
  private notifyWatch = () => { }
  // ... ...
}

          Antd 4 相比 Antd 3,在Form的性能上做了很大提升。Antd 3Form 底层依赖 rc-form,rc-form 主要通过 React state 来存储数据,当有一个表单项数据发生变化时,其他表单组件也会重新 render。而 Antd 4Form 底层依赖的 rc-field-form,则是通过 FormStore 类中的私有静态属性 store 和 fieldEntities 来收集数据和字段信息,如果只是改变静态属性的 value,是无法更新视图的。所以在收集数据的同时,通过发布订阅模式 + (forceRender || updateState),实现数据通信和增量更新视图。除了增量更新视图,使用静态属性而不是React state另一个好处,是给静态属性赋值后,可以同步拿到最新值,不用关注React state异步更新可能导致的意外问题。

二、如何收集字段

         在 Field 组件挂载后,registerField(同 FormStore 类中的 registerField)注册,注意这里的入参 this,代表的是 Field 类组件,这里注册整个类组件而不是表单字段,主要是为了在运行时更方便地去获取上下文信息(类组件 props 和类组件内的方法等)。

class Field extends React.Component {
  public componentDidMount() {
    const { fieldContext } = this.props;
    // Register on init
    if (fieldContext) {
      const { getInternalHooks }: InternalFormInstance = fieldContext;
      const { registerField } = getInternalHooks(HOOK_MARK);
      this.cancelRegisterFunc = registerField(this);
    }
  }
  public componentWillUnmount() {
    this.cancelRegister();
  }
}

三、数据如何通信

1、update State

         上文说到,rc-field-form 通过 FormStore 中的静态属性 store 来收集数据,并通过发布订阅模式 + (forceRender || updateState) 来实现数据的通信。此处展开来看一下。

const userName = Form.useWatch('userName', form);

         在 useWatch 内部,通过 registerWatch 来添加订阅,触发时机是当 store 发生变化时。在 callback 函数内部,会比较 value 是否发生变化,如果变化,则会更新 state,进而触发使用useWatch hook 的组件重新 render。

function useWatch(...args) {
  const [value, setValue] = useState<any>();

  const valueStr = useMemo(() => stringify(value), [value]);
  const valueStrRef = useRef(valueStr);
  valueStrRef.current = valueStr;

  const isValidForm = formInstance && formInstance._init;

  useEffect(() => {
    // Skip if not exist form instance
    if (!isValidForm) return;

    const { getInternalHooks } = formInstance;
    const { registerWatch } = getInternalHooks(HOOK_MARK);

    // 添加订阅 
    const cancelRegister = registerWatch((values, allValues) => {
      const newValue = getWatchValue(values, allValues);
      const nextValueStr = stringify(newValue);
      if (valueStrRef.current !== nextValueStr) { // 当监听的字段value发生变化时 update state 
        valueStrRef.current = nextValueStr;
        setValue(newValue);
      }
    });
    return cancelRegister;
  }, [isValidForm]);

  return value;
}
class FormStore {
  private watchList = [];
  private registerWatch = callback => {
    this.watchList.push(callback);
    return () => {
      this.watchList = this.watchList.filter(fn => fn !== callback);
    };
  };
  private notifyWatch = (namePath) => {
    if (this.watchList.length) {
      const values = this.getFieldsValue(); // 不包含被删除字段的值
      const allValues = this.getFieldsValue(true); // 包含被删除字段的值
      this.watchList.forEach(callback => {
        // 批量执行callback
        callback(values, allValues, namePath);
      });
    }
  };
  private notifyObservers = (
    prevStore,
    namePathList,
    info,
  ) => {
    const mergedInfo = {
      ...info,
      store: this.getFieldsValue(true),
    };
    this.getFieldEntities().forEach(({ onStoreChange }) => {
      onStoreChange(prevStore, namePathList, mergedInfo);
    });
  };
  private updateValue = (name, value) => {
    const namePath = getNamePath(name);
    const prevStore = this.store;
    this.updateStore(setValue(this.store, namePath, value));
    this.notifyObservers(prevStore, [namePath], {
      type: 'valueUpdate',
      source: 'internal',
    });
    //  this.store更新时,通知订阅者
    this.notifyWatch([namePath]);
  };
}

2、forceRender

         rc-field-form 在收集字段的时候,registerField(this) 的入参 this 指的是 Field 类组件,所以当 store 改变时,notifyObservers 方法中的 onStoreChange(Field 组件中的 onStoreChange),在 onStoreChange 中,会比较新旧 value 是否一致,如果不一致,则会通过 forceUpdate 强制更新组件。通过 forceUpdate 更新组件的好处是不用再去维护一个 state,在自定义 hook 中,则只能通过更新 state 来实现组件的重新 render。
         看到这里,你可能会疑问,通过forceUpdate更新组件的目的是什么呢?主要是为了向表单组件注入value。因为store只是FormStore的一个静态属性,store的变化不会触发组件的重新渲染,所以需要通过forceUpdate更新Field组件,从而向表单组件注入新的value。我们来看下getControlled方法,getControlled对表单组件的props进行了劫持并返回新的props,然后通过React.cloneElement传递给表单组件。

class Field extends React.Component {
  public reRender() {
    this.forceUpdate();
  }
  // Trigger by store update. Check if need update the component
  public onStoreChange = (prevStore, namePathList, info) => {
    const { store } = info;
    const prevValue = this.getValue(prevStore);
    const curValue = this.getValue(store);

    if (prevValue !== curValue) {
      this.reRender();
    }
  };
  public getControlled = childProps => {
    const {
      trigger = 'onChange',
      validateTrigger,
      getValueFromEvent,
      normalize,
      valuePropName,
      getValueProps,
      fieldContext,
    } = this.props;

    const mergedValidateTrigger = validateTrigger || fieldContext.validateTrigger;

    const namePath = this.getNamePath();
    const { getInternalHooks, getFieldsValue } = fieldContext;
    const { dispatch } = getInternalHooks(HOOK_MARK);
    const value = this.getValue();
    const mergedGetValueProps = getValueProps || (val => ({ [valuePropName]: val }));

    const originTriggerFunc = childProps[trigger];

    const control = {
      ...childProps,
      ...mergedGetValueProps(value), // mergedGetValueProps的返回值包含value属性
    };

    // Add trigger
    control[trigger] = e => {
      let newValue;
      if (getValueFromEvent) {
        newValue = getValueFromEvent(e);
      } else {
        newValue = defaultGetValueFromEvent(valuePropName, e);
      }

      dispatch({
        type: 'updateValue',
        namePath,
        value: newValue,
      });

      if (originTriggerFunc) {
        originTriggerFunc(e);
      }
    };
    return control;
  };

  public render() {
    const { children } = this.props;
    const { child } = this.getOnlyChild(children);
    const returnChildNode = React.cloneElement(
      child as React.ReactElement,
      this.getControlled(child.props),
    );
    return <React.Fragment>{returnChildNode}</React.Fragment>;
  }
}

         下面这段代码,是不是在我们的代码中偶尔会出现,你有没有好奇过,当我们手动往表单组件传入 onChange 时,我们传入的 onChange 可以正常执行,当我们输入内容时,数据也可以被正常收集,怎么做到的呢?
其实是在 getControlled 内部对 onChange 进行了重写,首先通过 originTriggerFunc 暂存我们传入的 onChange,然后在重写的 onChange 内部执行 originTriggerFunc。

const onChange = (e) => {
  // doing something
}

<Form.Item  label="Username" name="username">
  <Input placeholder="Username" onChange={onChange} />
</Form.Item>

四、如何校验数据

         Field 内部会向表单组件注入一个回调方法,在底层会遍历rules完成数据校验,触发时机默认为 onChange,我们可以通过传入 validateTrigger 来控制数据的校验时机。数据校验完成后,会调用父组件注入的 onMetaChange 方法,将 meta 数据传递给父组件(Antd Form.Item)。Antd Form.Item 拿到 meta 数据后,通过 update state,将 errors 内容渲染到页面上。

// meta信息
const meta = {
     touched: this.isFieldTouched(),
     validating: this.prevValidating,
     errors: this.errors,
     warnings: this.warnings,
     name: this.getNamePath(),
     validated: this.validatePromise === null,
   };

         提交表单的时候,如何将数据校验不通过的表单显示到可视区域呢?



         上图中,可以看到input有一个id属性,而id是在Form Item组件中动态生成的,结合我们开头的代码可以知道,id的生成规则:form name + 表单 name。当数据校验不通过时,通过id定位到具体dom元素,然后通过scrollIntoView将表单项滚动到可视区域。

 const scrollToField=(name, options) => {
  const namePath = toArray(name);
  const fieldId = getFieldId(namePath, wrapForm.__INTERNAL__.name);
  const node = fieldId ? document.getElementById(fieldId) : null;

  if (node) {
    scrollIntoView(node, {
      scrollMode: 'if-needed',
      block: 'nearest',
      ...options,
    } );
  }
}

源码地址:

Antdhttps://github.com/ant-design/ant-design
rc-field-formhttps://github.com/react-component/field-form
rc-form(Used by antd3):https://github.com/react-component/form

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容