菜鸟读代码记录(一)

阅读别人的代码是工作和学习的重要一环,本系列是我阅读一些项目代码时的心路历程的记录。限于个人能力和经验,文中肯定存在各种各样的不足,请多多指教。

作为系列的第一篇,本文中的被害者是一个名为rc-field-form的 React 组件。有些人可能对这个名字不熟悉,但是大部分的人肯定都有使用过,因为它是大名鼎鼎的 antd 底层所使用的组件。在开始之前,我们先来复习一下它是如何使用的毕竟我也忘记了

是怎么用的

第一步,clone 项目到本地,直奔 docs/examples 目录。里面有很多组件使用的例子。首先看一个最简单的:

// examples/basic.tsx

import React from "react";
import Form, { Field, FormInstance } from "rc-field-form";
import Input from "./components/Input";

const list = new Array(1111).fill(() => null);

interface FormValues {
  username?: string;
  password?: string;
  path1?: {
    path2?: string;
  };
}

export default class Demo extends React.Component {
  formRef: any = React.createRef<FormInstance<FormValues>>();

  onFinish = (values: FormValues) => {
    console.log("Submit:", values);

    setTimeout(() => {
      this.formRef.current.setFieldsValue({ path1: { path2: "2333" } });
    }, 500);
  };

  public render() {
    return (
      <div>
        <h3>State Form ({list.length} inputs)</h3>
        <Form<FormValues> ref={this.formRef} onFinish={this.onFinish}>
          <Field name="username">
            <Input placeholder="Username" />
          </Field>
          <Field name="password">
            <Input placeholder="Password" />
          </Field>
          <Field name="username">
            <Input placeholder="Shadow of Username" />
          </Field>
          <Field name={["path1", "path2"]}>
            <Input placeholder="nest" />
          </Field>
          <Field name={["renderProps"]}>
            {(control) => (
              <div>
                I am render props
                <Input {...control} placeholder="render props" />
              </div>
            )}
          </Field>

          <button type="submit">Submit</button>

          <h4>Show additional field when `username` is `111`</h4>
          <Field<FormValues> dependencies={["username"]}>
            {(control, meta, context) => {
              const { username } = context.getFieldsValue(true);
              console.log("my render!", username);
              return (
                username === "111" && (
                  <Input {...control} placeholder="I am secret!" />
                )
              );
            }}
          </Field>

          {list.map((_, index) => (
            <Field key={index} name={`field_${index}`}>
              <Input placeholder={`field_${index}`} />
            </Field>
          ))}
        </Form>
      </div>
    );
  }
}
复制代码

通过阅读这段代码,我有以下发现:这个库 export 了两个重要组件:FormField。似乎Form创建了一个类型为FormInstance的对象,form 的逻辑处理部分应该都是委托给这个对象完成的。FormField是需要配合使用的,两者并不是孤立的。Field可以从FormformInstance中读表单项的值。但是可以看到,代码中并没有直接通过 Props 将formInstasnce传给Field,因此应该是利用的 React Context 实现的。

我是怎么读的

Form组件

查看 Form 的代码,发现有如下的代码:

const [formInstance] = useForm(form);
复制代码

我的直觉告诉我,useForm是最为关键的代码,我们应该先从useForm.ts的代码开始看。

function useForm<Values = any>(
  form?: FormInstance<Values>
): [FormInstance<Values>] {
  const formRef = React.useRef<FormInstance>();
  const [, forceUpdate] = React.useState({});

  if (!formRef.current) {
    if (form) {
      formRef.current = form;
    } else {
      // Create a new FormStore if not provided
      const forceReRender = () => {
        forceUpdate({});
      };

      const formStore: FormStore = new FormStore(forceReRender);

      formRef.current = formStore.getForm();
    }
  }

  return [formRef.current];
}
复制代码

useForm的类型是(form?: FormInstance<Values>) => [FormInstance<Values>],因此我合理怀疑这个参数的作用是复用已有的formInstance,可能是出于保留 formInstance 的内部状态的考虑或者是保持两个 form 之间的状态同步等。接下来,创建了一个用于保存 formInstance 的 Ref,并且通过if语句保证它只被赋值一次。接下来是我今天第一条会记录到笔记本的小技巧:

// 函数式组件的 forceUpdate 实现
const [, forceUpdate] = React.useState({});

const forceReRender = () => {
  forceUpdate({});
};
复制代码

