react

rc-field-form源码分析

zwの小站

rc-field-form源码分析

本篇文章将简单分析rc-field-form的源码,rc-field-form是一个react表单管理解决方案,antd的form就是基于他进行的封装,如果大家想要了解react表单的主流解决方案,可以阅读

用法

首先我们来看看rc-field-form的用法

CodeBlock Loading...

在这段代码中,使我们使用 Form 组件来新建一个 Form 表单,然后在Field组件里包裹了我们的每一个表单项,并且通过Field的name字段创建表单的key,rules来配置校验。当我们点击button后,会根据是否通过校验来触发onFinsh或者onFinishFailed。

由此我们可以引出一些问题:

  • Form组件是如何管理我们的表单数据
  • 我们并没有给每个input绑定事件,表单的值是如何更新的
  • Form组件如何对我们的数据进行校验 下面就让我们从源码入手来解决这些问题 # ·如何实现数据的更新 ### Form组件如何管理数据 根据上面的代码,当我们创建Form组件时,必须要给Form组件传入一个通过useForm这个hook得到的form,那么这个form是什么呢?下面是useForm的源码
CodeBlock Loading...

当我们在使用useForm的时候,我们一般不会传入form参数,那么这个hook就会帮我们new一个FormStore,FormStore是一个很重要的类,整个表单数据的存储和操作方法都是由他提供,然后返回Formstore的getForm方法,其实这里就是通过getForm将Formstore里的一些属性和方法暴露了出来。 我们先来简单看下FormStore里都有啥(只展示部分属性和方法)

CodeBlock Loading...

还有很多方法感觉太多了这里没有列举,我们直接按流程进行分析理解,上面我们讲到了Form组件需要传入form,而form是FormStore通过getForm暴露出的一些属性和方法接下来我们来看看Form组件是如何消费form的

CodeBlock Loading...

大概就是为FormStore初始化一些东西,对主流程影响不大,我们继续看,下面来到了创建初始值

CodeBlock Loading...

我们先来看setInitialValues这个方法

CodeBlock Loading...

我们可以看到setinitalValues方法最后调用了updateStore,这个方法很简单

CodeBlock Loading...

直接修改了store,接下来是对child不同type的一些处理

CodeBlock Loading...

如果child是一个函数,则传入childNode为函数返回值,这里的getFieldsValue方法参数为true时返回的就是整个store,也可以传入Field的name数组获取指定的value,具体实现不做分析。继续继续😊

CodeBlock Loading...

然后我们创建了一个context,传入formInstance(FormStore暴露的方法和数据),还有一个validateTrigger,这个我们之前没有提到,这个属性是用户传给Form组件的,他的默认值是onChange,也就是说在onChange的时候会触发Field组件的validate。马上就到尾声了(其实是Form的尾声,后面还有一堆)

CodeBlock Loading...

接下创建一个wrapperNode,其实就是一个contextProvider,ListContext.Provider这个应该是为List组件服务的我们暂不关心,然后我们可以看到FieldContext.Provider就是提供了formInstance和validateTrigger,这样我们的Field组件也可以访问和操作formInstance啦。 然后就是我们最后的代码

CodeBlock Loading...

