阅读 627

浅析Ant Design中表单是如何实现的

前言

市面上的React表单组件一大堆,都很好使,最近打算开发一个小的ui库,表单组件自己写肯定很复杂,于是选择了用Ant Disign的表单实现,虽然没自己写,但是搞清原理还是重要的

那么其实是一个标题党了,本文探索的是field-form,众所周知,Ant Design的大部分组件都是基于react-component实现的,Form组件也是基于rc-field-form实现的,然后再增加了一些功能,如果能够搞懂rc-field-form的逻辑,那么就能知道Ant Design里的Form组件是如何运作的

<Form>
  <Field name="username">
    <Input placeholder="Username" />
  </Field>
  <Field name="password">
    <Input placeholder="Password" />
  </Field>
</Form>
复制代码

基本逻辑

field-form的实现还是基于Context,如果Context配合Hooks使用,在方便的同时有一个比较大的问题,就是它会全部刷新,有一个值改变了,即使组件没有用这个值,也会刷新,field-form是怎么做的呢?

form-field自己实现了表单值的存储、组件的更新,只用了Context来传递方法

// useForm.ts

// 创建了处理表单的FormStore实例
const formStore: FormStore = new FormStore(forceReRender);
// 调用getForm()方法获取方法,以免一些内部属性被访问
formRef.current = formStore.getForm();
复制代码
// Form.tsx

// 通过useForm获取formRef.current的值,用context将方法传递下去,每一个Field字段组件都能获取到这些方法
    <FieldContext.Provider value={formContextValue}>{childrenNode}</FieldContext.Provider>
复制代码

注册Field表单元素

FormStore是处理表单的实例,包括整个表单的值Store,以及更新Store的方法,那么FormStore是一个独立于React之外的类,Field组件需要将刷新组件的方法注册进FormStore

// Field.tsx

public componentDidMount() {
    const { shouldUpdate } = this.props;
    // 通过context获取FormStore实例的内部方法
    const { getInternalHooks }: InternalFormInstance = this.context;
    const { registerField } = getInternalHooks(HOOK_MARK);
    // 注册当前Field组件实例到Form
    this.cancelRegisterFunc = registerField(this);
}
复制代码

Field组件是Class而不是Hooks,原因是Hooks得写太多额外的代码了,Class可以轻松的将整个Field组件的注册进FormStore,这样在FormStore里就可以调用Field组件的方法了

值的改变

在值的改变处理这一块和Redux很相似,Field组件的值改变时,派发一个dispatchFormStore收到后改变值,通知所有注册的Field组件去对比值,如果需要作出更新就调用React中的this.forceUpdate()来强制刷新组件

首先,得把valueonChange传递给表单元素组件

// Field.tsx

// 创建onChange函数和`value`值的字段
public getControlled = (childProps: ChildProps = {}) => {
    // 为了方便阅读,删改了部分代码

    // 当前的namePath
    const { getInternalHooks }: InternalFormInstance = this.context;
    const { dispatch } = getInternalHooks(HOOK_MARK);
    const value = this.getValue();

    // children表单元素组件上本来的trigger函数
    // 如果children本来就有onChange,被Field受控后还是会调用的
    const originTriggerFunc: any = childProps.onChange;

    // 这是要传给children的props
    const control = {
        ...childProps,
        value,
    };

    // 更改值的函数,默认为onChange
    control.onChange = (...args: EventArgs) => {
        // 默认取值是event.target.value
        let newValue: StoreValue = defaultGetValueFromEvent(valuePropName, ...args);
        // ...
        
        // 派发dispatch,同时FormStore更新store的值
        dispatch({
            type: 'updateValue',
            namePath,
            value: newValue,
        });

        // 调用children本来的函数
        if (originTriggerFunc) {
            originTriggerFunc(...args);
        }
    };
	// ...
    
    return control;
};

复制代码

