阅读别人的代码是工作和学习的重要一环,本系列是我阅读一些项目代码时的心路历程的记录。限于个人能力和经验,文中肯定存在各种各样的不足,请多多指教。
作为系列的第一篇,本文中的被害者是一个名为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 了两个重要组件:Form
和Field
。似乎Form
创建了一个类型为FormInstance
的对象,form 的逻辑处理部分应该都是委托给这个对象完成的。Form
和Field
是需要配合使用的,两者并不是孤立的。Field
可以从Form
的formInstance
中读表单项的值。但是可以看到,代码中并没有直接通过 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 转型成了InternalFormInstance
。useForm
中使用 FormInstance
是因为useForm
是要 export 出去供用户使用的,而 FormInstance
只有一些 public 的方法。相比 FormInstance
,InternalFormInstance
的 validateFields
的类型被替换成内部版本,还增加了几个方法。接下来,通过 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
中,调用了formStore
的initEntityValue
方法,并且把this
传入。在initEntityValue
中,拿到传给Field
组件的 initialValues
, 如果 formStore 中的对应项还没有初始化,则使用这个initialValues
进行初始化;否则,什么都不做。这里有个小细节,因为是在constructor
里调用的initEntityValue
,所以最后即使这个组件并没有被挂载,通过 formStore 还是可以取到这个 Field
对应 key 的值的。
接下来,我决定先看render
。render
一上来就是可能需要做笔记的地方。在写组件的时候,对于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
会将control
和meta
信息直接传入(此时不需要克隆返回的 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",
});
}
复制代码
registerField
将Field
实例 push 到 fieldEntities
中。如果设置了initialValue
的话,将 formStore 中的对应值重置为初始值,然后调用notifyObservers
,通知Field
实例有值发生了更新。resetWithFieldInitialValue
的代码有点长,这里也就不贴了。总的来说,它的功能就是重置部分或者所有的 fields 为初始值。不过里面也有一些注意点:
- 如果 Form 已经为某个 key 指定了对应的初始值,那么 name 为该 key 的 Field 将不能再次指定初始值。
- 如果多个 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 完成验证工作。