手写一个 Antd4 Form 吧(中篇):核心逻辑实现

1,369 阅读18分钟

(题图与内容无关,这是我最喜欢的电影之一《爱乐之城》,夹带私货了嘿嘿)

写在前面

大家好,我是早晚会起风。不小心🕊(鸽)了这篇文章,本来是想要上周末更新的,结果由于周末的床吸引力太大了,导致没更出来,在此对读者们表示歉意。经过好几天的良心不安(并没有)之后,我终于搞出来这篇文章了!

在上篇文章 手写一个 Antd4 Form 吧(上篇):源码分析 中,我们对 And4 Form 的核心逻辑进行了逐步分析。这篇文章就是依次实现上篇内容分析的内容。

Q: 为什么还要再动手实现一遍呢?这不是重复造轮子吗?

A: 我们手写源码主要目的是深入了解好的思想,拓展我们的视野,当我们见到的好的代码之后,我们才能写出更好的代码。另外,了解源码的实现逻辑到自己能够实现还是有很大距离的。只有当你自己动手实现的时候,你才会发现,你有很多的细节还没有理解(我就是这样的,这也是我🕊的罪魁祸首)。动手实现的过程就是对你学习到的知识的检验过程。

Tips1:本篇文章实现的代码我也同步到 Github 仓库了,并且按照 commit 逐步提交,让读者可以清晰地看到每个模块都实现了哪些东西。戳这里跳转到 Github 仓库,如果觉得不错,大家可以点个👍

Tips2:在引用代码块的时候,我会在开头使用 //file: 标记出当前修改的文件,方便大家定位。我们实现的源码都存放在 Github 项目仓库的 ./src/rc-field-form

在开始之前,我再次贴一下总结的思维导图,方便大家参考。

Antd4 Form.png

实现状态管理

通过上一篇文章的源码解析,我们已经知道 Form 的状态管理分为三步,

  1. 创建 FormStore 实例维护表单状态
  2. 通过 Context 跨组件传递 store
  3. 子组件消费 Context,关联 store

我们就按这个顺序来,实现状态管理部分的逻辑。

创建 FormStore 实例

FormStore 的实例是通过 useForm 这个自定义 Hook 创建的,所以第一步我们先来实现它。代码如下,

// file: ./src/rc-field-form/useForm.tsx

class FormStore { ... }

export function useForm(formInstance?: FormInstance) {
  // 创建一个 ref 用于保存实例,直到组件被销毁
  const formRef = useRef<FormInstance>();

  // 如果 store 还没被初始化
  if (!formRef.current) {
    // 如果外界传入了实例,直接使用
    if (formInstance) {
      formRef.current = formInstance;
    } else {
      // 否则,创建一个实例
      formRef.current = new FormStore().getForm();
    }
  }

  return [formRef.current];
}

useForm 做的事情很简单,就是通过 new FormStore() 创建一个 form 实例并返回。接着我们来实现 FormStore,

// file: ./src/rc-field-form/useForm.tsx

class FormStore {
  private store: Store = {};

  // 对外暴露 API
  public getForm = (): FormInstance => ({
    getFieldValue: this.getFieldValue,
    getFieldsValue: this.getFieldsValue,
    setFieldValue: this.setFieldValue,
    setFieldsValue: this.setFieldsValue,
  });

  private getFieldValue = (name: NamePath) => {
    return this.store[name];
  };

  private getFieldsValue = () => {
    return { ...this.store };
  };

  private setFieldValue = (name: NamePath, value: any) => {
    this.store[name] = value;
  };

  private setFieldsValue = (newValues: any) => {
    return { ...this.store, ...newValues };
  };
}

这里我们先实现四个基础 API: getFieldValue getFieldsValue setFieldValue setFieldsValue ,之后使用的时候再来完善。

由于我们实现的是 TypeScript 版本的 Form 组件,所以别忘了补充类型定义。目前我们需要的类型定义如下,

// file: ./src/rc-field-form/interface.ts

export type NamePath = string | number;

export type StoreValue = any;