这里的Component也是由用户传入的,默认值为'form',所以最后的效果其实是 ```js

{ event.preventDefault(); event.stopPropagation();

    formInstance.submit();
  }}
  onReset={(event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();

    formInstance.resetFields();
    restProps.onReset?.(event);
  }}
>
  {wrapperNode}
</form>
这里阻止了一下form的默认行为,然后会在submit和reset时执行formInstance的方法。

到这里我们先总结一下,Form组件都干了什么
- 首先他拿到了form然后setCallbacks,setInitialValues对forminstance的callback和store进行了初始化
- 然后对children进行了处理,用FieldContext将它包裹,让Field组件可以拿到formInstance等一些东西
- 最后用form标签将处理后的child包裹起来,将事件进行绑定
### Field如何消费数据
接下来让我们继续看看Feld组件,Field组件有一点特殊,他是一个class组件🤔,源码中注释是
` We use Class instead of Hooks here since it will cost much code by using Hooks.`大概意思是通过class组件的方式实现可以减少代码量。我们先来看看Field组件大致的代码结构。
js class Field extends React.Component<InternalFieldProps, FieldState

implements FieldEntity { //传入的formInstance public static contextType = FieldContext; //组件默认参数 public static defaultProps //定义一个state用于触发rerender public state //用于组件卸载时清除formInstance里数据 private cancelRegisterFunc //是否已挂载 private mounted = false; //表单校验结果的Promise private validatePromise //校验结果error private errors //校验结果 warning private warnings

constructor(props) {
    super()
    ..........
}
  
public componentDidMount
public componentWillUnmount() {
}
//会调用并销毁cancelRegisterFunc
public cancelRegister
//获取Field的name
public getNamePath
//获取传入的rules
public getRules
//用于更新组件
public reRender
public refresh
// ========================= 这个方法很重要,跟组件更新相关 ==============================
public onStoreChange
//校验rulues的方法
public validateRules
public isFieldValidating = () => !!this.validatePromise;
   
//获取校验后的结果 public getErrors public getWarnings //对传入的child的一些处理 public getOnlyChild //返回当前Field字段的值 public getValue // ======== 就是这个方法让我们传入的组件受控,劫持了组件的onChange这类事件 ==================== public getControlled //返回处理后的子组件 public render() {
}
  
}

``` 了解了大体结构,接下来我们先从constructor入手,看看Field组件的渲染流程。

CodeBlock Loading...

这里其实就是调用了initEntityValue这个函数,传入Field组件

CodeBlock Loading...

这里就是简单设置了一下初始值并不会引起Field的rerender,接下来我们继续看Field组件都做了哪些初始化

CodeBlock Loading...

这里的核心就是registerField,还记得我们之前提到过formStore的fieldEntities数组里存储了Field吗,这个函数其实核心就是fieldEntities.push(field),然后给我们返回了一个函数用于在fieldEntities里delete这个Field,这样我们就可以通过formInstance调用Field里暴露的一些方法用于更新或者校验。

然后就是render函数

CodeBlock Loading...

getonlychild返回第一个child和他的类型,当child是合法的reactElement时,调用,然后返回Fragment包裹的cloneElement,所以这里的关键就是这个cloneElement的第二个参数,这里到底传入了什么东西🤨。
简单地说,cloneElement的第二个参数其实是props,它可以覆盖默认的props,fc-field-form就是在这里接管了Field的onChange等一系列事件,我们来看看getControlled的源码

CodeBlock Loading...

到了这里其实我们已经找到了rc-field-form在表单触发事件时,虽然我们并没有绑定事件,但是它已经将其劫持,并且通过dispatch这个函数通知formStore进行数据上的更新。接下来我们一起来探索data和ui是如何更新。

