Ant-design Form组件源码实现研究

1,854 阅读8分钟

介绍

ant-design 表单组件Form的核心实现机制研究

结构图

结构.png

流程图

调用流程.png

知识点列表

如何实现表单数据的变化监听数据的收集 && 修改数据更新UI视图

数据双向绑定的简易实现

codesandbox.io/s/form-jian…

  • 项目开发中,我们一般通过更新state或者context,或者是context包装成的props(redux)的值触发UI的更新,如果是一个静态变量,则无法进入生命周期对UI的后续视图产生影响。
  • 想要修改静态代码后影响UI视图,可以通过代码调用rerender的实现,rerender方法,class组件有forceRender方法,函数组件则可以通过useState或者useReducer实现rerender
  • 通过React.cloneElement可以实现value和onChange逻辑的干预

initialValues如何初始化表单数据并显示

有两种使用方式,一种是在Form组件中传入initialValues,一种是在Field组件中传入initialValue

在Form组件中传入initialValues

在FormStore.setInitialValues将initialValues存储在FormStore.store,只会执行一次

// Set initial value, init store value when first mount
const mountRef = React.useRef(null);
setInitialValues(initialValues, !mountRef.current);
if (!mountRef.current) {
    mountRef.current = true;
}

父组件Form先渲染,子组件Field再渲染,在Field组件中,在getControlled中返回劫持后的value,value通过getValue方法从FormStore.store中获取到,此时获取到的值就已是initialValues初始化过的数据,表单得到正确渲染

const value = this.getValue();
const mergedGetValueProps = getValueProps || ((val: StoreValue) => ({ [valuePropName]: val }));

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const originTriggerFunc: any = childProps[trigger];

const control = {
  ...childProps,
  ...mergedGetValueProps(value),
};
return control

在Field组件中传入initialValue

在Field组件的constructor中执行initEntityValue

  constructor(props: InternalFieldProps) {
    super(props);

    // Register on init
    if (props.fieldContext) {
      const { getInternalHooks }: InternalFormInstance = props.fieldContext;
      const { initEntityValue } = getInternalHooks(HOOK_MARK);
      initEntityValue(this);
    }
  }

Field组件传入的initialValue取出,初始化FormStore。这里检验了prevValue是否存在,如果父组件Form组件有传入initialValues则会先初始化Store,则此时prevValue就已存在值,Field组件的initialValue将不会执行初始化Store

  private initEntityValue = (entity: FieldEntity) => {
    const { initialValue } = entity.props;

    if (initialValue !== undefined) {
      const namePath = entity.getNamePath();
      const prevValue = getValue(this.store, namePath);

      if (prevValue === undefined) {
        this.updateStore(setValue(this.store, namePath, initialValue));
      }
    }
  };

在Form组件setInitialValues时初始化,初始化存储FormStore.initialValues,但是Field上的initialvalue是不会存储到FormStore.initialValues的,在resetFields有其他处理

如何收集通过人机交互表单值发生改变后的值

通过劫持onChange,通过getControlled返回再传递给表单组件。 分两部分逻辑。一是执行originTrigger的逻辑。二是执行校验的逻辑

originTrigger

  • 通过e.target.[valuePropName]获取组件的值,通过getValueFromEvent兼容自定义取值方式
  • 传入normalize对取值做自定义处理
  • dispatch updateValue ,附带namePath,转换后的value
  • updateStore内部包含以下处理逻辑:
  • FormStore.updateStore 将最新的数据存储到FormStore.store
  • FormStore.notifyObservers 通知全部的field更新,如果name匹配,则调用reRender,更新该Field视图
  • FormStore.notifyWatch 通知useWatch监听的函数回调
  • FormStore.triggerDependenciesUpdate 通知全部的field更新,如果匹配dependency则更新该Field视图
  • FormStore.onValuesChange 触发字段值更新时触发回调事件onValuesChange
  • FormStore.onFieldsChange 触发字段更新时触发回调事件onFieldsChange
control[trigger] = (...args: EventArgs) => {
  // Mark as touched
  this.touched = true;
  this.dirty = true;

  this.triggerMetaEvent();

// 获取合并后的数据
  let newValue: StoreValue;
  if (getValueFromEvent) {
    newValue = getValueFromEvent(...args);
  } else {
    newValue = defaultGetValueFromEvent(valuePropName, ...args);
  }

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

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

  if (originTriggerFunc) {
    originTriggerFunc(...args);
  }
};

执行校验

const { rules } = this.props;
if (rules && rules.length) {
  // We dispatch validate to root,
  // since it will update related data with other field with same name
  dispatch({
    type: 'validateField',
    namePath,
    triggerName,
  });
}

如何实现数据的重置