export interface FormInstance {
  getFieldValue: (name: NamePath) => StoreValue;
  getFieldsValue: () => any;
  setFieldValue: (name: NamePath, value: any) => void;
  setFieldsValue: (values: any) => void;
}

到这里,我们已经实现了第一部分——创建表单实例。简单总结一下,实现分为两个部分,

  1. useForm 自定义 Hook 的定义
  2. FormStore 的定义

通过 Context 跨组件传递 store

接着,我们来实现第二步,使用 Context 来传递 form 实例。通过上一篇文章的学习,我们已经知道,Context 是在我们使用 <Form ...>{children}</Form> 这个组件的时候,为子元素 children 包裹 Context,传入之前创建好的 form 实例,形式如 <Context.Provider value={store}>{children}</Context.Provider>

所以,这一小节我们需要实现两个部分,

  1. 创建 Context 对象并调用
  2. 实现 Form 组件

首先我们来创建 Context 对象,叫做 FieldContext 。这一部分比较简单,我就直接贴代码了,

// file: ./src/rc-field-form/FieldContext.tsx

import { createContext } from "react";

const FieldContext = createContext({});

export default FieldContext;

创建 Context 之后,我们来实现 Form 组件,并调用 Context 传入 form 实例。代码如下,

// file: ./src/rc-field-form/Form.tsx

import React from "react";
import FieldContext from "./FieldContext";
import { FormInstance, ValidateErrorEntity } from "./interface";
import { useForm } from "./useForm";

export interface FormProps {
  form?: FormInstance;
  children?: React.ReactNode;
}

export function Form({ form, children, ...restProps }: FormProps) {
  // 1. 创建实例
  const [formInstance] = useForm(form);

  // 2. 使用 Context 将实例包裹 children
  const wrapperNode = (
    <FieldContext.Provider value={formInstance}>
      {children}
    </FieldContext.Provider>
  );

  return (
    <form
      {...restProps}
      onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
        // 阻止 form 默认行为和事件传播
        event.preventDefault();
        event.stopPropagation();

        // 触发提交操作
        formInstance.**submit**();
      }}
    >
      {wrapperNode}
    </form>
  );
}

代码中除了使用 Context 传递实例得到 wrapperNode 外,return 的 <form> 也有处理。这里我们禁止掉了 form 自身的行为(在提交 form 表单时默认会刷新页面)和事件的传播。

当表单提交时,需要触发实例上的 submit 方法。这一部分我们还没实现,之后再补充。

子组件消费 Context

现在我们已经通过 Context 传递了 form 实例,接下来就是去 Field 中消费它。理所当然,我们就要实现 Field 组件。代码如下,

// file: ./src/rc-field-form/Field.tsx

import React from "react";
import FieldContext from "./FieldContext";
import { FormInstance } from "./interface";

interface InternalFieldProps {
  children?: React.ReactElement;
  name: string;
  fieldContext: FormInstance;
}

interface ChildProps {
  [name: string]: any;
}

class Field extends React.Component<InternalFieldProps> {
  public static contextType = FieldContext;

  public getControlled = (props: ChildProps) => {
    const { fieldContext, name } = this.props;
    const { getFieldValue, setFieldValue, dispatch } = fieldContext;
    return {
      value: getFieldValue(name),
      onChange: (e) => {
        // 获取输入值
        const newValue = e.target?.value;

        // 更新 store 中存储的值
        dispatch({
          type: 'updateValue',
          namePath: name,
          value: newValue
        })
      },
    };
  };

  public render() {
    const { children } = this.props;
    // clone 子元素,注入 value 和 onChange 事件 (这里目前只考虑正常情况)
    const returnChild = React.cloneElement(
      children as React.ReactElement,
      this.getControlled((children as React.ReactElement).props)
    );

    return returnChild;
  }
}

function WrapperField({ name, ...restProps }: any) {
  // 获取 fieldContext 并传递下去
  const fieldContext = React.useContext(FieldContext);
  return (
    <Field key={name} name={name} {...restProps} fieldContext={fieldContext} />
  );
}

export default WrapperField;

这里的 WrapperField 的作用就是通过 useContext 这个 hook 拿到 FieldContext 传下来的 form 实例,并传给真正的 Field 组件。

