前言
不知道有没有小伙伴跟我一样,在使用antd
之后,对其中的Form组件的实现很好奇。众所周知在react中是区分受控组件和非受控组件的,而要实时记录输入组件的value值,就需要用到受控组件同时结合state来保证数据更新后组件也会同时被重新渲染。而Form组件并不是使用state记录输入组件的value值的,那么它是如何实现更新数据后重新渲染组件的呢?下面我们就一起来讨论下这个问题。
forceUpdate
要使得组件重新渲染,除了更新state以外,class组件有一个强制更新当前组件的方法:forceUpdate
。调用该方法会直接跳过shouldComponentUpdate
生命周期,直接执行render函数重新渲染当前组件。
交出组件的控制权
用过Form组件的小伙伴肯定都知道,Form组件要获取表单值或者更新表单值都是通过form对象实现的(这里默认大家都用过了,如果没用过的话就请移步到antd-form熟悉一下Form的用法)。尽管有了forceUpdate
方法可以让我们在不使用state储存数据的情况下也能重新渲染组件,但是却面临了下一个问题:在组件外部如何调用forceUpdate
方法呢?请看下面的demo:
class Test {
constructor(store) {
store.registerInstance(this)
}
print() {
console.log(Math.random())
}
}
class Store {
instances = []
registerInstance(instance) {
this.instances.push(instance)
}
print() {
this.instances.forEach(i => {
i.print()
})
}
}
const store = new Store()
new Test(store)
new Test(store)
store.print()
// 打印结果
// 0.44673102451121527
// 0.8664812722668469
如上代码所示,Store类的instances用于存储Test组件的实例,这样就相当于把Test类的控制权移交给了Store类,在Store中可以执行Test类的任何方法、可以读取/更新Test类的任何属性。React类组件也可以用同样的方法将当前组件的控制权移交给外部,从而达到在外部调用forceUpdate方法重新渲染组件。
具体代码实现的关键点
解决了以上两个问题我们就可以开始Form组件的实现了。首先我们将外部记录输入组件的value值、控制组件重新渲染的类叫做FormStore,而将注册到FormStore中的组件实例叫做字段实体(fieldEntity)。请看下面的代码:
export class FormStore {
private values: StoreValue
private initialValues: StoreValue
private errors: Record<string, string>
private fieldEntities: IFieldEntity[]
constructor() {
this.values = {}
this.initialValues = {}
this.errors = {}
this.fieldEntities = []
}
getInternalHooks = (mark?: string): InternalHooks | null => {
if (mark === HOOK_MARK) {
return {
registerField: this.registerField,
unregisterField: this.unregisterField,
getForm: this.getForm,
setFieldError: this.setFieldError,
setInitialValue: this.setInitialValue,
removeFieldError: this.removeFieldError
}
}
warning(false, '`getInternalHooks` is internal usage. Should not call directly.')
return null
}
registerField = (fieldEntity: IFieldEntity): void => {
this.fieldEntities.push(fieldEntity)
}
}
export class Field extends Component {
init = false
componentDidMount(): void {
if (!this.init) {
const { name, initialValue } = this.props
// FormStore是用过Context的形式在Form组件的上下文互相传递的
const { registerField, setInitialValue } = this.context.getInternalHooks(HOOK_MARK)
this.init = true
registerField(this)
if (name) {
setInitialValue(name, initialValue)
}
}
}
}
如上述代码所示,FormStore类有values用于存储实时的Form表单value值、fieldEntities存储组件的实例。接下来要做的就是将FormStore的values绑定到Form表单的输入组件上,如下:
export class Field extends Component {
init = false
componentDidMount(): void {
if (!this.init) {
const { name, initialValue } = this.props
// FormStore是用过Context的形式在Form组件的上下文互相传递的
const { registerField, setInitialValue } = this.context.getInternalHooks(HOOK_MARK)
this.init = true
registerField(this)
if (name) {
setInitialValue(name, initialValue)
}
}
}
reRender = (): void => {
this.forceUpdate()
}
getControlled = (childProps: Record<string, any>): Record<string, any> => {
const {
name,
valuePropName,
changeMethodName,
validateTrigger
} = this.props
const { getFieldValue, setFieldValue } = this.context
if (!name) {
return { ...childProps }
}
return {
...childProps,
[valuePropName]: getFieldValue(name),
[changeMethodName]: (value: any) => {
setFieldValue(name, value)
if (validateTrigger === 'onChange') {
this.validateRules(value)
}
childProps[changeMethodName]?.(value)
},
onBlur: (e: NativeSyntheticEvent<TextInputFocusEventData>) => {
if (validateTrigger === 'onBlur') {
this.validateRules(getFieldValue(name))
}
childProps.onBlur?.(e)
}
}
}
}
以及FormStore的更新策略:
export class FormStore {
private values: StoreValue
private initialValues: StoreValue
private errors: Record<string, string>
private fieldEntities: IFieldEntity[]
constructor() {
this.values = {}
this.initialValues = {}
this.errors = {}
this.fieldEntities = []
}
getInternalHooks = (mark?: string): InternalHooks | null => {
if (mark === HOOK_MARK) {
return {
registerField: this.registerField,
unregisterField: this.unregisterField,
getForm: this.getForm,
setFieldError: this.setFieldError,
setInitialValue: this.setInitialValue,
removeFieldError: this.removeFieldError
}
}
warning(false, '`getInternalHooks` is internal usage. Should not call directly.')
return null
}
registerField = (fieldEntity: IFieldEntity): void => {
this.fieldEntities.push(fieldEntity)
}
private notifyObservers(
prevValues: StoreValue,
fields: string[] = [],
force = false
) {
this.fieldEntities.forEach(entity => {
const shouldUpdate = entity.props.shouldUpdate
if (force) {
entity.reRender()
} else if (fields.includes(entity.props.name as string)) {
entity.reRender()
} else if (typeof shouldUpdate === 'function' && shouldUpdate(prevValues, { ...this.values })) {
entity.reRender()
} else if (shouldUpdate) {
entity.reRender()
}
})
}
setFieldValue(field: string, value: ValueType): void {
if (field == null) {
return
}
const prevValues = { ...this.values }
this.values[field] = value
this.notifyObservers(prevValues, [field])
}
}
useForm
最后再说一下useForm的实现,useForm利用useRef来保证每次函数组件运行都只生成一个FormStore的实例,代码如下:
export function useForm<Values = ValueType>(): [FormInstance<Values>] {
const formRef = useRef<FormInstance<Values>>()
if (!formRef.current) {
const formStore = new FormStore()
formRef.current = formStore.getForm<Values>()
}
return [formRef.current]
}
总结
以上就是Form组件将数据存储到FormStore的同时又能重新渲染组件的实现原理,如有说的不好之处还请各位小伙伴多提提意见帮助我改进改进。 本文中只展示了部分关键的代码,如果想看完整代码请移步到我的组件库rn-element。