通常组件都是onChangevalue这两个受控字段,在getControlled()方法中,创建了这两个字段,在onChange中通知Store改变,并且还会调用组件上原有的onChange,同时将组件原有的props在传递下去

在渲染的时候将这两个字段传递给受控的表单元素组件

// Field.tsx

// 为了方便阅读,删改了部分代码
// 渲染Field组件和受控的表单元素组件
public render() {
  const { children } = this.props;

  let returnChildNode: React.ReactNode = React.cloneElement(
      children as React.ReactElement,
      //child.props为参数,同时将onChange和value传递给组件
      this.getControlled((children as React.ReactElement).props),
    );

  return <React.Fragment>{returnChildNode}</React.Fragment>;
}
复制代码

通知Field组件刷新

FormStore的表单值改变时,还需要通知所有注册上的Field组件更新

// useForm.tsx   

// class FormStore
// Field组件通过调用dispatch来更新值
private dispatch = (action: ReducerAction) => {
  switch (action.type) {
      case 'updateValue': {
          const { namePath, value } = action;
          // 更新值
          this.updateValue(namePath, value);
          break;
      }
      // ...
};
    
// 更新值
private updateValue = (name: NamePath, value: StoreValue) => {
    const namePath = getNamePath(name);
    const prevStore = this.store;
    // 对值进行一个深拷贝
    this.store = setValue(this.store, namePath, value);

    // 通知注册的Field组件更新
    this.notifyObservers(prevStore, [namePath], {
        type: 'valueUpdate',
        source: 'internal',
    });
    // ...
};
    
// 通知观察者,也就是通知所有的Field组件
private notifyObservers = (
    prevStore: Store,
    namePathList: InternalNamePath[] | null,
    info: NotifyInfo,
) => {
    // 当前store的值和NotifyInfo
    const mergedInfo: ValuedNotifyInfo = {
        ...info,
        store: this.getFieldsValue(true),
    };
    // 获取所有的字段实例,调用其中的onStoreChange
    // 字段实例就是Field组件的React实例,注册的时候直接将Field的this注册进来了
    this.getFieldEntities().forEach(({ onStoreChange }) => {
        onStoreChange(prevStore, namePathList, mergedInfo);
    });
};
复制代码

先是Field组件值改变,派发dispatch来改变FormStore的值,dispatch内部调用notifyObservers方法将参数传递通知每一个Field组件

所以最后值改变了应该怎么做还是回到了Field组件

// Field.tsx

public onStoreChange: FieldEntity['onStoreChange'] = (prevStore, namePathList, info) => {
  const { shouldUpdate } = this.props;
  const { store } = info;
  const namePath = this.getNamePath();
  // 当前字段上一次的值
  const prevValue = this.getValue(prevStore);
  // 当前字段的值
  const curValue = this.getValue(store);

  // 当前字段NamePath是否在namePathList中
  const namePathMatch = namePathList && containsNamePath(namePathList, namePath);

  switch (info.type) {
	// ...
        
    default:
      // 如果值有改变就刷新组件
      if (
        namePathMatch ||
        dependencies.some(dependency =>
          containsNamePath(namePathList, getNamePath(dependency)),
        ) ||
        requireUpdate(shouldUpdate, prevStore, store, prevValue, curValue, info)
      ) {
        this.reRender();
        return;
      }
      break;
  }
// ...
};


复制代码

只包括值是如何改变的基本逻辑就是这样,还是蛮绕的

总结

对我来说最大的缺点是没能提供useField,在hooks泛滥的年代,缺少了这种方式,使用的还是render props,多少有点不习惯,可能因为一开始就没这个需求,所以注册Field直接注册了一个class实例,除非暴露store改变的订阅事件,这样应该会好做一点


浅析不了了,头大,断断续续看了一个月,结果今天想总结下,发现都不记得了,只记得一点简单的逻辑,脑壳疼

果然奔着为看源码而看源码并没有太大用,还是得需要做相同的功能再来看源码参考实现比较靠谱