Field 组件做的事情就是通过 React.cloneElement 克隆子组件(比如最常见的 Input 组件)并调用 getControlled 方法为其挂载 valueonChange 方法。

好了,到这里我们已经实现了 Form 的状态管理流程。不过还有一些小问题,我们为子组件挂载的 onChange 中调用了 dispatch 方法,我们现在还没有定义。不要着急,我们接下来就来实现它。

实现 Field 组件的状态更新

这里,我再展示一遍 Field 组件更新的流程,方便大家理清思路,

  1. 输入内容,触发挂载到子组件(例如 Input)上的 onChange 方法,通过 dispatch 告诉 store 值需要更新。
  2. 将新的状态存储到 store 中。
  3. store 通知 Field 组件进行重新渲染。

第一步我们在上一节已经实现了,接下来我们看剩余的两个步骤,

将新的状态存储到 store 中

现在我们来实现 dispatch 方法,我们先看实现代码,

// file: ./src/rc-field-form/useForm.tsx

interface UpdateAction {
  type: "updateValue";
  namePath: NamePath;
  value: StoreValue;
}

interface ValidateAction {
  type: 'validateField';
  namePath: NamePath;
  triggerName: string;
}

export type ReducerAction = UpdateAction | ValidateAction;

...

class FormStore {

    ...

    public getForm = (): FormInstance => ({
        ...
        dispatch: this.dispatch, // 记得对外暴露 dispatch 方法
    });

    // 更新 store
    private updateStore = (nextStore: Store) => {
    this.store = nextStore;
  };

  private dispatch = (action: ReducerAction) => {
    switch (action.type) {
      case "updateValue": {
        const { namePath, value } = action;
        this.updateValue(namePath, value);
        break;
      }
      case "validateField": {
        break;
      }
      default:
    }
  };

  // 更新值
  private updateValue = (name: NamePath, value: StoreValue) => {
    const prevStore = this.store;
    // 1. 更新 store 对象
    this.updateStore({
      ...this.store,
      [name]: value, // 这里要注意 key 值为 [name] 而不是 'name'
    });
    // 2. 触发对应组件的更新
    this.notifyObservers(prevStore, name, {
      type: "valueUpdate",
    });
  };

}

我们已经知道触发 dispatch 的两种方式有 updateValue(值更新)validateField(校验) 。这里我们只把后者列出来,方便我们梳理思路,具体的实现放在第三部分。

更新值没什么好说的,就是替换 [this.store](http://this.store) 对象。在 updateValue 中我们除了实现值的更新外,还需要通知对应组件的更新,这就是下一步我们需要做的事情。

到这里我们已经实现了值的更新,即 dispatch 方法的调用流程。还有一个细节别忘了,更新 FormInstance 这个类的定义。

// file: ./src/rc-field-form/interface.ts

export interface FormInstance {
  ...
  // 增加 dispatch 类型定义
  dispatch: (action: ReducerAction) => void;
}

通知 Field 组件重新渲染

通知 Field 组件,我们首先需要在 store 中注册 Field 组件吧,不然怎么调用组件上的 onStoreChange 方法呢(不生 🐔 怎么下得出 🥚 呢)。好,我们来生🐔…… 错了错了,是注册 Field 组件。

1. FormStore 新增组件注册方法

第一步,我们需要为 FormStore 新增注册组件的部分。这一步很简单,就是创建 fieldEntities 属性存放注册的 Field 组件,并新增 registerField 方法注册组件。

// file: ./src/rc-field-form/useForm.tsx

class FormStore {
    ...
    // 定义变量存放注册的 fields
    private fieldEntities: FieldEntity[] = [];
	
    private registerField = (entity: FieldEntity) => {
        this.fieldEntities.push(entity);
    };

    public getForm = (): FormInstance => ({
        ...
        registerField: this.registerField, // 对外暴露 registerField 方法
    });
}
	

这里定义了一个新的 TS 类型 FieldEntity ,我们补充一下,

// file: ./src/rc-field-form/interface.ts

export type Store = Record<string, StoreValue>;

export interface FieldEntity {
	// 目前只用到 onStoreChange 方法
  onStoreChange: (
    store: Store,
    namePathList: NamePath,
    info: any, // info 的类型我们之后再补充
  ) => void;
}

export interface FormInstance {
  ...
  registerField: (entity: FieldEntity) => void; // 同样补充 registerField 的定义
}

2. 注册 Field 组件

定义好注册方法之后,我们来进行第二步——注册 Field 组件到 store 中。

// file: ./src/rc-field-form/Field.tsx

class Field extends React.Component<InternalFieldProps> {
    public static contextType = FieldContext;
    private mounted = false;

    componentDidMount() {
        this.mounted = true;
        const { fieldContext } = this.props;
        const { registerField } = fieldContext;
        registerField(this);
    }
    ...
}

这一步就很简单了,就是在组件挂在到页面上(componentDidMount)时调用注册方法,把自己传进去。

3. 通知 Field 组件更新

我们做好了前置条件的准备,终于可以进入正题了。这一步我们就要实现 store updateValue 方法中调用的 notifyObservers 方法了。

// file: ./src/rc-field-form/useForm.tsx

...
interface ValueUpdateInfo {
  type: "valueUpdate";
}

export type NotifyInfo = ValueUpdateInfo;

export type ValuedNotifyInfo = NotifyInfo & {
  store: Store;
};

class FormStore {

    ...

    private getFieldEntities = () => {
        return this.fieldEntities;
    }

    private notifyObservers = (
        prevStore: Store,
        name: NamePath,
        info: NotifyInfo
    ) => {
        // info 与 store 合并,传给 onStoreChange 方法
        const mergedInfo: ValuedNotifyInfo = {
          ...info,
          store: this.getFieldsValue(),
        };
        // 遍历每个注册的 Field 组件更新
        this.getFieldEntities().forEach(({ onStoreChange }) => {
          onStoreChange(prevStore, name, mergedInfo);
        });
     };

    ...
}

记得更新一下 onStoreChange 方法的类型定义,

// file: ./src/rc-field-form/interface.ts

export interface FieldEntity {
  onStoreChange: (
    store: Store,
    namePathList: NamePath,
    info: ValuedNotifyInfo, // +
  ) => void;
}

4. 需要更新的 Field 组件重新渲染

到现在,我们实现了从 dispatch 到更新 store 状态到触发组件 onStoreChange 的过程,只差最后一步——组件重渲染。

// file: ./src/rc-field-form/Field.tsx

class Field extends React.Component<InternalFieldProps> {
    ...

    public reRender() {
        if (!this.mounted) return;
        this.forceUpdate();
    }

    public onStoreChange: FieldEntity['onStoreChange'] = (prevStore, namePath, info) => {
        // 确定当前 Field 是否需要更新
        const namePathMatch = namePath && namePath === this.props.name;

        switch (info.type) {
          default:
            if (namePathMatch) {
              this.reRender();
              return;
            }
        }
    }

  ...

}

这里的重点在于 namePathMatch ,由于我们实现的比较简单,这里目前直接通过 namePath === this.props.name 来判断当前 Field 组件是否需要更新。如果需要,就调用 reRender 方法。

至此,我们已经实现了 Form 表单最基本的功能。在进行剩余部分之前,建议读者先写一个例子验证一下。需要做到能够输入并渲染值。我在这里给一个基本的例子以供测试,

// file: ./src/App.tsx

import Form, { useForm, Field } from "./rc-field-form";
import Input from "./rc-field-form/Input";

export default () => {
  const [form] = useForm();

  return (
    <Form
      form={form}
    >
      <Field name="name" rules={[nameRules]}>
        <Input placeholder="Username" />
      </Field>
      <Field name="name1" rules={[nameRules]}>
        <Input placeholder="Username1" />
      </Field>
    </Form>
  );
};

实现表单的校验逻辑

到这里,我们就实现了最基本的表单状态管理和状态更新逻辑啦,我们继续来实现表单的校验逻辑。

触发表单校验

默认情况下,当我们输入表单值的时候就会触发表单校验逻辑。这里为了简单起见,我们直接在 onChange 方法中执行 validateField 类型的 dispatch 触发表单校验。代码如下,

// file: ./src/rc-field-form/Field.tsx

public getControlled = (props: ChildProps) => {
    const { fieldContext, name } = this.props;
    const { getFieldValue, dispatch } = fieldContext;
    return {
      value: getFieldValue(name),
      onChange: (e) => {
        // 获取输入值
        // ...
        // 更新 store 中存储的值
        // ...

        // 进行表单校验
        dispatch({
          type: "validateField",
          namePath: name,
          triggerName: "onChange",
        });
      },
    };
  };

实现表单校验流程(前半部分)

接下来我们来实现校验流程,从这一部分开始稍微有点难理解,还请大家耐心一点。代码如下,

// file: ./src/rc-field-form/useForm.tsx

// FormStore 中新增 validateFields 方法
private validateFields = (name?: NamePath, options?: ValidateOptions) => {
    const promiseList: Promise<FieldError>[] = [];

    this.getFieldEntities().forEach((field) => {
        // 1. 如果该 field 没有定义 rules,直接跳过
        if (!field.props.rules || !field.props.rules.length) {
            return;
        }

        // 2. 如果触发校验的 field 不是当前 field,直接跳过
        if (name !== field.props.name) {
            return;
        }

        // 3. 调用 field 自身的方法进行校验,返回一个 promise
        const promise = field.validateRules();

        // 剩余步骤我们等实现第 3 步的具体过程后再补充
        ...
  });
};

简单梳理一下这里的校验流程就是,对需要校验的 field 进行校验,并将校验结果收集起来,最后统一通知校验失败的 fields 去重新渲染。

这里的重点是第 3 点,field 自身触发的校验, const promise = field.validateRules()

实现 Field 自身的校验逻辑

话不多说,我们先上代码,

// file: ./src/rc-field-form/Field.tsx

import { validateRules } from "./utils/validateUtil";

...

interface ValidateFinishInfo {
  type: 'validateFinish';
}

export type NotifyInfo = ValueUpdateInfo | ValidateFinishInfo;

...

class Field extends React.Component<InternalFieldProps> {
    ...
    private errors: string[] = [];
    private warnings: string[] = [];

	...

    public validateRules = (options?: ValidateOptions): Promise<RuleError[]> => {
        const { getFieldValue } = this.props.fieldContext;
        const currentValue = getFieldValue(this.props.name);

        const rootPromise = Promise.resolve().then(() => {
        if (!this.mounted) {
            return [];
        }
        // 1. 获取 filterRules 这里其实还过滤出与当前校验的 triggerName 一致的规则。
	// 我们这里都是 onChange,所以就不作过滤了。
        let filteredRules = this.getRules();

        // 2. 调用工具函数 validateRules 进行校验,这一部分我们就不实现了,具体的过程在第一篇文章中已经分析过了。
        // 如果忘了,可以再翻回去看看。
        const promise = validateRules(
            [this.props.name],
            currentValue,
            filteredRules,
            options,
            false
        );

        // 3. 处理校验结果
        promise
            .catch((e) => e)
            .then((ruleErrors: RuleError[] = []) => {
            // 将校验结果保存起来
            // Get errors & warnings
            const nextErrors: string[] = [];
            const nextWarnings: string[] = [];
            ruleErrors.forEach(({ rule: { warningOnly }, errors = [] }) => {
                if (warningOnly) {
                  nextWarnings.push(...errors);
                } else {
                  nextErrors.push(...errors);
                }
            });

            this.errors = nextErrors;
            this.warnings = nextWarnings;

            this.reRender();
        });

        // 4. 返回 promise 对象
        // 注意,这里返回的是 promise 对象,而不是 promise.catch(...).then(...) 之后的结果。
        // 也就是说,promise 处理了两次。
        // 第一次是上边这部分代码,将校验结果保存到了 this.errors 和 this.warnings 中,
        // 第二次是返回给 form 实例之后,再次处理。目的是将所有 fields 的校验信息收集起来,统一处理。
        return promise;
    });

    return rootPromise;
  };
}

这部分逻辑有两个部分需要注意,

  1. field 组件自身的校验需要调用工具方法 validateRules ,这一部分由于太多,我这里直接将代码复制过来,放到了 ./src/rc-field-form/utils 文件夹下边。有兴趣的读者可以自行实现。
  2. 代码这里的第 4 步返回的结果是 promise 对象本身,不是 promise.catch(...).then(...) 之后的结果,具体作用我在代码里注释了(怕读者没仔细看,对后面的流程可能会造成一些误解)。

实现表单校验流程(后半部分)

我们说完 field 自身的校验流程之后,再回到 validateFields 方法中看剩余的步骤。

// file: ./src/rc-field-form/useForm.tsx

private validateFields = (name?: NamePath, options?: ValidateOptions) => {
    const promiseList: Promise<FieldError>[] = [];

    this.getFieldEntities().forEach((field) => {
      ...

      // 3. 调用 field 自身的方法进行校验,返回一个 promise
      const promise = field.validateRules();

      // 4. 将 promise 存放到 promiseList 中
      promiseList.push(
        promise
          .then<any, RuleError>(() => {
            return { name, errors: [], warnings: [] };
          })
          .catch((ruleErrors: RuleError[]) => {
            const mergedErrors: string[] = [];
            const mergedWarnings: string[] = [];

            ruleErrors.forEach(({ rule: { warningOnly }, errors }) => {
              if (warningOnly) {
                mergedWarnings.push(...errors);
              } else {
                mergedErrors.push(...errors);
              }
            });

            if (mergedErrors.length) {
              return Promise.reject({
                name,
                errors: mergedErrors,
                warnings: mergedWarnings,
              });
            }

            return {
              name,
              errors: mergedErrors,
              warnings: mergedWarnings,
            };
          })
      );
    });

    // 5. 这一步很关键,allPromiseFinish 返回一个新的 Promise ,等待 promiseList 中的所有 promise 都完成
    const summaryPromise = allPromiseFinish(promiseList);
    // Notify fields with rule that validate has finished and need update
    summaryPromise
      .catch((results) => results)
      .then((results: FieldError[]) => {
        // 获取到校验失败的 fields 的错误信息
        const resultNamePathList: NamePath[] = results.map(({ name }) => name);
        // 6. 通知这些 fields 更新
        this.notifyObservers(this.store, resultNamePathList, {
          type: "validateFinish",
        });
      });
  };

这里第 4 步就是收集 field 校验的结果存放到 promiseList 中。

为什么要这样做呢?比如我们在 onSubmit 提交整个表单的时候会对表单内容全部进行校验,这个时候我们就需要收集校验结果供用户下一步操作。

5 步很重要,通过 allPromiseFinish(promiseList) 做到等待所有 field 的校验完成后,再进行第 6 步。我们来看这个函数做了什么,

// file: ./src/rc-field-form/utils/asyncUtil.ts

import type { FieldError } from '../interface';

export function allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]> {
  // 标记校验结果是否有错误
  let hasError = false;
  let count = promiseList.length;
  const results: FieldError[] = [];

  if (!promiseList.length) {
    return Promise.resolve([]);
  }

  // 这里返回的一个新的 Promise 对象
  return new Promise((resolve, reject) => {
    // 遍历 promisList
    promiseList.forEach((promise, index) => {
      promise
        .catch(e => {
          // 将 hasError 标记为 true
          hasError = true;
          return e;
        })
        .then(result => {
          count -= 1;
          results[index] = result;

          // 当还有 promise 没处理时,继续下一个 promise 的处理
          if (count > 0) {
            return;
          }

          // 如果所有的 promise 都处理完成,并且有错误,则 reject
          if (hasError) {
            reject(results);
          }
          // 否则 resolve
          resolve(results);
        });
    });
  });
}

