antd表单的值控制和渲染性能控制

486 阅读3分钟

以antd5.0为例, antd表单的实现主要关注"rc-field-form"

先看代码(主要看Form、useForm、Field)

初始化表单组件,实例化表单构造函数, 为Field组件提供注册方法以及初始化方法等

// src/Form.tsx(仓库简化代码示例)
// 创建context
const formContext: FormContextProps = React.useContext(FormContext);
// 通过useForm中的FormStore构造函数构造表单实例
const [formInstance] = useForm(form);

// useForm.tsx (https://github.com/react-component/field-form/blob/master/src/useForm.ts)
function useForm() {
  const formRef = React.useRef<FormInstance>();
      const formStore: FormStore = new FormStore(forceReRender);
      formRef.current = formStore.getForm();
    }
  }
  return [formRef.current];
}

字段Field的注册

//src/Field.tsx
 // rende方法
  public render() {
   returnChildNode = React.cloneElement(
        child as React.ReactElement,
        // 重新组合组件props(注入
        this.getControlled((child as React.ReactElement).props),
      );

    return <React.Fragment key={resetCount}>{returnChildNode}</React.Fragment>;
  }
}
// getControlled  处理表单项组件props
public getControlled = (childProps: ChildProps = {}) => {
    // 通过FieldContext获取表单实例的dispatch方法
    const { getInternalHooks, getFieldsValue }: InternalFormInstance = fieldContext;
    const { dispatch } = getInternalHooks(HOOK_MARK);
    
    const control = {
      ...childProps,
      ...mergedGetValueProps(value),
    };

    // Add trigger
    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({
        type: 'updateValue',
        namePath,
        value: newValue,
      });

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

    // Add validateTrigger
    const validateTriggerList: string[] = toArray(mergedValidateTrigger || []);

    validateTriggerList.forEach((triggerName: string) => {
      // Wrap additional function of component, so that we can get latest value from store
      const originTrigger = control[triggerName];
      control[triggerName] = (...args: EventArgs) => {
        if (originTrigger) {
          originTrigger(...args);
        }

        // Always use latest rules
        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,
          });
        }
      };
    });

    return control;
  };

store状态广播:

当调用setFieldsValue时、validateFields时等会进行状态广播来通知对应的Field,然后由Filed决定是否要更新(见下文)

getControlled通过FieldContext获取表单实例的dispatch方法来更新表单值, 具体updateValue方法实现在useForm中,如下所示,通过调用notifyObservers方法获取每个Field实例,这个实例是在componentDidMount生命周期内调用form实例的registerField进行注册的

// Field.tsx
 public componentDidMount() {
    // Register on init
    if (fieldContext) {
      const { getInternalHooks }: InternalFormInstance = fieldContext;
      const { registerField } = getInternalHooks(HOOK_MARK);
      // 通过registerField注册
      this.cancelRegisterFunc = registerField(this);
    }
  }

通过notifyObservers方法进行广播当前state
// useForm.tsx
 private notifyObservers = (
    prevStore: Store,
    namePathList: InternalNamePath[] | null,
    info: NotifyInfo,
  ) => {
    if (this.subscribable) {
      const mergedInfo: ValuedNotifyInfo = {
        ...info,
        store: this.getFieldsValue(true),
      };
      this.getFieldEntities().forEach(({ onStoreChange }) => {
        onStoreChange(prevStore, namePathList, mergedInfo);
      });
    } else {
      this.forceRootUpdate();
    }
  };

Field组件的更新逻辑判定

上述是通过调用所有注册的Flied实例进行广播的,组件的渲染不能没有更新的也要去刷新,所以需要有一层控制,这层控制是在Fild组件层统一做了处理,通过组件的namePathMatch路径匹配进行判断实现精准更新Field状态

// src/Field.tsx

 // 被上述 useForm.tsx 里的 notifyObservers 方法所调用,并检查是否需要更新,检查条件有fied name、等等
    public onStoreChange: FieldEntity['onStoreChange'] = (prevStore, namePathList, info) => {
      if (
          namePathMatch ||
          ((!dependencies.length || namePath.length || shouldUpdate) &&
            requireUpdate(shouldUpdate, prevStore, store, prevValue, curValue, info))
        ) {
          this.reRender();
          return;
        }
    }
    
  // 通过notifyObservers广播给每个Field组件,由对应Field通过namtPath、依赖等判断决定是否要重新渲染(forceUpdate)
  private updateValue = (name: NamePath, value: StoreValue) => {
    this.notifyObservers(prevStore, [namePath], {
      type: 'valueUpdate',
      source: 'internal',
    });
  };

小结

从以上的一个简要代码逻辑看整个表单的设计是通过手动的forceupdate来进行手动控制决定是否要渲染,这个判定是通过值的更新(路径判定)、依赖更新、depencencies脏检查、以及shouupdate脏检查来判断是否要更新,目前用下来是没有发现有额外的更新开销。

以上文章是之前用ant3时候表单性能过于卡顿,特别是涉及到一些中后台的复杂表单,转用了formily,后来antd4解决了不能精确渲染的卡顿问题,之前fomily的上提了个discuss讨论精确渲染问题,有兴趣可以看下作者说的跨表单联动的性能问题~