form.resetFields();
  • this.updateStore(setValues({}, this.initialValues)); 使用this.initialValues更新store
  • this.resetWithFieldInitialValue();,获取到Field上的initailValue更新store,如果initialValues上已存在初始化值,以Form的initialValues为准
  • this.notifyObservers(prevStore, null, { type: 'reset' });通知观察者(组件)更新UI,调用Field的onStoreChange中通过resetCount加一更新组件视图
  • this.notifyWatch(); 通知watch cb回调

如何实现的onFinish和onFinishFailed

Form组件最终会被渲染成<form>...</form>,点击type为submit的按钮会触发组件上的onSubmit执行formInstance.submit();

...
// 默认为form
component: Component = 'form',
...
<Component
  {...restProps}
  onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    event.stopPropagation();

   // 执行表单提交
    formInstance.submit();
  }}
>
  {wrapperNode}
</Component>
  • FormStore.validateFields() 执行校验
  • FormStore.onFinish() 执行成功回调
  • FormStore.onFinishFailed() 执行失败回调
private submit = () => {
this.warningUnhooked();

// 执行校验
this.validateFields()
  .then(values => {
    const { onFinish } = this.callbacks;
    if (onFinish) {
      try {
      // 执行onFinish回调
        onFinish(values);
      } catch (err) {
        // Should print error if user `onFinish` callback failed
        console.error(err);
      }
    }
  })
  .catch(e => {
    const { onFinishFailed } = this.callbacks;
    if (onFinishFailed) {
    // 执行onFinishFailed回调
      onFinishFailed(e);
    }
  });
};

如何实现校验

校验的时机。

  • onchange时校验,代码执行validateFields
  • 手动提交表单时代码调用validateFields

核心demo:codesandbox.io/s/form-vali…

dependencies如何实现依赖更新后重新渲染

更新数据后会触发FormStore.triggerDependenciesUpdate

  • FormStore.triggerDependenciesUpdate 触发依赖更新
  • FormStore.notifyObservers 通知观察者更新UI
  • this.getFieldEntities()获取全部Field进行遍历触发其Field.onStoreChange
  • Field.onStoreChange中判断当前Field的dependencies是否依赖当前更新的组件,如果依赖则reRender该Field

如何实现的表单销毁后保存数据

可通过form.getFieldsValue(true)获取全部的表单数据,绕过内部过滤逻辑

测试demo: codesandbox.io/s/ce-shi-fr…

Form组件。在destroyForm的逻辑中,将需要保存值的name保存在了this.prevWithoutPreserves

  private destroyForm = () => {
    const prevWithoutPreserves = new NameMap<boolean>();
    this.getFieldEntities(true).forEach(entity => {
      if (!this.isMergedPreserve(entity.isPreserve())) {
        prevWithoutPreserves.set(entity.getNamePath(), true);
      }
    });

    this.prevWithoutPreserves = prevWithoutPreserves;
  };

Field组件。registerField返回的cancelRegisterFunc中逻辑,如果preserve为true,则不销毁store中的数据。

// un-register field callback
return (isListField?: boolean, preserve?: boolean, subNamePath: InternalNamePath = []) => {
  this.fieldEntities = this.fieldEntities.filter(item => item !== entity);

  // Clean up store value if not preserve
  if (!this.isMergedPreserve(preserve) && (!isListField || subNamePath.length > 1)) {
    const defaultValue = isListField ? undefined : this.getInitialValue(namePath);

    if (
      namePath.length &&
      this.getFieldValue(namePath) !== defaultValue &&
      this.fieldEntities.every(
        field =>
          // Only reset when no namePath exist
          !matchNamePath(field.getNamePath(), namePath),
      )
    ) {
      const prevStore = this.store;
      this.updateStore(setValue(prevStore, namePath, defaultValue, true));

      // Notify that field is unmount
      this.notifyObservers(prevStore, [namePath], { type: 'remove' });

      // Dependencies update
      this.triggerDependenciesUpdate(prevStore, namePath);
    }
  }

  this.notifyWatch([namePath]);
};

在不刷新页面的情况下,后续再次加载Form组件时调用setInitialValues会从原有的store中获取到保存的数据并再次初始化,卸载组件时保存的数据比initialValues的优先级高

  private setInitialValues = (initialValues: Store, init: boolean) => {
    this.initialValues = initialValues || {};
    if (init) {
      let nextStore = setValues({}, initialValues, this.store);

      // We will take consider prev form unmount fields.
      // When the field is not `preserve`, we need fill this with initialValues instead of store.
      // eslint-disable-next-line array-callback-return
      this.prevWithoutPreserves?.map(({ key: namePath }) => {
        nextStore = setValue(nextStore, namePath, getValue(initialValues, namePath));
      });
      this.prevWithoutPreserves = null;

      this.updateStore(nextStore);
    }
  };

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);

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

  return [formRef.current];
}
  • useForm(),传入forceReRender实例化FormStore,并将formStore.getForm()闭包对象存储在formRef上,返回formRef.current
  • formStore.getForm()闭包对象包含了众多FormStore内部方法,通过useForm()获得的ref对象可以很方便的操作FormStore内部方法。
  • formRef.current将传递给Form组件,在Form组件内部调用const [formInstance] = useForm(form);,此时formInstance指向通过useForm()创建的formRef.current,是同一个对象

useWatch的作用

在指定name的value发生变化的时候获取到最新的value

const cancelRegister = registerWatch(store => {
    const newValue = getValue(store, namePathRef.current);
    const nextValueStr = stringify(newValue);

    // Compare stringify in case it's nest object
    if (valueStrRef.current !== nextValueStr) {
      valueStrRef.current = nextValueStr;
      setValue(newValue);
    }
});

// TODO: We can improve this perf in future
const initialValue = getValue(getFieldsValue(), namePathRef.current);
setValue(initialValue);
  • registerWatch() 向FormStore传递callback,存储在FormStore.watchList上
  • notifyWatch方法会从FormStore.watchList取出callback,逐一执行。当updateValue、setFieldsValue、resetFields、setFields等方法中有调用notifyWatch
  • 获取initialValue值,返回给value返回。执行一次。

name传递一个数组有什么作用

支持嵌套的initialValues 在初始化和更新Store时都是可以按照namePath存储到嵌套属性里面

List组件是如何实现的

核心在于Field组件可以接收一个函数,在List组件的实现中也是内嵌了Field组件,内部传入一个函数可以接受Field组件传入的参数(value、onchange还以一起其他的对象和方法)。然后给children传入目标的field的值和操作方法(增加、删除、移动),这样children必须是一个函数,由用户自定义实现数组表单的渲染逻辑

fields们是何时被FormInstance收集到的

Field组件componentDidMount会通过registerField向FormInstance组件传递自己,这样FormInstance就收集到全部的Field,FormInstance每次使用都是FormStore的唯一单例。

  public componentDidMount() {
    const { shouldUpdate, fieldContext } = this.props;

    this.mounted = true;

    // Register on init
    if (fieldContext) {
      const { getInternalHooks }: InternalFormInstance = fieldContext;
      const { registerField } = getInternalHooks(HOOK_MARK);
      this.cancelRegisterFunc = registerField(this);
    }

    // One more render for component in case fields not ready
    if (shouldUpdate === true) {
      this.reRender();
    }
  }

setValues的作用

每次更新Store都指向一个全新的对象,支持嵌套合并,归功于setValues的实现

每层进行属性合并

/**
 * Copy values into store and return a new values object
 * ({ a: 1, b: { c: 2 } }, { a: 4, b: { d: 5 } }) => { a: 4, b: { c: 2, d: 5 } }
 */
function internalSetValues<T>(store: T, values: T): T {
  const newStore: T = (Array.isArray(store) ? [...store] : { ...store }) as T;

  if (!values) {
    return newStore;
  }

  Object.keys(values).forEach(key => {
    const prevValue = newStore[key];
    const value = values[key];

    // If both are object (but target is not array), we use recursion to set deep value
    const recursive = isObject(prevValue) && isObject(value);

    newStore[key] = recursive ? internalSetValues(prevValue, value || {}) : cloneDeep(value); // Clone deep for arrays
  });

  return newStore;
}

深度复制

function cloneDeep(val) {
  if (Array.isArray(val)) {
    return cloneArrayDeep(val);
  } else if (typeof val === 'object' && val !== null) {
    return cloneObjectDeep(val);
  }
  return val;
}

function cloneObjectDeep(val) {
  if (Object.getPrototypeOf(val) === Object.prototype) {
    const res = {};
    for (const key in val) {
      res[key] = cloneDeep(val[key]);
    }
    return res;
  }
  return val;
}

function cloneArrayDeep(val) {
  return val.map(item => cloneDeep(item));
}

export default cloneDeep;

更新机制

总的来说,UI更新有两种场景,一种是修改了自身表单的值需要更新UI,另一种是更新自己的值,然后其他依赖了这个表单的其他表单需要更新UI

  • notifyObservers更新发生值改变的Field本身。人机交互触发某个Field的onchange,distach({type: 'updateValue',...})进入到FormStore的方法updateValue,调用notifyObservers,获取到全部的getFieldEntities,循环调用Field的onStoreChange方法,由于info.type为valueUpdate所以走最后一个switch,只有name匹配上且值发生了改变,更新此Field
  • triggerDependenciesUpdate。更新依赖值发生了变更的Field的Field

render props

Form组件和Field组件都支持render props,render props方式就是children是一个函数,用于接受Form组件和Field组件的组件状态,并通过函数传递给外部使用的方式。

总结

UI组件源码的阅读理解,总结以下技巧:

  • 可从实现功能出发去找核心实现,忽略次要逻辑
  • 好的源码库在变量和方法命名上语义化都比较好,可根据语义和注释猜测功能
  • 比较冗长的实现不便找到核心逻辑的,可在网络上找找相应文章,能提高效率,不必死磕