具体的逻辑我标记在代码中了。为什么要这样做呢?

最基本的,我们肯定是需要等待所有的 promise 都校验完成后再进行下一步处理。但是由于 promise 的校验可能会有异步的逻辑(比如我们调用一个后端接口进行校验)。通过 allPromiseFinish ,我们就能做到等待最后一个异步的 promise 校验完毕后再 resolve/reject 进行后续的处理环节。

最后一步(第 6 步)就是通知 fields 进行更新重渲染。这一步没什么好说的,需要注意的是 resultNamePathList 变量的类型是 NamePath[] ,我们之前对 notifyObserversonStoreChange 的变量定义都是 NamePath ,需要适配改造一下。

逻辑适配改造 & 补充类型定义

// file: ./src/rc-field-form/useForm.tsx

private notifyObservers = (
    prevStore: Store,
    // - 
    // name: NamePath,
    // + 
    name: NamePath[],
    info: NotifyInfo
)

...

private updateValue = (name: NamePath, value: StoreValue) => {
    ...
    // - 
    // this.notifyObservers(prevStore, name, {
    // +
    this.notifyObservers(prevStore, [name], {
      type: "valueUpdate",
    });
  };
// file: ./src/rc-field-form/interface.ts

export interface FieldEntity {
  ...
  onStoreChange: (
    store: Store,
    // - 
    // namePathList: NamePath,
    // + 
    namePathList: NamePath[],
    info: ValuedNotifyInfo,
  ) => void;
  ...
}
// file: ./src/rc-field-form/Field.tsx