dispatch这个函数的代码很少,如下 js private dispatch = (action: ReducerAction) => { switch (action.type) { case 'updateValue': { const { namePath, value } = action; this.updateValue(namePath, value); break; } case 'validateField': { const { namePath, triggerName } = action; this.validateFields([namePath], { triggerName }); break; } default: // Currently we don't have other action. Do nothing. } }; 可以看到,我们触发updateVlue进入了updateValue这个函数

CodeBlock Loading...
简略后的代码如上,updateStore更新了一下store,其实到这里我们就已经将formInstance的store更新了,接下来思考的是如何更新ui,我们一起来看看这个notifyObservers函数
CodeBlock Loading...

还记得我们提到过Field里的onStoreChange与更新有关吗,没错现在这一切都连了起来。

CodeBlock Loading...

rerender方法也很简单,就是使用了类组件的forceUpdate强制更新 js public reRender() { if (!this.mounted) return; this.forceUpdate(); } 到这里,我们已经知道了表单如何进行最基本的更新,这里放一张字节大佬的图

![64652037b3ee4d1184d79e8e105e2429~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75[1].awebp]() 再理一下思路

  1. 首先我们在Form组件中创建了formInstance,将初始值和一些callback绑定到了form上,然后通过FieldContext将formInstance下放到每个Field组件里实现了方法和数据的共享
  2. 在Field组件中,它会在componentDidMount阶段被注册到formInstance中,然后我们通过cloneElement这个api对传入Field的子组件的事件进行了劫持,当触发某生事件时Field组件调用formInstance的dispath方法开始触发更新
  3. dispath根据不同的action type会派发不同的事件,在updateVlue的情况下调用了updateValue方法,这个方法中首先通过updateStore方法对Store中的数据进行了更新,然后调用notifyObserver方法,notifyObserver会遍历所有Field组件,调用他们的onStorechange方法,每个Field组件会判断是否需要更新和更新的类型(如reset,setField),然后据此进行不同的操作,最后更新的方法是refresh(),其实就是调用了类组件的forceUpdate方法

上面这种更新方式是通过用户的一些行为,我们也可以通过forminstance暴露的一些方法如setFieldsValue对表单进行更新,我们再来看看这是如何做到的

CodeBlock Loading...

最后走到onStorechange的代码

CodeBlock Loading...

只是多了一些数据的处理,其他代码都差不多,到这里form的更新大概就聊完了,接下来我们来说说表单校验是如何实现的。

·如何实现校验

如何触发校验

想要知道校验的实现,我们还是得先看看有哪些方法可以触发form表单的校验,首先是formInstance的submit

CodeBlock Loading...

还有当一些用户行为触发的事件如onChange,其实这也是在getControlled里帮我们劫持了

CodeBlock Loading...

这里的dispatch也会触发formInstance的validateFields方法,下面我们就把注意力放到这个函数中

校验的实现

CodeBlock Loading...

这里的代码比较长,我们来整理一下关键的地方:如果传入了nameList那么会对nameList对应的Field进行校验,否则就会全部校验,当然他们需要传入了rules,而这里的校验方法其实是调用的Field组件的validateRules方法,这个函数我们后续会分析,然后我们会把validateRules返回的promise收集到promiseList中,通过allPromiseFinish函数,我们就可以拿到校验结果的数组了,接下来主要就是2件事,一是通知对应的Field进行更新,二是根据校验结果返回Promise作为 onFinishFailed和onFinish的触发依据,这里的逻辑也比较简单。所以下面我们就来看看Field组件中的validateRules方法

CodeBlock Loading...

所以这里的大部分代码还是在进行流程的串联和Field内部状态的一些处理,校验相关的还是也并非在这里实现,其实rc-field-form的表单校验依赖了rc-component/async-validator,然后对其进行了一些封装,这里也不做过多介绍了。

三 一些其他功能

上面聊完了form的核心功能,下面我们再来看一看一些比较好用的特性. 首先是list组件,这里我放一个,不熟悉的可以去了解一下效果。

List

CodeBlock Loading...

大概的使用方法如上,我们需要给List组件传入一个函数,然后在这个函数里通过组件给我们的fields参数进行遍历渲染出每个Field,同时他也给我们提供了一些方法对数据进行操控,下面我们一起来看看如何实现。先贴出这个组件的代码,然后我们来慢慢分析。

CodeBlock Loading...

看到这里其实我们能够发现,list组件其实还是根据Field组件进行的封装,现在我们再来看看Field组件里都有什么东西

CodeBlock Loading...
首先我们注意到,Field组件里我们传入的也是一个函数,这里需要先带大家复习一下,在Field组件中,如果我们传入的child是一个函数,那么会传入getControlled(), meta,fieldContext这3个参数(相关函数getOnlyChild ),并将函数的返回值作为最终的child,meta其实就是Field的一些状态,接下来来看看operations
CodeBlock Loading...

其实这一部分就是维护了一个对象,提供了之前add等一些修改数据的方法,其他方法这里省略了。

CodeBlock Loading...

这里也很简单,对listvalue进行了一些判断,接下来是最后的一部分

CodeBlock Loading...

这里的children是什么呢,其实这里的children就是我们在List组件里传入的函数,这样的话就很明显了List组件帮其实就是帮我们进行了数据管理,并将操作数据的方法暴露给我们,我们再来看看最开始我们是如何使用List组件的

image.png 这里传递的filed属性其实就把这样的属性传递给了Field。

image.png
接着我们再来看看List组件为什么能够做到对子数据的统一管理呢。
举个简单的例子,比如我们维护了一个users的Field数组,那么他的数据结构大概是 users:['xiaomin','xiaozhang'],当我们通过list组件暴露出来的方法对数据进行修改时因为list组件是基于Field的封装,所以这些修改会触发onStoreChange让list组件rerender,而其中的子Field自然也会重新渲染,那么子Field是如何获取正确的值呢,看上面那张图,我们给子组件传递了一个key,在getControlled的时候,Field会调用getValue方法获取值,其实这个getValue函数就类似与lodash中的get方法,而如果一个Field是listField的话,那么当我们获取namePath时其实一种 [parentName,key] 的形式

image.png 所以我们就可以根据这个获取新值达到更新的效果,这里还有一点,某个子Field的更新其实是不会影响到List组件的。

dependence

最后我们再来聊一聊另一个功能,dependence.这里还是给出一个,简单的说,就是我们可以给某个Flied配置dependence字段,当dependence数组中包含的Field触发了更新,这个Field也会同步触发更新。 下面是rc-field-form官方demo,大家可以自己试一下。当name为1时可以看到password渲染,然后password如果不为空则password2渲染 ,后来在写文章的时候感觉这个例子是有问题的,我们一会再分析

CodeBlock Loading...

这里我们直接来讲他是如何实现的,我看了一下源码然后写demo测试后发现在rc-field-form里如果我们通过如setFieldValue这样的api是无法触发dependence更新的,这里我们就只聊通过onChange等行为触发的更新。

CodeBlock Loading...

可以看到具体的逻辑是由updateValue开始的,我们先来看看triggerDependenciesUpdate干了啥

CodeBlock Loading...

接下来我们看看getDependencyChildrenFields这个方法,

CodeBlock Loading...

接下来我们来看看具体的处理,首先是构建依赖map

CodeBlock Loading...

举个例子,如果C和D依赖B,B依赖A,这样就会创建出这样的map来,我们为什么需要这样的操作呢,其实我们可以想一下,在这个例子中,虽然C,D的dependence是B,但是B同时也依赖于A,那么如果A触发了更新,C和D也应该更新,所以getDependencyChildrenFields就是为了解决这种循环依赖,现在我们已经有了dependenceMap,接下来就需要通过这个map获取所有的依赖

CodeBlock Loading...
CodeBlock Loading...

这样我们就可以拿到所有依赖的Field了,我们继续回到更新的流程。

CodeBlock Loading...

这里已经很熟悉了,通过notifyObserver调用所有Field的onStoreChange,我们直接看在onStoreChange里进行了哪些操作

CodeBlock Loading...

其实这里也比较简单,所以dependence的流程我们也分析完了。
最后就是说说刚才我提到了官方的demo有问题,下面就来谈谈为什么有问题。还是先放一下代码

CodeBlock Loading...

首先,我们可以看到在官方demo中每个配置了dependence的Filed字段其实是没有配置name的,但是当我们构建childrenFields时是需要获取Field的name的,这就导致了获取childrenFields其实是获取了一个空数组,这样看来,如果当前name=1,password存在value,然后我们改变name的值,password和password1都不会隐藏,但是我们可以发现这个demo运行起来其实是没有问题的,这是为什么呢。
关键在这里:

CodeBlock Loading...

我们在触发dependence更新的时候在relatedFields中还把触发更新的Field name传递了过去,这里也就是'name',所以当我们触发所有组件的onStoreChange,password是能够更新的,那password1又是如何正确更新的呢?

image.png 还记得这个方法吗,password这个Field在卸载的时候会执行这个方法,这个方法其实就是registerField的返回函数,而这个返回函数里又调用了this.triggerDependenciesUpdate(prevStore, namePath);,后面的流程就跟上面相似了。

完结撒花

写了这么多终于把rc-field-form的一些主要流程讲完了🧐,第一次写文章写的真挺烂的,最后还是大家推荐一些关于rc-field-form的文章: