Rc Form 学习笔记
学习 form 的时候遇到的一些问题
- 使用 FormProvider 的时候发现
onFormChange
被触发两次。
<FormProvider
validateMessages={myMessage}
onFormChange={(name, { changedFields, forms }) => {
// 每次form 有更新,这个方法被触发了两次?????
// 一次是 valueUpdate, 一次是 formvalidation。
console.log("changes from: ", name, changedFields, forms);
if (name === "first") {
forms.second.setFields(changedFields);
}
}}
onFormFinish={(name, { values, forms }) => {
console.log("finish form: ", name, values, forms);
if (name === "second") {
forms.first.setFieldsValue(values);
}
}}
>
<div style={{ display: "flex", width: "100%" }}>
<Form1 />
<Form2 />
</div>
</FormProvider>
Field
FieldContext
实际上是FormInstance
,由 Form 通过 Context 传递给 Field。- Field 被 FC 封装,主要设置 key,name,取得 FieldContext。实际的内容在类组件内。
- Field 设置初始值,通过调用 hook 里面的 initialize 方法。
- Field 的 children 是 ReactNode 或者
(control,meta,formInstance)=>{child:ReactNode,isFunction:boolean}
的方法。 - 首先,如果 children 是方法,那么执行该方法,然后将结果递归调用
getOnlyChild
,如果不是方法,则将 children 摊平通过rc-util/toArray
,原理是,如果是空则跳过,如果是Fragment
则递归它的children
,要不就push
到结果结合中。结果返回 childrenlist 或者第一个 children。 - 构建 childNode,如果上一步
isFunction
,则上一步的返回就是 child,如果isValidateElement(child)
,修改它的输入,getControlled(child.props)
, 包括:1.从 form 中拿 field 值,变成 controlled 模式,2.提供trigger
默认是onChange
方法,(响应 Field child 组件值的变化,)更新 Field 状态,touch/dirty/validating,触发onMetaChange
,从onChange
中获取值,dispatch
值到 Form。然后再触发原来的trigger
方法。这里有个技巧,先暂存原理的 trigger 方法,然后用自定义的 trigger 方法复写,在自定义的方法最后再调用原来的方法。 3.按顺序触发 validateList
const originTriggerFunc: any = childProps[trigger];
const control = {
...childProps,
...mergedGetValueProps(value),
};
// Add trigger
control[trigger] = (...args: EventArgs) => {
// Mark as touched
this.touched = true;
this.dirty = true;
this.triggerMetaEvent();
let newValue: StoreValue;
if (getValueFromEvent) {
newValue = getValueFromEvent(...args);
} else {
newValue = defaultGetValueFromEvent(valuePropName, ...args);
}
if (normalize) {
newValue = normalize(newValue, value, getFieldsValue(true));
}
dispatch({
type: "updateValue",
namePath,
value: newValue,
});
if (originTriggerFunc) {
originTriggerFunc(...args);
}
};
- 将上一步的 children 用 Fragment 封装。 提供 key,
key={resetCount}
- 在
componentDidMount
里面向 Form 注册 Field。shouldUpdate
会在挂载后再次触发一次 render。
useForm
Field 的 children 触发 onChange,由 Field 调用dispatch()
方法,我们来捋捋这个方法会干些什么,我们先谈谈 updateValue,这个事件
- 首先进入
useForm.updateValue(namepath,fieldValue)
,首先将 fieldValue 更新到整个 value 中, - 调用
notifyObservers()
: 这个方法则遍历所有的 field,然后,调用field.onStoreChange
方法。这个方法主要是触发 forceRender,这里面有一种场景是由shouldUpdate
来控制是否需要 forceRender,在setField/默认
的info.type
下会触发这个shouldUpdate
执行。1.reset,2.对没有 name 的 field 进行 setField,并且 shouldUpdate 返回 true, 3.dependenciesUpdate,当前 field 依赖它,4.field 属于这次更新的 field 成员中一个,并且 shouldupdate 返回 true.这些条件都会触发forceRender
。 - 调用
useForm.getDependencyChildrenFields(namePath)
获取所有依赖该 field 的孩子 field.这个里面存在一个递归调用,获得,isFieldDirty 的所有孩子孙子 fieldNamePath, - 遍历这些依赖的孩子 Field, 调用
useForm.validateFields()
,它会遍历所有的 fieldEnitties,找到合适的(如果没有提供目标 field,就是所有的,如果有目标,则只找目标),调用目标field.validateRules()
,结果存到promiseList
里面,然后这个 promiseList 会用Promise.allSettled()
。验证的 promise 并没有直接插入promiseList
,而是预先做了 error/warning/success 处理。 field.validateRules()
,首先获取field.getRules()
,它通过 field.props.rules 获取,可能是Rule
也可能是(formInstance)=>RuleObject
. 依据triggerName
过滤 Rule,调用validateUtil.validateRules()
验证,返回 promise. 它先逻辑validateFirst
是 Rules 里面只要有 reject, 就整个 reject,否则,所有成功 resolve([]),parallel validate
,- 对于
summaryPromise
,会先调用notifyObservers(type:'validateFinish')
,然后对有 error 的 field,调用this.triggerOnFieldsChange(resultNamePathList, results)
。 - 接着调用
notifyObservers()
,发布type: 'dependenciesUpdate'
事件。调用onValuesChange
,最后再调用this.triggerOnFieldsChange([namePath, ...childrenFields])
。
List 聊聊这个
它实际上是接管了 List 里面每个选项的 name,key
这两个属性,同时提供了add/remove/move这三个方法
我们来总结一下一些方法 valueUtil
valueUtil.setValues<T>(store: T, ...restValues: T[]): T
, 这个方法其实时lodash.merge(target,...source)
, 它干啥呢。给个它自己的例子,({ a: 1, b: { c: 2 } }, { a: 4, b: { d: 5 } }) => { a: 4, b: { c: 2, d: 5 } }
,({a: [{ b: 2 }, { d: 4 }]},a: [{ c: 3 }, { e: 5 }])=>{a: [{ b: 2, c: 3 }, { d: 4, e: 5 }] }
。valueUtil.getValue(store:Store,namePath:InternalNamePath)
,类似于lodash.get(object,path,default)
。给个例子,自己体会,{a:[{b:{c:3}}]},['a',0,'b','c'] => 3
valueUtil.setValue(store,Store,namePath: InternalNamePath,value: StoreValue,removeIfUndefined = false,)
,类似于lodash.set(object, path, value)
valueUtil.isSimilar()
,这个方法时比较第一层的两个对象是否一样。valueUtil.cloneByNamePathList(store: Store, namePathList: InternalNamePath[]): Store
,这个方法是从 store 中提取特定的属性,同时保持结构, 类似于pick
。跟lodash.pick(object,[paths])
。valueUtilmove<T>(array: T[], moveIndex: number, toIndex: number)
把一个元素移动到另一个位置,会产生一个新的数组。
asyncUtil
allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]>
这个方法其实就是Promise.allSettled(promiseList)
, 这个是es2020
里面的方法。所有的 promise 都结束后再返回 resolve/reject(results[])
NameMap
它其实就是一个 map, 它的特殊是,它的 key 是 string|number[],也就是 form 的 namePath,它内部调用normalize
方法,将路径序列化,同时也可以反序列化。反序列化的时候可以区分 string 与 number。它的主要功能是将 form 里面的 field,按照路径为 key,进行存放,实现缓存。
validateUtil
这个主要是用来做验证的。
validateRules(namePath: InternalNamePath,value: StoreValue,rules: RuleObject[],options: ValidateOptions,validateFirst: boolean | 'parallel',messageVariables?: Record<string, string>,)
它主要用来验证 field,上面所有的 validations. 首先对 rules 排序,warning 优先。validateRule(name: string,value: StoreValue,rule: RuleObject,options: ValidateOptions,messageVariables?: Record<string, string>,): Promise<string[]>
- 这里面有个
validateFirst
,true: 表示,按顺序来验证,挂了,就结束,false: 表示同时并行验证,Promise.all() 来绝对返回结果。
我们来谈谈 Form 从创建,到有值的改变,到提交,这个过程中的调用链。
- 首先我们得有一个
FormInstance
,两种方法获取,一个是主动调用useForm()
,另一个就是通过 ref 拿到Form
内部调用useForm()
创建的。同时,Form
还把自己注册到FormContext
里面,以方便多个Form
之间的协调调用。 Form
向FormInstance
更新ValidateMessage
,传递事件处理函数onValuesChange,onFieldsChange,onFinish,onFinishFailed,setPreserve,setInitalValues
,这个初始值是Form level
的设置。这一些列都属于初始化的操作,将 Form 的输入转移到FormInstance
里面。- Form 有两种使用模式,render Props 以及 ReactElement模式,第一种模式,children:(values,formInstance)=>React.Node,则 Form 会执行这个函数。这种模式下,它属于非 Subscribe模式。
Form
有fields:FieldData[]
的输入参数,它表示的是 fields 的状态跟值(touched,value,name,error .etc) 这个值会依据formInstance, fields
来进行缓存。- 构建
FieldContextValue
, 它其实就是FormInstance + validateTrigger
,通过封装到 FieldContext 传递给 Field,这也是为什么 Field 里面会拿到formInstance
。 它用了 memo 进行缓存。 - Form 以及
Component
参数来绝对直接返回上一步封装的 children, 还是 通过form/Componnet
来封装。 - Form 里面会调用
setCallbacks
,它会将onValuesChange,onFieldsChange,onFinish,onFinishFailed
托管到useForm
里面去。
接下来我们进入到 Fields 里面的创建过程。
首先呢,Fields 对于 Form 来说是平层化了,它的立体化是通过 name 来体现的,name:string|number[]
- Field 是通过一个方法套在了 Field 类上面,方法的作用就是接管
key,name,fieldContext
。把它们作为参数传递给 Field Class。 - Field 的
children:React.ReactNode|(control:ChildProps,meta:Meta,context:FormInstance)=>React.ReactNode
, 如果 children 是 function, 则执行方法,再次调用 getOnlyChild() 方法,递归。执行 children 方法的时候有个getControlled()
, 这个方法功能是将 children 控件变成 control 模式,也就是说给它传递value
, 接受它传递上来的valuechange
,通过valuePropName
,trigger
这两个属性控制的。它拿到更新后的 value 后,通过dispatch('updateValue')
转发到formInstance
里面去。完了,触发forceUpdate
,页面刷新,这个方法又可以拿到更新后的值,再传递给 children 组件,从而实现 controlled 模式下的双向绑定。 这个方法还包括另一个功能,通过dispatch('validateField')
来触发 validation,它是通过在triggerName
里面放置处理函数来实现,它会迭代 Field 里面所有的 validation,来触发验证。 - Field 依据上一步返回的 ChildNode, 如果不是 renderProps, 则调用
getControlled()
,把 children 变成 controlled 模式。然后返回 childrenNode. - 上面主要是谈论的
render()
方法,其中关于valueChange
,trigger
,triggerName
只是用来定义的方法,方法的执行还没有执行。然后在ComponentDidMount()
里面,Field
向formInstance
注册自己。 这样就完成了一个简单的流程。
一直跟着源码走,按着用例走,知道了,它能干啥,怎么用,但是有个问题,Form/Field/List,它的职责是什么,为什么要它,它是怎么做的。我的理解是,
- 它的职责就是,将 Form 的值,保存到一个对象里,或者从一个对象里将值分发到 Dom 里面,同时保证软件运行过程中的同步,validaiton 的及时触发。
- 它是怎么做的,将每个
input
变成controlled
模式,通过trigger
+valuePropName
,拿到 input 改变的值。trigger
默认是onChange
事件,valuePropName
默认是value
也就是(event.target.value
)。这样我们就不用自己定义事件了。而验证条件的触发则是通过validateTrigger
默认是onChange
事件来触发的。
来聊聊如果一个 Field 的值发生了改变,那么整个 Form 是如何运行的。我们假设trigger
是默认值也就是onChange
- 当 Dom 发出onChange事件,Field 首先更新
touched,dirty
,然后激发this.triggerMetaEvent();
,这个方法主要激发onMetaChange
事件,它是由 FieldProps 来的。 - Field 然后拿到onChange里面的值,可能会normalize?.(value),然后调用
useForm.dispatch({type:'updateValue'})
,最后调用我们自己添加到 Field 上面的onChange事件。 useForm.dispatch
里面会调用this.updateValue(namePath, value);
,它首先把 value 保存到整个 Form 的 data 里面,然后调用
this.notifyObservers(prevStore, [namePath], {
type: "valueUpdate",
source: "internal",
});
notifyObservers
这个方法之前也讲过,主要是遍历所有的 FieldList,然后调用它们的onStoreChange(prevStore, namePathList, mergedInfo)
Field.onStoreChange
,这个函数主要功能是更新 Field meta 状态,响应 Field 上本身定义的事件,刷新页面, 首先看自己在不在 这次触发改变的 FielList 里面,对于,if (info.type === 'valueUpdate' && info.source === 'external' && prevValue !== curValue)
,则更新自己 Field Meta 状态。接下来,判断info.type
的类型。- 如果是
reset
,则响应 reset, - 如果是
setField
, 只要当自己在 FieldList 里面才响应。 - 如果是
dependenciesUpdate
,只要自己的 dependencies 里面包含在 FeildList 里面,才响应。 - 对于其他情况,如果跟自己相关,或者需要更新(由 shouldUpdate 计算)同时(要么没有依赖性,是一个真的 field,有名字,或者,设置了 shouldupdate)则刷新页面。还有如果 shouldupdate===true,一定刷新 Field.
- 如果是
- 回到
useForm.updateValue()
这个方法里面接着获取依赖该 field 的所有孩子及孙子,平层化后,调用this.validateFields(childrenFields);
- 接着对这些依赖项调用
notifyObservers
this.notifyObservers(prevStore, childrenFields, {
type: "dependenciesUpdate",
relatedFields: [namePath, ...childrenFields],
});
- 调用注册的 callback 里面的 onValuesChange 方法,然后再调用
this.triggerOnFieldsChange([namePath, ...childrenFields]);
如果直接调用useForm.setFieldsValue()
则调用链
- 这种情况比较简单,直接
setValues()
,然后调用this.notifyObservers(prevStore, null, {type: 'valueUpdate',source: 'external',});
,通知每个 Field,按需刷新。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构