public onStoreChange: FieldEntity["onStoreChange"] = (
    prevStore,
    namePath,
    info
  ) => {
    // 确定当前 Field 是否需要更新
    // - 
    // const namePathMatch = namePath && namePath === this.props.name;
    // + 
    const namePathMatch = namePath && namePath.some(name => name === this.props.name);

    ...
  };

最后,我们补充一下 TS 定义。由于这块定义比较多,我就直接 copy 定义了,大家如果感兴趣可以学习一下 Antd4 Form 的类型是怎样定义的。

// file: ./src/rc-field-form/interface.ts

import type { ReactElement } from 'react';

export type InternalNamePath = (string | number)[];
export type NamePath = string | number | InternalNamePath;

...

export type RuleType =
  | 'string'
  | 'number'
  | 'boolean'
  | 'method'
  | 'regexp'
  | 'integer'
  | 'float'
  | 'object'
  | 'enum'
  | 'date'
  | 'url'
  | 'hex'
  | 'email';

type Validator = (
  rule: RuleObject,
  value: StoreValue,
  callback: (error?: string) => void,
) => Promise<void | any> | void;

export interface ValidatorRule {
  warningOnly?: boolean;
  message?: string | ReactElement;
  validator: Validator;
}

interface BaseRule {
  warningOnly?: boolean;
  enum?: StoreValue[];
  len?: number;
  max?: number;
  message?: string | ReactElement;
  min?: number;
  pattern?: RegExp;
  required?: boolean;
  transform?: (value: StoreValue) => StoreValue;
  type?: RuleType;
  whitespace?: boolean;

  /** Customize rule level `validateTrigger`. Must be subset of Field `validateTrigger` */
  validateTrigger?: string | string[];
}

type AggregationRule = BaseRule & Partial<ValidatorRule>;

interface ArrayRule extends Omit<AggregationRule, 'type'> {
  type: 'array';
  defaultField?: RuleObject;
}

export type RuleObject = AggregationRule | ArrayRule;

export interface FieldError {
  name: NamePath;
  errors: string[];
  warnings: string[];
}

export interface RuleError {
  errors: string[];
  rule: RuleObject;
}

好了,到现在我们已经实现了一个 mini 版的表单校验逻辑了。现在我们只剩最后一个问题了。

实现表单的依赖项更新

话不多说,我们直接来实现。在第一节我们已经实现的更新值的逻辑,当我们更新完值后,就要去查看是否有其他 field 依赖这个 field,去更新被依赖的 field 的状态。

值改变时通知 Field 依赖项更新

所以,我们就要通知依赖项的更新,代码如下,

// file: ./src/rc-field-form/useForm.tsx

...

interface DependenciesUpdateInfo {
  type: "dependenciesUpdate";
}

export type NotifyInfo =
  | ValueUpdateInfo
  | ValidateFinishInfo
  | DependenciesUpdateInfo;

...

