淺析Ant Design中表單是如何實現的

前言

市面上的React表單組件一大堆,都很好使,最近打算開發一個小的ui庫,表單組件本身寫確定很複雜,因而選擇了用Ant Disign的表單實現,雖然沒本身寫,可是搞清原理仍是重要的react

那麼實際上是一個標題黨了,本文探索的是field-form,衆所周知,Ant Design的大部分組件都是基於react-component實現的,Form組件也是基於rc-field-form實現的,而後再增長了一些功能,若是可以搞懂rc-field-form的邏輯,那麼就能知道Ant Design裏的Form組件是如何運做的git

<Form>
  <Field name="username">
    <Input placeholder="Username" />
  </Field>
  <Field name="password">
    <Input placeholder="Password" />
  </Field>
</Form>
複製代碼

基本邏輯

field-form的實現仍是基於Context,若是Context配合Hooks使用,在方便的同時有一個比較大的問題,就是它會所有刷新,有一個值改變了,即便組件沒有用這個值,也會刷新,field-form是怎麼作的呢?github

form-field本身實現了表單值的存儲、組件的更新,只用了Context來傳遞方法ide

// 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組件的方法了ui

值的改變

在值的改變處理這一塊和Redux很類似,Field組件的值改變時,派發一個dispatchFormStore收到後改變值,通知全部註冊的Field組件去對比值,若是須要做出更新就調用React中的this.forceUpdate()來強制刷新組件this

首先,得把valueonChange傳遞給表單元素組件spa

// 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在傳遞下去code

在渲染的時候將這兩個字段傳遞給受控的表單元素組件component

// 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改變的訂閱事件,這樣應該會好作一點


淺析不了了,頭大,斷斷續續看了一個月,結果今天想總結下,發現都不記得了,只記得一點簡單的邏輯,腦袋疼

果真奔着爲看源碼而看源碼並無太大用,仍是得須要作相同的功能再來看源碼參考實現比較靠譜

相關文章
相關標籤/搜索