likes
comments
collection
share

手写一个 Antd4 Form 吧(上篇):源码分析

作者站长头像
站长
· 阅读数 49

2022/7/14 - 中篇已经更新啦 手写一个 Antd4 Form 吧(中篇):核心逻辑实现

写在前面

大家好,我是早晚会起风。这个主题会分为上、中、下三篇(如果没有弃坑),分别是源码分析、源码手写、细节完善。在这个主题下,我会带着大家一起动手实现一个 Form 组件。

Antd4 Form 的核心实现其实是 rc-field-form 这个包。所以,这个主题是 rc-field-form 源码的分析与实现。

(哈哈,rc-field-form 仓库的 Star 数在我点完后,刚好从 699 变成了 700)

话不多说,我们开始把~

如何使用

在阅读源码之前,我们先引入一个简单的例子,一是了解 Form 在使用时长什么样,二是引出我们想要在源码中找到的答案。

import React from "react";
// 这里引入的 rc-field-form 是我手动从 Github 下载放到 demo 的 src 目录下的,不需要关心引用路径
import Form, { Field } from "../components/rc-field-form";
import Input from "../components/rc-field-form/Input";

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

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

  return (
    <Form form={form} preserve={false}>
      <Field name="name">
        <Input placeholder="Username" />
      </Field>
      <Field dependencies={["name"]}>
        {() => {
          return form.getFieldValue("name") === "1" ? (
            <Field name="password">
              <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>
    </Form>
  );
};

使用过 Form 组件的同学肯定很清楚了。首先我们需要使用 Form 组件来新建一个 Form 表单,里面包裹若干个 Field 组件,就是每一个表单项了。使用上看起来很简单,但是我们有几个疑问:

  1. 我们在 Form 表单中输入的值都存放到哪里了?Form 是怎么管理这些状态的?
  2. 当我们在 Input 中输入值时组件状态是怎么触发更新的?
  3. 表单的校验发生在什么时候,有哪些步骤?
  4. dependencies 是怎么关联的表单项的?

本篇文章将围绕这几个问题来展示,带大家一探究竟。

注意:在文中引用源码时,由于篇幅原因以及阅读体验,我只保留了相关源码信息,不关键的部分我会使用 ... 来过滤掉。

在正式开始之前,我先放一张总览图,大家可以结合思维导图阅读全文。

手写一个 Antd4 Form 吧(上篇):源码分析

小节一:Form 是怎样管理状态的?

首先来解答第一个问题, Form 是怎样管理状态的。

我们直接看 Form 组件的源码,

import FieldContext, { HOOK_MARK } from './FieldContext';

const Form: React.ForwardRefRenderFunction<FormInstance, FormProps> = (
  {
    fields,
    form,
    children,
		validateTrigger = 'onChange',
    ...
  }: FormProps,
  ref,
) => {
	...
	const [formInstance] = useForm(form);
	...
	const formContextValue = React.useMemo(
    () => ({
      ...(formInstance as InternalFormInstance),
      validateTrigger,
    }),
    [formInstance, validateTrigger],
  );

  const wrapperNode = (
    <FieldContext.Provider value={formContextValue}>{childrenNode}</FieldContext.Provider>
  );
	...
	return (
    <Component
      ...
    >
      {wrapperNode}
    </Component>
  );
}

从源码中,我们可以看到有两个个关键信息,

  1. 通过 useForm 创建 formInstance 实例。
  2. 引入 FieldContext,将 Form 组件的 children 包裹起来。

不难看出,Form 表单其实是自己维护了一套状态(formInstance)。然后通过 Context 这种方式将状态传递给子组件。Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

使用 Context 的好处在于,当组件层级很深时,我们不用繁琐地每次都把状态传递下去。只要在任意深度的子组件中使用对应的方法就能拿到当前的状态。

接着我们来看这个 formInstance 实例中都有些什么东西,

通过 useForm 创建 form 实例

我们在使用 Form 组件时,通常会使用 useForm 来初始化一个组件实例,就像这样,

const [form] = Form.useForm();

这一步其实就是创建了一个 form 实例,useForm 是一个自定义的 Hook。我们来看这个 Hook 都干了些什么,

function useForm<Values = any>(form?: FormInstance<Values>): [FormInstance<Values>] {
  // 使用 ref 保存创建的 form 实例
  const formRef = React.useRef<FormInstance>();
  // 强制更新——forceUpdate
  const [, forceUpdate] = React.useState({});

  // 如果当前 ref 上还没有实例,则创建
  if (!formRef.current) {
    // 如果外界提供了 form 实例,直接使用
    if (form) {
      formRef.current = form;
    } else {
      // Create a new FormStore if not provided
      // 否则,新建一个 FormStore 实例
      const forceReRender = () => {
        forceUpdate({});
      };

      const formStore: FormStore = new FormStore(forceReRender);

      // 把实例暴露的方法保存到 ref 上
      formRef.current = formStore.getForm();
    }
  }

  return [formRef.current];
}

useForm 做的事情很简单,就是创建一个 FormStore 实例并返回。

这个实例被存储到 formRef.current 上,原因也很简单, useForm 会随着我们的组件的重新渲染被多次调用,这里的创建的实例需要保存起来,直到组件被销毁,而不是每次执行 useForm 时,都重新创建一个实例 。

这里有一个小的细节是,useForm 怎么在函数组件中强制重新渲染。我们知道,在类组件中,我们可以使用 React 提供了 this.forceUpdate 来强制渲染。函数组件默认是没有提供这样的方法的,所以这里通过 useState 这个 Hook 来实现,利用的其实就是当组件的 state 改变时,组件会重新传染这个特点。

注意,useForm 并不是直接返回整个 FormStore 实例,而是 formStore.getForm() 。这样做是为了只暴露需要的 API。

form 实例暴露的方法如下,

public getForm = (): InternalFormInstance => ({
    getFieldValue: this.getFieldValue,
    getFieldsValue: this.getFieldsValue,
    getFieldError: this.getFieldError,
    getFieldWarning: this.getFieldWarning,
    getFieldsError: this.getFieldsError,
    isFieldsTouched: this.isFieldsTouched,
    isFieldTouched: this.isFieldTouched,
    isFieldValidating: this.isFieldValidating,
    isFieldsValidating: this.isFieldsValidating,
    resetFields: this.resetFields,
    setFields: this.setFields,
    setFieldValue: this.setFieldValue,
    setFieldsValue: this.setFieldsValue,
    validateFields: this.validateFields,
    submit: this.submit,
    _init: true,

    getInternalHooks: this.getInternalHooks,
  });

我们常用的如 getFieldsValue setFieldsValue validateFields submit 这些方法都是通过 getForm 暴露的。也就是说这些方法都是在 FormStore 这个类中定义的,我们会在后面两篇文章的源码手写中实现这些 API。

创建 FieldContext

FieldContext 源码的实现非常简单,就是通过 React.createContext 创建了一个 Context 对象,并初始化了一些属性。

import warning from 'rc-util/lib/warning';
import * as React from 'react';
import type { InternalFormInstance } from './interface';

export const HOOK_MARK = 'RC_FORM_INTERNAL_HOOKS';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const warningFunc: any = () => {
  warning(false, 'Can not find FormContext. Please make sure you wrap Field under Form.');
};

const Context = React.createContext<InternalFormInstance>({
  getFieldValue: warningFunc,
  ...

  getInternalHooks: () => {
    warningFunc();

    return {
      dispatch: warningFunc,
      ...
    };
  },
});

export default Context;

Field 组件消费 Context

我们已经知道了 Form 组件使用 Context 把将表单状态传给了子组件。接下来我们看看 Field 组件是如何消费 Context 的。

function WrapperField<Values = any>({ name, ...restProps }: FieldProps<Values>) {
  const fieldContext = React.useContext(FieldContext);

  const namePath = name !== undefined ? getNamePath(name) : undefined;

  let key: string = 'keep';
  if (!restProps.isListField) {
    key = `_${(namePath || []).join('_')}`;
  }

  // Warning if it's a directly list field.
  // We can still support multiple level field preserve.
  if (
    process.env.NODE_ENV !== 'production' &&
    restProps.preserve === false &&
    restProps.isListField &&
    namePath.length <= 1
  ) {
    warning(false, '`preserve` should not apply on Form.List fields.');
  }

  return <Field key={key} name={namePath} {...restProps} fieldContext={fieldContext} />;
}

export default WrapperField;

Field 组件返回的是一个函数组件 WrapperField ,这个组件使用 React.useContext 来获取到 fieldContext,最后返回 Field 组件。组件定义如下,

class Field extends React.Component<InternalFieldProps, FieldState> implements FieldEntity {
  public static contextType = FieldContext;
	...
	public render() {
    const { resetCount } = this.state;
    const { children } = this.props;

    const { child, isFunction } = this.getOnlyChild(children);

    // Not need to `cloneElement` since user can handle this in render function self
    let returnChildNode: React.ReactNode;
    if (isFunction) {
      // 支持 render 方式渲染
      returnChildNode = child;
    } else if (React.isValidElement(child)) {
      **returnChildNode = React.cloneElement(
        child as React.ReactElement,
        this.getControlled((child as React.ReactElement).props),
      );**
    } else {
      warning(!child, '`children` of Field is not validate ReactElement.');
      returnChildNode = child;
    }

    return <React.Fragment key={resetCount}>{returnChildNode}</React.Fragment>;
  }
}

Field 是一个类组件,可以看到组件中使用 React 提供的 static contextType = FieldContext 获取表单状态。

Field 组件返回的是 returnChildNode ,我们重点关注一下这个变量的定义,

returnChildNode = React.cloneElement(
  child as React.ReactElement,
  this.getControlled((child as React.ReactElement).props),
);

这里使用了 React.cloneElement 复制了一份子组件,这个方法的官方定义长这样,

React.cloneElement(
  element,
  [config],
  [...children]
)

Field 组件这里传参是 this.getControlled(child.props) 传参的结果。这个函数做的事情就是返回子组件上需要绑定的属性:值(value)、change 事件(onChange)、校验事件等。

以我们文章开头的例子来说,就是给 Field 下的 Input 注入了 valueonChange 事件。这样,子组件就可以获取到表单状态中的值,并当我们在输入框输入值的时候出发 change 事件来更新状态。

小节二:Input 改变时组件状态是怎么触发更新的

通过上一小节的分析,我们已经从宏观角度知道了 Form 是怎样进行状态管理。这一小节,我们来看看当我们输入内容时,状态到底是怎样更新的。

我们刚才看到,Field 组件对子组件(比如例子中的 Input)进行了复制(React.cloneElement),并默认挂载了 value onChange

1. 触发挂载到 Input 上的 onChange 方法

当我们在输入框输入内容时,就会触发 onChange 方法,定义如下,

// 定义默认的 valuePropName 和 trigger
public static defaultProps = {
  trigger: 'onChange',
  valuePropName: 'value',
};

...

public getControlled = (childProps: ChildProps = {}) => {
    const {
      trigger, // 
      validateTrigger,
      getValueFromEvent,
      normalize,
      valuePropName,
      getValueProps,
      fieldContext,
    } = this.props;

    ...

    const { getInternalHooks, getFieldsValue }: InternalFormInstance = fieldContext;
    const { dispatch } = getInternalHooks(HOOK_MARK);

    ...

    // Add trigger
    // ================ onChange 方法 ================
    control[trigger]** = (...args: EventArgs) => {
      // Mark as touched
      this.touched = true;
      this.dirty = true;

      this.triggerMetaEvent();

      let newValue: StoreValue;
    // 1. 获取 Event 上新的 value
      if (getValueFromEvent) {
        newValue = getValueFromEvent(...args);
      } else {
        newValue = defaultGetValueFromEvent(valuePropName, ...args);
      }

      if (normalize) {
        newValue = normalize(newValue, value, getFieldsValue(true));
      }
			
    //2. 触发 dispatch 将新的值存入 store 中
      dispatch({
        type: 'updateValue',
        **namePath,**
        value: newValue,
      });

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

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

    validateTriggerList.forEach((triggerName: string) => {
      // ...
    });

    ...

    return control;
  };

onChange 方法一共做了两件事情:

  1. 从 Event 中获取新的值
  2. 触发 store 的 dispatch 方法,将新的值存入 store。这里的 dispatch 方法就是从 fieldContext 中获取的,就是通过 useForm 创建出的 form 实例。(dispatch 传入的第二个参数 namePath 很关键,我们一会会用到)

2. 将新的状态存储到 store 中

接着我们来看 dispatch 的定义,

// 触发 store 更新的 dispatch 方法,更新动作有两种:1. updateValue,2. validateField
  private dispatch = (action: ReducerAction) => {
    switch (action.type) {
    // 更新值
      case 'updateValue': {
        const { namePath, value } = action;
				
        **this.updateValue(namePath, value);**
        break;
      }
    // 触发校验
      case 'validateField': {
        ...
      }
      default:
      // Currently we don't have other action. Do nothing.
    }
  };

可以看到,当 action 类型是 updateValue 时, 会调用 this.updateValue 方法。

// 更新 value
  private updateValue = (name: NamePath, value: StoreValue) => {
    const namePath = getNamePath(name);
    const prevStore = this.store;
    // 1. 更新 store 对象
    this.updateStore(setValue(this.store, namePath, value));

    // 2. 触发对应组件更新
    this.**notifyObservers**(prevStore, [namePath], {
      type: 'valueUpdate',
      source: 'internal',
    });
    this.notifyWatch([namePath]);

    // Dependencies update 依赖更新(这里先不用关注,我们会在第 4 小节分析)
    const childrenFields = this.triggerDependenciesUpdate(prevStore, namePath);

    ...
  };

这里分为两个步骤,

  1. 调用 updateStore 方法更新 store 对象。这个步骤很简单,定义如下,

    private updateStore = (nextStore: Store) => {
        this.store = nextStore;
    };
    
  2. 触发对应组件的重新渲染

3. 触发对应组件的重新渲染

我们重点来看 updateValue 的第二个步骤,触发组件重新渲染。 notifyObservers 方法定义如下,

private notifyObservers = (
    prevStore: Store,
    namePathList: InternalNamePath[] | null,
    info: NotifyInfo,
  ) => {
    // 判断是否使用了订阅制,默认为 true
    if (this.subscribable) {
      const mergedInfo: ValuedNotifyInfo = {
        ...info,
        store: this.getFieldsValue(true),
      };
      // 更新相关组件
      this.getFieldEntities().forEach(({ onStoreChange }) => {
        onStoreChange(prevStore, namePathList, mergedInfo);
      });
    } else {
      // 全量更新,这里的 forceRootUpdate 就是 new FormStore(forceReRender) 这里传入的 forceReRender
      this.forceRootUpdate();
    }
  };

这个方法首先会判断 this.subscribable 是否为 true。

如果为 true,这里就会获取并遍历每一个注册组件( fieldEntity) ,触发 Field 组件上的 onStoreChange 方法。

这里你可能会有两个疑问:

  1. Field 组件是什么时候注册到 store 中的?
  2. 这么做不就等于全量更新吗?每一个 Field 组件都触发了 onStoreChange 事件。

我们挨个来回答,

Field 组件时候被注册到 store 的

在 Field 组件的 componentDidMount 中有这样几行代码,

class Field extends React.Component<InternalFieldProps, FieldState> implements FieldEntity {
	public componentDidMount() {
    const { shouldUpdate, fieldContext } = this.props;

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

    ...
  }
}

也就是说,当一个 Field 组件挂载完毕时,会调用 store 上的 registerField 方法 将其自身注册到 store 中,注册的 Field 组件会保存到 store 的 fieldEntities 上。

触发 onStoreChange 事件,组件重渲染

我们接着看 onStoreChange 发生了什么,

public onStoreChange: FieldEntity['onStoreChange'] = (prevStore, namePathList, info) => {
    console.log('onStoreChange: ', prevStore, namePathList, info)
    const { shouldUpdate, dependencies = [], onReset } = this.props;
    const { store } = info;
    const namePath = this.getNamePath();
    const prevValue = this.getValue(prevStore);
    const curValue = this.getValue(store);

    const namePathMatch = namePathList && containsNamePath(namePathList, namePath);

    // `setFieldsValue` is a quick access to update related status
    // 通过 setFieldsValue 调用时,快速更新状态
    if (info.type === 'valueUpdate' && info.source === 'external' && prevValue !== curValue) {
      ...
    }

    switch (info.type) {
      case 'reset':
        ...
      case 'remove': {
        ...
      }

      case 'setField': {
        ...
      }

      case 'dependenciesUpdate': {
        ...
      }

      default:
        // 1. If `namePath` exists in `namePathList`, means it's related value and should update
        //      For example <List name="list"><Field name={['list', 0]}></List>
        //      If `namePathList` is [['list']] (List value update), Field should be updated
        //      If `namePathList` is [['list', 0]] (Field value update), List shouldn't be updated
        // 2.
        //   2.1 If `dependencies` is set, `name` is not set and `shouldUpdate` is not set,
        //       don't use `shouldUpdate`. `dependencies` is view as a shortcut if `shouldUpdate`
        //       is not provided
        //   2.2 If `shouldUpdate` provided, use customize logic to update the field
        //       else to check if value changed
        if (
          namePathMatch ||
          ((!dependencies.length || namePath.length || shouldUpdate) &&
            requireUpdate(shouldUpdate, prevStore, store, prevValue, curValue, info))
        ) {
          this.reRender();
          return;
        }
        break;
    }

    if (shouldUpdate === true) {
      this.reRender();
    }
  };

首先,我们在 onStoreChange 方法开头打印了一行 log。在例子的 Username 输入框中输入一个数字 1 后,log 打印的结果如下,

onStoreChange: {} ['name'] {type: 'valueUpdate', source: 'internal', store: {name: "1"}}

onStoreChange 方法有很多分支,根据我们打印的结果, switch 会走到 default 这条分支,我们现在只关注这条分支。

default 分支首先需要判断一组条件。第一个条件是 namePathMatch ,即 Field 名称路径是否匹配,如果匹配,就执行 this.reRender() 方法重渲染该 Field 组件 。

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

可以看到,组件是否重渲染的关键在于名称是否能够匹配得上。首先需要解释一下这两个参数—— namePathnamePathList

  • namePath:这里可以简单理解为 Field 组件的 props.name,如 <Field name="name"> 的 namePath 就是 ["name"]
  • namePathList:与 namePath 类似,它是 onStoreChange 的第二个参数,就是触发更新的 Field 组件的 namePath。(如果忘了,请回到本小节 1.)
const namePathMatch = namePathList && containsNamePath(namePathList, namePath);

export function containsNamePath(namePathList: InternalNamePath[], namePath: InternalNamePath) {
  return namePathList && namePathList.some(path => matchNamePath(path, namePath));
}

所以,当我们在 <Field name="name"> 这个 Field 的组件下的 Input 输入内容时,也只有该组件的 onStoreChange 方法的 namePathMatchtrue 。这样就确保了只有相关的组件会被重渲染,而不是所有 Field。

到这里,我们已经对 Form 基本的工作流程有了基本的了解。

小节三:校验是在什么时机触发的,有哪些步骤

我们先来回答第一个问题,校验是在什么时机触发的。

校验的触发时机

在第一小节我们提到,Field 组件的 getControlled 方法给 Field 组件的子组件(也就是这里的 Input)绑定了 change 事件、校验事件等。我们之前分析了 onChange 事件。这一小节来看校验事件。老规矩,先上源码,

public getControlled = (childProps: ChildProps = {}) => {
    const {
      trigger,
      validateTrigger,
      // ...
    } = this.props;
    const mergedValidateTrigger =
    validateTrigger !== undefined ? validateTrigger : fieldContext.validateTrigger;

    const namePath = this.getNamePath();
    const { getInternalHooks, getFieldsValue }: InternalFormInstance = fieldContext;
    const { dispatch } = getInternalHooks(HOOK_MARK);
    const mergedGetValueProps = getValueProps || ((val: StoreValue) => ({ [valuePropName]: val }));

    // Add trigger
    control[trigger] = (...args: EventArgs) => {
      ...
    };

    // 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
    // 1. 保留上一步注册的 onChange 事件
    const originTrigger = control[triggerName];
    // 2. 添加校验部分
      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;
  };

首先,我们创建了一个变量 validateTriggerList ,用于保存校验触发的方式,例如常见的 onChange onBlur 等 。默认情况下,validateTrigger 的值也是 onChange,这个值是 Form 组件传入的一个 prop,定义如下,

const Form: React.ForwardRefRenderFunction<FormInstance, FormProps> = (
  {
    ...
    validateTrigger = 'onChange',
    ...
  }: FormProps,
  ref,
) => { ... }

接着我们为每个触发事件绑定校验动作(dispatch action)。这儿有两个关键点:

  1. 保留上一步注册的 control[trigger] 事件,例如 control["onChange"] 。这样做是因为校验(action.type: validateField)和值的更新(action.type: updateValue)都共用一个事件。
  2. 为每个 triggerName 绑定表单校验 action。

所以,在默认情况下,我们在修改表单值的时候就会触发表单的校验事件。

表单校验过程都发生了什么

我们在 小节二 -> 2. 已经分析过 action.type 为 updateValue 时的逻辑了,现在来看另一个分支 validateField

private dispatch = (action: ReducerAction) => {
    switch (action.type) {
      case 'updateValue': {
        ...
      }
      case 'validateField': {
        const { namePath, triggerName } = action;
        this.validateFields([namePath], { triggerName });
        break;
      }
      default:
      // Currently we don't have other action. Do nothing.
    }
  };

类似的,这个分支触发了 validateFields 方法。这个方法内容比较多,大致可以分为 5 个阶段,我们先看源码,

private validateFields: InternalValidateFields = (
    nameList?: NamePath[],
    options?: ValidateOptions,
  ) => {
    this.warningUnhooked();

    const provideNameList = !!nameList;
    const namePathList: InternalNamePath[] | undefined = provideNameList
      ? nameList.map(getNamePath)
      : [];

    // Collect result in promise list
    const promiseList: Promise<FieldError>[] = [];

    this.getFieldEntities(true).forEach((field: FieldEntity) => {
      // Add field if not provide `nameList`
      if (!provideNameList) {
        namePathList.push(field.getNamePath());
      }

      ...

      const fieldNamePath = field.getNamePath();
      // Add field validate rule in to promise list
      // 1. 判断是否需要校验
      if (!provideNameList || containsNamePath(namePathList, fieldNamePath)) {
        // 执行 field 的 Rules 方法进行校验
	const promise = field.validateRules({
          validateMessages: {
            ...defaultValidateMessages,
            ...this.validateMessages,
          },
          ...options,
        });

        // 2. 将校验对应的 Promise 保存在 promiseList 上,方便等会调用
        promiseList.push(
          promise
            .then<any, RuleError>(() => ({ name: fieldNamePath, 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: fieldNamePath,
                  errors: mergedErrors,
                  warnings: mergedWarnings,
                });
              }

              return {
                name: fieldNamePath,
                errors: mergedErrors,
                warnings: mergedWarnings,
              };
            }),
        );
      }
    });
		
    // 3. 创建一个新的 promise,等待校验完毕
    const summaryPromise = allPromiseFinish(promiseList);
    this.lastValidatePromise = summaryPromise;

    // Notify fields with rule that validate has finished and need update
    summaryPromise
      .catch(results => results)
	// 4. 校验完成后,通知 field 更新
      .then((results: FieldError[]) => {
        const resultNamePathList: InternalNamePath[] = results.map(({ name }) => name);
        this.notifyObservers(this.store, resultNamePathList, {
          type: 'validateFinish',
        });
        this.triggerOnFieldsChange(resultNamePathList, results);
      });
		
    // 5. 返回总的校验结果
    const returnPromise: Promise<Store | ValidateErrorEntity | string[]> = summaryPromise
      .then((): Promise<Store | string[]> => {
        if (this.lastValidatePromise === summaryPromise) {
          return Promise.resolve(this.getFieldsValue(namePathList));
        }
        return Promise.reject<string[]>([]);
      })
      .catch((results: { name: InternalNamePath; errors: string[] }[]) => {
        const errorList = results.filter(result => result && result.errors.length);
        return Promise.reject({
          values: this.getFieldsValue(namePathList),
          errorFields: errorList,
          outOfDate: this.lastValidatePromise !== summaryPromise,
        });
      });

    // Do not throw in console
    returnPromise.catch<ValidateErrorEntity>(e => e);

    return returnPromise as Promise<Store>;
  };

根据源码的执行逻辑,这五个阶段可以归纳为,

  1. 遍历 fieldEntities,判断单个 field 是否需要进行校验。
  2. 对于需要校验的 field,执行 field.validateRules 方法。
  3. 将校验返回的 Promise(因为是异步校验) 保存在变量 promiseList 中。
  4. 等待校验都完成后,通知对应的 field 组件更新
  5. 返回校验结果

可以看出,validateFields 方法其实是通知每一个 field 去进行自身的校验,将校验的结果收集起来并返回。所以,这里的重点在于 field 组件本身的校验逻辑,field 本体调用的其实是 validateUtil.ts 文件中的 validateRules 方法,最终引用 async-validator 对每一条规则进行校验(有机会我们再说)。

validateRules 方法其实就是遍历我们在 Field 组件传入的 rules 数组进行校验。校验分为两种模式,分别是 serialization(串行)parallel(并行) 两种。两者的区别我们可以在 antd 文档中找到,

手写一个 Antd4 Form 吧(上篇):源码分析

即,当 validateFirst = true 时,会按顺序校验规则,当前一规则校验完毕且通过时,才会校验下一个规则。如果校验不通过,则直接返回。下面是源码的实现,理解就可以。

export function validateRules(...) {
	...
    if (validateFirst === true) {
    // >>>>> Validate by serialization
    summaryPromise = new Promise(async (resolve, reject) => {
      /* eslint-disable no-await-in-loop */
      for (let i = 0; i < filledRules.length; i += 1) {
        const rule = filledRules[i];
        const errors = await validateRule(name, value, rule, options, messageVariables);
        if (errors.length) {
          reject([{ errors, rule }]);
          return;
        }
      }
      /* eslint-enable */

      resolve([]);
    });
  } else {
    // >>>>> Validate by parallel
    const rulePromises: Promise<RuleError>[] = filledRules.map(rule =>
      validateRule(name, value, rule, options, messageVariables).then(errors => ({ errors, rule })),
    );

    summaryPromise = (
      validateFirst ? finishOnFirstFailed(rulePromises) : finishOnAllFailed(rulePromises)
    ).then((errors: RuleError[]): RuleError[] | Promise<RuleError[]> => {
      // Always change to rejection for Field to catch
      return Promise.reject<RuleError[]>(errors);
    });
  }
}

好了,到现在我们已经知道 Form 在校验时都发生了些什么。只剩下最后一个问题——dependencies 是怎么关联的表单项的。

小节四:dependencies 如何关联表单项

注意:如果你对 dependencies 这个概念还不理解,可以戳这里看官网文档的介绍。

顾名思义,dependencies 是用来处理依赖关系的。当某一个 Field 依赖的字段更新时,该 Field 将自动触发更新与校验。

通常情况下都是值改变时(action.type = valueUpdate)触发依赖项的更新。值改变的过程我们已经在 小节二 分析过来,这里再次引用 小节二 2. 的源码片段。

// 更新 value
  private updateValue = (name: NamePath, value: StoreValue) => {
    const namePath = getNamePath(name);
    const prevStore = this.store;
    // 1. 更新 store 对象
    this.updateStore(setValue(this.store, namePath, value));

    // 2. 触发对应组件更新
    this.notifyObservers(prevStore, [namePath], {
      type: 'valueUpdate',
      source: 'internal',
    });
    this.notifyWatch([namePath]);

		// Dependencies update 依赖更新(这里先不用关注,我们会在第 4 小节分析)
    const childrenFields = this.triggerDependenciesUpdate(prevStore, namePath);

    ...
  };

这次我们来关注最后一行 this.triggerDependenciesUpdate(prevStore, namePath); 。很显然,这里调用的是 triggerDependenciesUpdate 这个方法来通知 Field 进行更新的,我们来看它的定义,

private triggerDependenciesUpdate = (prevStore: Store, namePath: InternalNamePath) => {
    const childrenFields = this.getDependencyChildrenFields(namePath);
    if (childrenFields.length) {
      this.validateFields(childrenFields);
    }

    this.notifyObservers(prevStore, childrenFields, {
      type: 'dependenciesUpdate',
      relatedFields: [namePath, ...childrenFields],
    });

    return childrenFields;
  };

又是熟悉的 notifyObservers,在之前的分析中我们已经知道,这个函数会执行每一个 Field 组件上的 onStoreChange 方法(如果忘了可以翻阅 小节二 3. )。直接看 onStoreChange 方法,

public onStoreChange: FieldEntity['onStoreChange'] = (prevStore, namePathList, info) => {
    const { shouldUpdate, dependencies = [], onReset } = this.props;
    const { store } = info;
    const namePath = this.getNamePath();
    const prevValue = this.getValue(prevStore);
    const curValue = this.getValue(store);

    const namePathMatch = namePathList && containsNamePath(namePathList, namePath);

    // `setFieldsValue` is a quick access to update related status
    // 通过 setFieldsValue 调用时,快速更新状态
    if (info.type === 'valueUpdate' && info.source === 'external' && prevValue !== curValue) {
      ...
    }

    switch (info.type) {
      case 'reset':
        ...
      case 'remove': {
        ...
      }

      case 'setField': {
        ...
      }

      case 'dependenciesUpdate': {
        /**
         * Trigger when marked `dependencies` updated. Related fields will all update
         */
        const dependencyList = dependencies.map(getNamePath);
        // No need for `namePathMath` check and `shouldUpdate` check, since `valueUpdate` will be
        // emitted earlier and they will work there
        // If set it may cause unnecessary twice rerendering
        if (dependencyList.some(dependency => containsNamePath(info.relatedFields, dependency))) {
          this.reRender();
          return;
        }
        break;
      }

      default
        ...
    }
    ...
  };

case dependenciesUpdate 的代码很简单,就是遍历当前 Field 的 dependencies,如果发现有依赖项匹配到了 onStoreChange 传入的依赖,就进行重渲染。依赖更新过程结束。

写在最后

到这里,我们已经给文章开头提出的四个问题依次给出了解答,也完成了这篇文章要做的事情——分析 Form 核心运行流程。由于篇幅原因,文章中还有很多细节没有涉及到,如果你感兴趣,可以直接阅读源码。

希望这篇文章能够给你带来一点收获,如果有文章内容有问题,欢迎及时指正。下一篇我们,我们就模仿 Form 源码来实现自己的 Form 库。

如果觉得文章写得还不错,欢迎点赞 👍

转载自:https://juejin.cn/post/7116390485710602254
评论
请登录