接下来,我们发现 formInstance 是由通过 new FormStore 创建的 formStore 对象提供的。由于 FormStore 的代码有点长,这里就不贴了。它的 constructor 就只把传入的forceRootUpdate存了一下,没做其他的。

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,
    setFieldsValue: this.setFieldsValue,
    validateFields: this.validateFields,
    submit: this.submit,

    getInternalHooks: this.getInternalHooks,
});
复制代码

原来 getForm 返回的就是由 FormStore 的部分方法构成的对象而已,我猜想这么做是为了防止用户直接访问 formStore 的 private fields 和 methods。

接下来回过头来看 Form.tsx 的代码,

const {
  useSubscribe,
  setInitialValues,
  setCallbacks,
  setValidateMessages,
  setPreserve,
} = (formInstance as InternalFormInstance).getInternalHooks(HOOK_MARK);
复制代码

可以看到,这里将 formInstance 转型成了InternalFormInstanceuseForm中使用 FormInstance 是因为useForm是要 export 出去供用户使用的,而 FormInstance 只有一些 public 的方法。相比 FormInstanceInternalFormInstancevalidateFields 的类型被替换成内部版本,还增加了几个方法。接下来,通过 getInternalHooks拿到了一些内部的方法。关于getInternalHooks还有一段注释:

/**
  * Form component should register some content into store.
  * We pass the `HOOK_MARK` as key to avoid user call the function.
  */
复制代码

看来,作者为了防止用户调用这个方法真的是费尽了心思。继续:

React.useEffect(() => {
  formContext.registerForm(name, formInstance);
  return () => {
    formContext.unregisterForm(name);
  };
}, [formContext, formInstance, name]);
复制代码

其中,formContext是之前通过 React.useContext(FormContext)创建的。我发现 index.tsx 中确实 export 了 FormProvider组件,只是我们刚才看到的 basic 例子中没有使用到,想必应该是比较 advanced 的 feature。那就意味着,我们使用的 FormContext 的默认值。不过这个默认值的方法都是 noop,没有任何操作。接下来,涉及到 formContext 的代码,我们就当作没看到就好了。

setValidateMessages({
  ...formContext.validateMessages,
  ...validateMessages,
});
复制代码

validateMessages 是从 props 中来的,查看文档:

Prop Description Type Default
validateMessages Set validate message template ValidateMessages

可以得出结论,这个是用来自定义 validate 的提示信息的,对于国际化应该也很有用。继续:

setCallbacks({
  onValuesChange,
  onFieldsChange: (changedFields: FieldData[], ...rest) => {
    formContext.triggerFormChange(name, changedFields);

    if (onFieldsChange) {
      onFieldsChange(changedFields, ...rest);
    }
  },
  onFinish: (values: Store) => {
    formContext.triggerFormFinish(name, values);

    if (onFinish) {
      onFinish(values);
    }
  },
  onFinishFailed,
});
复制代码

这一段代码基本上就是让 props 中的 callbacks 和 formStore 挂上钩,让 formStore 可以在合适的时机触发传入的 callbacks。

setPreserve(preserve);
复制代码
Prop Description Type Default
preserve Preserve value when field removed boolean false

这个就是用来配置 field 被卸载的时候,绑定的值要不要保留。接下来,又到了记笔记的时间:

// Set initial value, init store value when first mount
const mountRef = React.useRef(null);
setInitialValues(initialValues, !mountRef.current);
if (!mountRef.current) {
  mountRef.current = true;
}
复制代码

这段代码的作用,这段注释已经解释的非常清楚了,其实就有点类似生命周期函数componentDidMount的作用。

// Prepare children by `children` type
let childrenNode = children;
const childrenRenderProps = typeof children === "function";
if (childrenRenderProps) {
  const values = formInstance.getFieldsValue(true);
  childrenNode = (children as RenderProps)(values, formInstance);
}

// Not use subscribe when using render props
useSubscribe(!childrenRenderProps);
复制代码

准备 children。如果 children 被作为 render prop 使用,将所有的表单值以及 formInstance 传入。另外,不要被useSubscribe这个名字骗了,它只是用于配置 formStore 的一个方法。作者希望不使用 render prop 的时候,才把 formStore 的 subscribable 设置为true。至于,subscribable具体是控制 formState 的什么行为的,我们留到后面再说。接下来,我又跳过了一段,关于配合 Redux 使用的代码,来到了:

const formContextValue = React.useMemo(
  () => ({
    ...(formInstance as InternalFormInstance),
    validateTrigger,
  }),
  [formInstance, validateTrigger]
);

const wrapperNode = (
  <FieldContext.Provider value={formContextValue}>
    {childrenNode}
  </FieldContext.Provider>
);
复制代码

可以看到,这里偷偷的用 FieldContext.Provider 将 children 包了起来,很明显是想给 Field 组件中取得 formInstance 用的。这也证实了我们之前关于 React Context 的猜想。悄悄说一句,个人认为 React hooks 的设计有点被 Vue 3 的 Composition API 比下去了。因为 Hooks 在使用的时候,要仔细考虑 deps。对于一些刚开始使用 hooks 的开发者来说,感觉挺容易出错的。

总结一下,Form组件所做的就是创建和配置 formStore,以及注入 FieldContext

Field组件

Field.tsx 的 default export 是 WrapperField 组件。这个组件中,通过React.useContext(FieldContext)拿到了 fieldContext,然后将它传给了Field组件。Field是 class 组件,代码有点长,我们尽量挑重点看。

首先是constructor。在constructor中,调用了formStoreinitEntityValue方法,并且把this传入。在initEntityValue中,拿到传给Field组件的 initialValues, 如果 formStore 中的对应项还没有初始化,则使用这个initialValues进行初始化;否则,什么都不做。这里有个小细节,因为是在constructor里调用的initEntityValue,所以最后即使这个组件并没有被挂载,通过 formStore 还是可以取到这个 Field 对应 key 的值的。

接下来,我决定先看renderrender一上来就是可能需要做笔记的地方。在写组件的时候,对于children的处理会比较让人头疼。children可以接受ReactNode类型的值,可是一看ReactNode的定义:

type ReactNode =
  | ReactChild
  | ReactFragment
  | ReactPortal
  | boolean
  | null
  | undefined;

type ReactText = string | number;
type ReactChild = ReactElement | ReactText;

interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
复制代码

ReactNode的可能类型也太丰富多彩了。然而,有时候我们希望对用户的 children 选择进行限制。比如在这里,作者就希望用户只能传入一个 react element。但是按理说,传入只包含一个 react element 的数组或者 Fragment 应该也是正确的。我们来看看,作者是怎么处理这种情况的:

const childList = toChildrenArray(children);
if (childList.length !== 1 || !React.isValidElement(childList[0])) {
  // ...
} else {
  // ...
}
复制代码

这里的toChildrenArray是来自rc-util包的一个 helper 函数。它的作用是将复杂的 children 规范化为一个 flat 数组:

function toArray(children: React.ReactNode): React.ReactElement[] {
  let ret: React.ReactElement[] = [];

  React.Children.forEach(children, (child: any) => {
    if (child === undefined || child === null) {
      return;
    }

    if (Array.isArray(child)) {
      ret = ret.concat(toArray(child));
    } else if (isFragment(child) && child.props) {
      ret = ret.concat(toArray(child.props.children));
    } else {
      ret.push(child);
    }
  });

  return ret;
}
复制代码

在拿到数组之后,取数组的第一个元素,作为唯一认可的 child。最后,克隆这个 child,并且将control传入。这也就是以下代码中,

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

Input 组件可以拿到 formStore 中的值,并且事件发生时可以通知 formStore 的原因。当然,从下面这个例子,可以看到Field的 children 支持作为 render prop 使用,并且Field会将controlmeta信息直接传入(此时不需要克隆返回的 element)。

<Field name={["renderProps"]}>
  {(control) => (
    <div>
      I am render props
      <Input {...control} placeholder="render props" />
    </div>
  )}
</Field>
复制代码

虽然不知道control是什么,但是大概可以猜到它是类似这样的结构:

type Control<T> = {
  value: T;
  onChange: (x: T) => void;
};
复制代码

那接下来,我们来看看control到底是啥。control是由getControlled返回的,包含的重要代码有:

const value = this.getValue();
const getValueProps = (val: StoreValue) => ({ [valuePropName]: val });

const control = {
  ...mergedGetValueProps(value),
};

// Add trigger
// trigger 指的是收集 value 的时机
control[trigger] = (...args: EventArgs) => {
  // Mark as touched
  this.touched = true;
  this.dirty = true; // dirty 和 touched 的区别是, dirty 在 validate 的时候,也会被置为 true

  this.triggerMetaEvent(); // 触发 meta 更新事件

  let newValue: StoreValue = getValueFromEvent(...args); // 获得新的 value

  dispatch({
    type: "updateValue",
    namePath,
    value: newValue,
  }); // 更新 formStore
};