private updateValue = (name: NamePath, value: StoreValue) => {
    const prevStore = this.store;
    // 1. 更新 store 对象
    ...
    // 2. 触发对应组件的更新
    ...
    // 3. 触发依赖项更新
    this.notifyObservers(prevStore, [name], {
      type: "dependenciesUpdate",
    });
  };

onStoreChange 处理依赖更新

很明显,我们需要稍微对 onStoreChange 方法稍微改造一下,使得它能够判断 dependenciesUpdate 这种情况。代码如下,

// file: ./src/rc-field-form/Field.tsx

interface InternalFieldProps {
  ...
  dependencies?: NamePath[];
}

...

class Field extends React.Component<InternalFieldProps> {
  ...

  public onStoreChange: FieldEntity["onStoreChange"] = (
    prevStore,
    namePath,
    info
  ) => {
		...
    switch (info.type) {
      case "dependenciesUpdate": {
        const { dependencies } = this.props;

        if (dependencies) {
          const dependenciesMatch = namePath.some((name) =>
            dependencies.includes(name)
          );
          if (dependenciesMatch) {
            this.reRender();
            return;
          }
        }
      }
      default:
        ...
    }
  };

  ...
}

这里的处理也很简单,如果发现发生变化(onChange)的 field 与当前 field 的依赖项匹配,即当前 field 依赖这个发生改变的 field 时,就重新渲染当前 field。很简单有木有~

测试是否成功

我们来看上一篇开头就给出的例子,

// file: ./src/App.tsx

import Form, { useForm, Field } from "./rc-field-form";
import Input from "./rc-field-form/Input";

const nameRules = { required: true, message: "请输入姓名!" };
const passwordRules = { required: true, message: "请输入密码!" };

export default () => {
  const [form] = useForm();

  return (
    <Form
      form={form}
      // preserve={false}
      // onFinish={onFinish}
      // onFinishFailed={onFinishFailed}
    >
      <Field name="name" rules={[nameRules]}>
        <Input placeholder="Username" />
      </Field>
      <Field dependencies={["name"]}>
        {() => {
          return form.getFieldValue("name") === "1" ? (
            <Field name="password" rules={[passwordRules]}>
              <Input placeholder="Password" />
            </Field>
          ) : null;
        }}
      </Field>
      <Field dependencies={["password"]}>
        {() => {
          const password = form.getFieldValue("password");
          console.log(">>>", password);
          return password ? (
            <Field name="password2">
              <Input placeholder="Password 2" />
            </Field>
          ) : null;
        }}
      </Field>
      {/* <button onClick={() => form.submit()}>submit</button> */}
    </Form>
  );
};

仔细的读者已经发现了,我们有依赖项的 Field 组件的子元素都是 function ,但是我们现在还没有处理。在测试之前,我们先来补充处理一下这种情况,

// file: ./src/rc-field-form/Field.tsx 

class Field extends React.Component<InternalFieldProps> {
    ...
    public render() {
        const { children } = this.props;

        // 如果 children 的类型是 function 的话,我们直接执行函数
        if (typeof children === "function") {
          return (children as Function)();
        }

    ...
  }
}

万事大吉,我们来测试一下,

测试结果.png

写在最后

恭喜你看到了这里,通过这篇文章的学习,我们已经实现了 And4 Form 的核心逻辑,相信你对它的理解也更深了。希望你从 Form 代码中学到的思想和各种处理方式(比如对 promise 的处理和 Field 的订阅机制)能够触类旁通,写出更好的代码~

当然,我们实现的只是最核心的逻辑。一些配套的功能还没实现,比如说,

  1. 我们现在连表单都提交不了(这耍个锤子哦)
  2. onFinish、onFinishFailed 等钩子也还没实现
  3. Field 之前有链式依赖的情况我们也还没处理

这些我们都留到下篇文章中实现,不过是在核心逻辑的基础上补充完善,最重要的还是我们学习到的代码思想。

最后,如果觉得文章写得还不错,点个 👍 嘿嘿嘿,这是我更文最大的动力!文章不免有错误出现,烦请大家指出~

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