// 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) {
      // trigger 时机 同时也是 validteTrigger 时机的处理
      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,
      });
    }
  };
});
复制代码

我尽可能的多加了一些注释。为什么不一行一行解释下来了?因为我有点累了。 总之,control 确实是包含了表单值和一些事件处理的函数。而 formStore 有两个我们需要关注的事件 updateValue 和 validateField。

至于 Meta 的话,就是一些关于Field当前状态的信息。不过值得注意的一点是这些信息并不是放在Field的 state 中的,而是作为 class 的 property 存在的。那么问题就来了,我们知道利用setState改变 state 会触发组件的更新,而对普通 property 的重新赋值并不会触发组件的更新。再联系上文,表单值的收集和处理实际上是交由 formStore 处理的。因此合理怀疑Field组件的更新也是由外部的 formStore 来触发的。等后面读 formState 的 updateValue 和 validateField 事件相关处理代码的时候,可以留心一下是不是这样。

之后,在componentDidMount中,通过调用 formStore 的 registerField将这个组件实例注册到了 formStore 上。

this.fieldEntities.push(entity);

// Set initial values
if (entity.props.initialValue !== undefined) {
  const prevStore = this.store;
  this.resetWithFieldInitialValue({ entities: [entity], skipExist: true });
  this.notifyObservers(prevStore, [entity.getNamePath()], {
    type: "valueUpdate",
    source: "internal",
  });
}
复制代码

registerFieldField实例 push 到 fieldEntities 中。如果设置了initialValue的话,将 formStore 中的对应值重置为初始值,然后调用notifyObservers,通知Field实例有值发生了更新。resetWithFieldInitialValue的代码有点长,这里也就不贴了。总的来说,它的功能就是重置部分或者所有的 fields 为初始值。不过里面也有一些注意点:

  1. 如果 Form 已经为某个 key 指定了对应的初始值,那么 name 为该 key 的 Field 将不能再次指定初始值。
  2. 如果多个 Field 对应同一个 key, 那么只能有一个指定初始值。

这样描述可能有点难以理解,但是只要记住初始值的指定不能冲突就行了。而且在registerField中调用resetWithFieldInitialValue的时候,skipExist传的是true,也就是说如果 formStore 中对应 key 已经有值时,就不需要重置了。但是其他地方主动调用这个函数的时候,skipExist一般都是传false,否则就没有意义了。

还记得在Field的 constructor 中,已经对值进行了设置吗?除非在 constructor 调用之后,componentDidMount触发之前,对应值又再次被置为空,否则当registerField函数被调用的时候, formStore 中 key 都对应有值的,也就不会再去重置。

接下来的notifyObservers应该就是重点了。

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();
  }
};
复制代码

subscribable我们之前已经见过面了。当 Form 的 children 被作为 render prop 时,subscribable被置false,通过让Form刷新的方式,为作为 children 的函数传入最新的状态,接下来如何更新就不需要 formStore 操心了;否则,就调用指定的 fields 的onStoreChange方法。这onStoreChange中, Field会决定该如何更新自己的状态以及是否需要重新渲染等。之前,我关于Field如何更新的疑惑也解开了。

最后,我们来看一下 formStore 对于 updateValue 和 validateField 事件的处理。updateValue方法负责处理 updateValue 事件,工作内容是更新 store, 通知对应的 fields。找是否有依赖这个 key 的值的 fields, 如果有,触发对这些受影响的 fields 的 validate 流程,再通过notifyObservers通知它们依赖已经改变。同样的,这些 fields 会在onStoreChange中决定是否需要更新状态和重新渲染等。最后,再触发一些回调函数。

validateFields方法负责处理 validateFields 事件。该方法先找到目标 fields, 然后调用它们的validateRules方法,收集返回值,组成一个 promise 数组。等待所有 promise 都finish之后,通知受影响的 fields,再触发之前注册的相关回调函数。还有一点需要注意,由于这个过程是异步的,在 promise 进行finished 状态的时候,可能已经又触发了一次 validate 事件,因此作者对这种情况也进行了处理,在返回错误信息的同时,还有错误是否已经过时的信息。validateRules调用的是utils/validteUtils.ts 中的 validateRules,最终利用的是 async-validator 完成验证工作。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享