背景
formily 框架是一个复杂表单领域的解决方案,它既能优雅的实现复杂的表单交互逻辑,又得益于其分布式状态管理的理念,在大量表单字段场景下,有着很好的性能表现。但 formily 框架还是有一定的上手门槛,尤其对一个新人,往往刚接触 formily 时会不可避免的踩一些坑,比如下面关于 actions 的问题:调用 forceUpdate() 进行强制刷新以后再次点击提交时,actions 就失效了
我当时遇到这个问题的时候,被卡了大半天,并且在实际业务中只会更加的复杂,真正的问题往往会被层层包裹,一时间很难被定位出来。所以为了后续开发的顺利,本文试着说明下 formily 内部的一些工作机制。当然由于本人并不是 formily 框架的开发者,所以有写得不对的地方还望各位指出
架构
formily 项目是一个 Monorepo 仓库(单一代码库),其中包含了 antd、antd-components、core、devtools、...。这些包按功能主要分为四大类:核心层、UI 渲染层、Schema 协议层以及开发工具层
- 核心层:提供了 formily 的核心能力,主要包括底层的数据结构以及核心的 API
- UI 渲染层:提供了 UI 相关的组件实现,这里接入了常见的 antd、next(fusion)和 meet 这三个 UI 组件库
- Schema 协议层:formily schema 协议的实现(协议的联动、渲染、布局等能力)
- 开发者工具:提供针对 formily 的调试等能力
上述的分层结构可以将 formily 的核心能力和 UI 分开,方便跨端使用
本文主要讲解核心层的实现,并结合一个实际的例子对其运作机制进行探讨(注意:下面讲述的 formily 版本是 1.x)
工作机制
formily 框架的本质是一个无限循环的状态机
对于一个常见的表单场景:表单由 Form 对象维护,表单字段由 Field 对象维护(每个字段都对应一个独立的 Field 对象),它们的状态都由各自内部维护。表单和字段可以看成一个个节点,由 FormGraph 对象来管理。FormHeart 对象管理表单的整个生命周期,包含表单的初始化、字段状态的变动等生命周期事件,并将这些事件通知到其他对象
基础数据结构
formily 中主要通过以下 5 种数据结构来实现上述的状态机:
- Subscribable:实现发布/订阅功能的类,同时也是 Model、FormGraph 和 FormHeart 的基础类
- Model(Form、Field):状态管理类,提供了对状态的基本操作,包括改变状态、获取状态等。注意:Model 类只是提供了对状态的基本操作,实际的状态对象由其中的 factory 属性实现,Form 类由 Model 类结合 FormStateFactory 组成,Field 类由 Model 类结合 FieldStateFactory 组成。Model 类继承自 Subscribable 类,具有发布/订阅能力
- FormGraph:节点管理的类。在 formily 中,表单对象对应 Form 类的实例,表单字段对应 Field 类的实例,Form 和 Field 的实例都可以看成是一个个独立的节点,而 FormGraph 就是用来管理这些节点的。它提供包括遍历、查找节点等操作,这里每个节点都以他们的路径作为唯一的标识,并且通过 refrences 属性来管理节点之间的父子关系。同时继承自 Subscribable 类,具有发布/订阅能力
- FormHeart(FormLifeCycle):生命周期管理类,通过 lifecycle 属性来管理不同的生命周期。生命周期是 formily 中实现表单联动逻辑的核心能力。同时继承自 Subscribable 类,具有发布/订阅能力
- FormValidator:实现字段值校验的类
Subscribable
Subscribable 是一个实现发布/订阅功能的类,它也为 formily 中消息的通知提供了底层支持
Subscribable 对象通过 subscribers 属性管理监听事件,通过 subscribe/unsubscribe 函数添加/移除订阅函数,通过 notify 函数来发布消息通知,通过 subscription 属性来支持自定义监听的属性
Model(Form、Field)
如前所述,formily 本质是一个状态机,并且每一次的状态变更都会通知其他节点。而 Model 类就是负责管理这些状态的基础模型,它提供了获取状态(getState)、更新状态(setState)、批量更新状态(batch)等操作
注意:Model 类内部通过 factory 属性(一个通用的状态,类似于范型)来表示通用状态对象,实际的状态对象还需要传入,比如 Form 就是 Model + FormStateFactory(factory) 组成,而 Field 就是 Model + FieldStateFactory(factory) 组成。在 Model 类中提供了状态变更的基本流程,而 FormStateFactory 和 FieldStateFactory 类则提供了实际的状态以及变更后的一些钩子函数
实际的 Form 和 Field 的状态如下:
// form 的状态
state = {
valid: true,
invalid: false,
loading: false,
validating: false,
initialized: false,
submitting: false,
editable: true,
modified: false,
errors: [],
warnings: [],
values: {},
initialValues: {},
mounted: false,
unmounted: false
}
// field 的状态
state = {
name: '', // field 的路径名
path: '', // field 的路径名
dataType: 'any',
initialized: false,
pristine: true,
valid: true,
modified: false,
inputed: false,
touched: false,
active: false,
visited: false,
invalid: false,
visible: true,
display: true,
loading: false,
validating: false,
errors: [],
values: [],
ruleErrors: [],
ruleWarnings: [],
effectErrors: [],
warnings: [],
effectWarnings: [],
editable: true,
selfEditable: undefined,
formEditable: undefined,
value: undefined,
visibleCacheValue: undefined,
initialValue: undefined,
rules: [],
required: false,
mounted: false,
unmounted: false,
props: {}
}
状态的管理是 formily 中需要处理的核心问题,在 formily 中,是通过 immer 框架来实现了对状态变更的管理
immer 框架提供了 produce 函数来抓取到用户对基本状态(baseState)的变更操作,并将这些变更操作序列化为 patches 对象返回。示例如下:
Model 中的状态更新就是使用了上述的 produce 能力,来获取到用户对状态变更的操作,从而可以在状态变更的时候进行一些自定义的操作。过程如下:其中蓝色的部分是 Model 类中修改状态的固定流程,红色的部分则是状态对象提供的钩子,分别在变更前、变更中和变更后提供了 beforeProduce、produce 和 afterProduce 这三个钩子,并且是由状态对象自己实现这些钩子函数,实现自定义的操作内容,也就是 FormStateFactory 和 FieldStateFactory 中的对应方法
FormGraph
在 formily 框架中,表单(Form 类的实例)、字段(Field 类的实例)都可以看成一个个独立的节点,他们的状态由各自维护,而 FormGraph 好比一棵树,用来挂载和管理这些节点。他们的关系如下:其中 Form、Field 等节点都是以路径(Path)作为标识,Form 节点的路径为空字符串,为所有节点的父节点。在 FormGraph 中通过 refrences 属性来管理这些节点的父子关系,并提供添加节点、删除节点、选择节点、遍历节点等操作
FormGraph 类提供了添加/删除/替换/遍历等节点的操作,所有的节点都挂载到 nodes 属性中,并且节点的路径作为 key 值,同时节点之间的父子关系通过 refrences 属性来实现
注意: 路径为空字符串的节点是所有节点的父节点,在 formily 中,表单对象的路径就是空字符串
FormHeart(FormLifeCycle)
FormHeart 类是用来管理生命周期的,它也是 formily 中实现表单联动逻辑的核心。lifecycles 属性管理生命周期事件,并通过 publish 函数来发布生命周期事件
FormLifeCycle 类是生命周期类,它可以接收 3 种不同类型的参数(函数作为参数、事件类型和处理函数作为参数、对象作为参数),并在对应的生命周期中调用相应的监听函数,如下:
FormHeart 类提供了对上述生命周期对象的管理,通过 lifecycles 属性来管理所有的生命周期对象,并提供 beforeNotify, afterNotify 方法作为生命周期处理的前后钩子。通过 publish 函数来发布生命周期事件,如下:
FormValidator
FormValidator 是管理校验逻辑的类,内部通过 nodes 属性来管理所有的节点,这里同样也是以节点的路径作为 key 值,如下:
FormValidator 类中校验的本质是通过内置的 ValidatorRules 对象管理的校验函数来进行校验的。根据用户传入 rules 校验规则(比如 required, max, ...),它会在 ValidatorRules 对象中找对应的校验函数,如果存在,则调用该校验函数进行校验。如下:
const ValidatorRules = {
[ruleKey]: ruleValidateFn,
}
/*
ruleKey 的值如下:
required, max, maximum, exclusiveMaximum, minimum, exclusiveMinimum, len, min, pattern, validator, whitespace, enum
每个 ruleKey 都有各自的校验函数 ruleValidateFn
*/
比如用户传入以下的校验规则时,此时 rules 对象中的 required 属性会命中 ValidatorRules 中的 ruleKey = required 的校验函数,然后会调用该校验函数对字段值进行校验
const rules = [{
required: true,
message: '必填项'
}]
FormValidator 类中也提供了对常见的数据格式的校验,比如 url、email、ipv6 等格式。这是通过内置的 ValidatorFormators 对象来实现的,其内部添加了常见的数据格式的正则表达式,如下:
const ValidatorFormators = {
url: new RegExp(...),
email: new RegExp(...),
ipv6: new RegExp(...),
...
}
在实际使用中,用户通过 format 属性来表明校验的数据形式,如下:
const rules = [{
format: 'number',
message: '必须是一个数字'
}]
对于上述的 format 格式的校验,在校验过程中会对 format 格式的校验对象添加 pattern 属性,然后调用 ValidatorRules 对象中 ruleKey = pattern 的校验函数进行校验,pattern 对应的校验函数就是一个 类似于 (new RegExp()).test() 的正则校验表达式
FormValidator 中也支持自定义校验规则,其本质就是将自定义校验规则添加到 ValidatorRules 对象中。FormValidator 中还支持自定义模板引擎,这里会将用户自定义的模板引擎去替换内置的 template 引擎
分布式状态管理框架 react-eva
在 formily 中出于性能的考虑,摒弃了 react 中的数据驱动理念,而是使用 API 的方式来进行组件之间的通信。这样的好处是可以将更新精准的局限在某个组件,而不会从根节点往下全量更新
如下,react-eva 提供了一个 actions 对象来管理 API,组件只需要将需要对外暴露的 API 添加到 actions 对象中(通过 implementActions 方法来添加 API),其他组件就可以通过 actions 对象来直接使用这些 API 了
react-eva 中还提供了 effects 对象来处理组件之间的联动逻辑。在 effects 内部可以用传入的 $ 函数来添加特定事件的监听函数,然后通过它提供的 dispatch 函数来触发这些事件。如下:在 effects 中添加了 onClick 事件的监听,当调用 dispatch('onClick') 时,触发监听函数,并通过 actions 对象的 API 更改组件 B 的状态为 'aa':
const effects = createEffects(async $ => {
$('onClick').subscribe(() => {
actions.setStateB('aa');
});
});
// 当调用 dispatch('onClick') 后,组件 B 的状态会被设置为 'aa'
下面通过一个完整示例来说明 react-eva 的工作过程:首先调用 createAsyncActions 和 createEffects 创建 actions 和 effects 对象,注意此时 actions 对象中的 getText 和 setText 函数都是空函数,内部还未实现。然后使用 useEva 得到内部的 API:implementActions(用来挂载实际的 API 到 actions 对象上) 和 dispatch(触发 effects 中监听的事件)。这时候内部会直接运行 effects 函数,并注入监听函数 添加对 onClick 事件的监听(最后会保存在 createEva 的闭包变量 subscribes 中)。接着调用 implementActions 函数来实现 actions 中的 API,这样其他组件中就可以直接使用这两个API。当用户调用 dispatch('onClick') 时,就会在 createEva 的闭包变量 subscribes 中触发对应事件的监听函数,在监听事件中,会调用 actions 对象的 setText API 来改变组件的状态,一次联动就结束了
注意:actions 对象中有个延迟调用的机制。如果在调用 implementActions 实现具体的 actions 中的 API 之前,直接调用该 API 的话(如下,在 effects 内部,先直接调用了 actions.getText(),但此时还未调用 implementActions 实现 getText 方法),在 actions 对象内部会先将此次调用保存在 resolvers 中,等后面调用 implementActions 实现了对应 API 后,才会拿出来运行
核心 API
createForm
初始化 formily 框架:创建 FormGraph 对象并添加监听函数 onGraphChange,创建 Form 对象并添加监听函数 onFormChange,创建 FormValidator、FormHeart 对象,并将 Form 对象添加到 FormGraph 中进行管理(路径为空字符串),同时发布 'onFormWillInit' 生命周期事件,最后返回内置的 api,如下:
初始化完成后,我们在 createForm 的闭包中可以得到 graph、form、validator 和 heart 这4个对象(还有个环境变量 env),这4个对象分别就是上面基础数据结构中的对应类的实例对象
这4个内部对象就是 formily 框架工作的核心对象,后续的所有操作都依赖于这4个对象:
- graph: form 和 field 节点的管理对象,并通过 refrences 维护节点的父子关系。其内部添加了 onGraphChange 监听事件
- form: 表单管理对象,同时也是所有表单字段的父节点。其内部添加了 onFormChange 的监听事件,在每次 form 的状态变更后,都会调用 onFormChange 进行处理
- heart: 管理 formily 的整个生命周期
- validator: 管理 formily 中字段的校验
registerField
创建字段对象。在 formily 中,每个表单字段都是独立的节点,这些字段节点就是通过 registerField 创建的,在创建的时候,会添加字段变更的监听函数 onFieldChange,在每次字段值有更新时,会调用该函数。同时字段创建完成后会将其添加到 graph 对象中管理(字段的路径作为 key),并且也会将其添加到 validator 中,供后续字段校验时使用(字段的路径作为 key),如下:
注意:在创建字段的时候,会通过 setValue 钩子函数来将字段状态同步到表单状态中:
function registerField(...) {
...
const field = new Field({
...,
getValue(name) { // 通过 getValue 钩子函数,将字段的 value 值代理到 form 状态中该字段路径下的 value
return getFormValuesIn(name)
},
setValue(name, value) { // 通过 setValue 钩子函数将字段的值同步更新到 form 对象的 state 中
upload(() => {
setFormValuesIn(name, value)
})
},
})
}
setFormState/getFormState
表单对象(form)的状态设置 API。form 对象是 Model 类的实例,所以这两个 API 最终会调用 Model 类的 setState/getState 来设置/获取状态(详细的状态设置过程可看基础数据中的 Model 类),具体过程如下:
setFieldState/getFieldState
字段对象(field)的状态设置 API。field 对象也是 Model 类的实例,所以这两个 API 最终会调用 Model 类的 setState/getState 来设置/获取状态,在状态变更后,会发布 onFieldValueChange 的生命周期事件
form 对象和 field 对象的值同步机制
在 formily 中,form 对象的 state.values 中包含了所有 field 对象的 value,而每个 field 对象又各自维护自己的状态,所以 form 状态中的 values 和各个 field 对象的 value 这两者需要保持一致。而这种一致性主要通过 field 字段的 getValue 和 setValue 钩子函数来确保。如下:
createMutators
创建字段的操作对象。传统的表单控件如 input 输入框、select 选择框等都有 change、focus、blur 等各种事件,所以 formily 中提供了 createMutators 方法来快速生成对应的字段操作方法,方便修改字段的对应状态,如下:该 API 最终返回 mutators 对象,其中包含 change、focus、blur 等方法,change 方法修改 field 对象中状态的 value 值,focus 方法修改 field 对象中状态的 active 值,blur 方法修改对象状态中的 active 和 visited 值,validate 方法则进行字段的校验
注意:这里在调用 change 修改字段的状态时,最终还是调用 field.setState 方法来修改,但是最后会发布 onFieldInputChange 的生命周期事件。而 actions.setFieldState 方法也能修改字段状态,但并不会发布 onFieldInputChange 的生命周期事件
validate
校验函数,主要通过 createForm 初始化后的闭包变量 validator 对象实现校验,详情可见上述基础数据结构中的 FormValidator
submit
表单提交 API,在提交前会先调用 validate 函数进行校验,校验通过后,则返回 form 对象的值,如下:
实例说明
这里结合一个具体的例子来说明 formily 框架是如何结合 react 工作的,如下:通过 Form 和 Field 组件我们构建了一个基本的用户名和密码框的表单,同时在 effects 中添加了联动逻辑的处理(联动逻辑:当用户在 username 中输入 123 时,password 字段隐藏),最后当用户点击提交按钮后,打印出用户输入的内容
import React, { useState } from 'react'
import ReactDOM from 'react-dom'
import {
Form,
Field,
createAsyncFormActions,
FormEffectHooks
} from '@formily/react'
const { onFormInit$, onFieldInputChange$ } = FormEffectHooks
const actions = createAsyncFormActions()
const App = () => {
const handleSubmit = async () => {
const params = await actions.submit();
console.log('### values: ', params.values);
}
return (
<Form
actions={actions}
effects={() => {
onFormInit$().subscribe(() => {
console.log('initialized')
})
onFieldInputChange$('username').subscribe(({ value }) => {
actions.setFieldState('password', state => {
state.visible = value === '123' ? false : true;
});
})
}}
>
<div>
<label>username: </label>
<Field name="username" rules={[
{
format: 'number',
message: 'username is not a number.'
}
]}>
{({ state, mutators }) => (
<React.Fragment>
<input
disabled={!state.editable}
value={state.value || ''}
onChange={mutators.change}
onBlur={mutators.blur}
onFocus={mutators.focus}
/>
<span style={{ color: 'red' }}>{state.errors}</span>
<span style={{ color: 'orange' }}>{state.warnings}</span>
</React.Fragment>
)}
</Field>
</div>
<div>
<label>password: </label>
<Field name="password">
{({ state, mutators }) => (
<React.Fragment>
<input
disabled={!state.editable}
value={state.value || ''}
onChange={mutators.change}
onBlur={mutators.blur}
onFocus={mutators.focus}
/>
</React.Fragment>
)}
</Field>
</div>
<div>
<button onClick={handleSubmit}>提交</button>
</div>
</Form>
)
}
ReactDOM.render(<App />, document.getElementById('root'));
上述例子的渲染效果如下:
表单中包含了 username 和 password 两个输入框,其中 username 字段限制了用户只能输入数字,当 username 输入值为 123 时,password 字段隐藏。点击提交的时候,最终打印出用户输入的内容
表单初始化
初始化过程主要有三个部分:
- react-eva 的初始化:创建 actions 对象,添加 effects 中的生命周期事件
- 调用 createForm 创建表单对象:创建 graph、form、heart 和 validator 对象
-
- 生命周期对象 heart 中添加了两个生命周期监听事件 handler1 和 handler2。其中 handler1 监听所有类型的生命周期事件,并在函数内部调用 dispatch 函数发布该事件,然后会触发 effects 联动逻辑中的相应处理函数,这就是表单联动逻辑的基础
- handler2 监听 onFormWillInit 事件,在表单对象创建完成后会触发该事件,在 handler2 中会调用 implementActions(...formApi) 方法来将所有的 api 都挂载在 actions 对象上,此时用户就可以通过 actions 方法来调用这些 API 了
- 注册字段对象:每个表单控件都对应一个字段对象,这里添加 username 和 password 两个 field 对象。同时通过 createMutators 方法为每个字段创建一个操作对象 mutators
状态管理
在上述例子中可以看到,对每个受控的表单控件,其值由字段对象的的 state.value 提供,同时在表单控件的值改变时,会调用 mutators 对象提供的 change, blur, focus 等方法修改字段对象的状态,如下:
同时对于一个字段来说,它的值即保存在 field 对象中,同时也保存在 form 对象中。如下:username 字段中输入 1234, password 字段中输入 abc,这时候,form 中的状态值如下,它的 values 中也同步保存着字段的 value,所以这里就涉及到字段和表单对象的值同步,其原理可以看上述核心 API 中的 setFieldState/getFieldState 的同步机制
数据校验
在创建字段操作函数的时候,会在字段对象上会添加一个监听函数,在该监听函数中会调用 mutators.validate 校验字段的值(在 validate 函数内部最终还是通过闭包变量 validator 对节点进行校验,并输出错误信息)
每当 field 字段的值变更的时候,通过 field.notify 触发该监听该函数(使用 Subscribable 类的订阅/监听的能力),并会将错误结果同步到 form 对象的状态中
除了字段的校验,在表单提交的时候,也会对表单对象 form 进行校验。对 form 对象的校验本质就是对其内部的所有子节点进行校验,在校验时,传递 path = '' 即可校验所有子节点:
联动逻辑的实现
在 formily 中,提供了一种优雅的方式来处理字段之间的联动逻辑。如下:通过在 effects 函数中监听对应的生命周期来实现具体的联动逻辑。通过 onFormInit 监听 username 字段的输入,并在输入为 123 的时候,隐藏 password 字段
<Form
actions={actions}
effects={() => {
onFormInit$().subscribe(() => {
console.log('initialized')
})
onFieldInputChange$('username').subscribe(({ value }) => {
actions.setFieldState('password', state => {
state.visible = value === '123' ? false : true;
});
})
}}
>
...
</Form>
下面来讲述下在 formily 中的联动逻辑的实现。这里为了方便说明,我们先使用如下等效的写法来阐述其工作原理:
<Form
actions={actions}
effects={($, actions) => {
$('onFormInit').subscribe(() => {
console.log('initialized')
})
$('onFieldInputChange', 'username').subscribe(({ value }) => {
actions.setFieldState('password', state => {
state.visible = value === '123' ? false : true;
});
})
}}
>
...
</Form>
在表单组件初始化的时候,在 createEva 闭包中添加对 onFormInit、onFieldInputChange 的监听事件(fn1 和 fn2,具体的添加过程见"分布式状态管理框架 react-eva"),同时会在生命周期对象 heart 中添加对“所有生命周期”和 “onFormWillInit”这两个事件的监听(handler1 和 handler2)
当表单初始化完成后,会触发 onFormWillInit 事件,此时会调用 handler2,也就是调用 implementActions 来将所有 API 挂载到 actions 上
当表单发布任何生命周期事件时都会调用 handler1 函数,在函数内部会调用 dispatch 函数发布该类型的生命周期事件,然后就会从 effects 中添加的生命周期钩子函数中(即 createEva 闭包中的 subscribes 变量保存的生命周期钩子函数)寻找对应的监听函数并运行。这就是联动逻辑的运行机制
生命周期函数的监听 hooks
在 effects 中还提供了快捷的生命周期监听函数,如下:左边是我们原先的写法,右边就是使用快捷的生命周期钩子函数进行监听。两者实现的功能是一样的,但后者更简洁
接下来我们说一下上图右边的实现原理。实现代码如下:从代码中可以看到这些钩子函数都是由 createEffectHook 生成的闭包,在这些闭包中最终会调用 env.effectSelector 来添加监听事件
在运行 effects 函数时,其实两种写法都会先创建 env.effectSelector 函数,然后调用它进行事件的监听,不同的是,左边的写法中就直接调用 env.effectSelect 进行监听,右边则是通过闭包返回的函数中调用 env.effectSelector 进行监听。所以这两者其实是等效的。下面是使用两种方式监听的运行过程比对:
表单提交
在提交的过程中,会先进行表单的校验(本质是校验内部所有的节点),若校验失败则直接退出。校验通过后,则返回表单的状态(详情见核心 API 中的 submit 方法)
避坑指南
这里我们来说下在使用 formily 的过程中容易遇到的一些问题,以及背后的工作原理
actions 中的缓存问题
在文章一开始的背景部分中提到的那个 actions 的问题其实就是 actions 的缓存问题。在 formily 初始化的时候,Form 组件内部 actionsRef.current 为空,所以会使用传外部传入的 props.actions 或者自己新建一个 actions,但在组件更新时则会优先使用 actionsRefs.current,也就是上一次的 actions 对象,但在顶层组件中,actions 又是一个新的对象,所以这时候外部的 actions 和 Form 组件内部的 actions 已经不是同一个对象了
解决方法:
使用 useMemo 来确保 actions 对象只被创建一次
备注:虽然 formily 官网例子中都是用全局的 actions 方式,同样也可以避免这个问题,但并不推荐全局创建,一是因为会破坏组件的封装行,而是如果组件多次引入时,全局创建的方式也会有问题
异步actions vs 同步actions
formily 中提供了两种创建 actions 的方式:createAsyncFormActions、createFormActions。前者创建一个异步的 actions ,后者创建一个同步的 actions。但这里的异步 actions 和同步 actions 跟直观感觉上的异步/同步有点区别。比如对于同步的 actions,你依然可以用异步的方式进行调用,如下:
为了说明这个问题,我们可以看下 actions 的具体实现,如下:对于异步方法创建的 actions,会在最外层包裹一个 Promise 对象,从而确保可以异步调用,但对于同步方法创建的 actions,调用时则直接返回 actions 中对应的方法,如果此时该方法本身就是一个异步方法,那么它也可以采用异步调用的方式的。而上面例子中的 submit 方法本身就是一个异步方法,所以这也就解释了上面也能正常工作的原因
备注:为了使用的统一,这里还是建议使用 createAsyncFormActions 来创建 actions 对象
effects 中的缓存问题
在 effects 中也会遇到一些问题,比如如果用户在 effects 中使用外部的 state 变量,只会获取到最初的值,而不会获取到最新的值。如下,我们在提交函数中更新 msg 值后,此时 effects 中获取的 msg 还是最初值 'hello'
上述问题的原因在于 react-eva 初始化的时候,内部使用了 useMemo 来添加监听该函数,所以 effects 只会被运行一次,所以即使外部组件更新,effects 函数并不会重新运行,所以其内部的变量值也不会改变
注意:虽然 effects 中的变量值不会改变,但通过 actions.getFieldState 等 actions 的方法依然可以获取到字段最新的状态,这是因为虽然 actions 对象不会改变,但 actions.getFieldState 是实时返回字段的状态,所以不受影响
onFieldInputChange
formily 提供了 onFieldInputChange 和 onFieldValueChange 这两个不同的生命周期事件,都代表字段的状态修改,但前者表示是人为操作表单控件带来的值变化,不包含间接联动(也就是直接调用 actions.setFieldState 产生的状态修改)
这两者的区别在上面“createMutators”中有提到,当直接调用 actions.setFieldState 时,只会发布 onFieldValueChange 的生命周期事件,而调用 mutators.change 时,会发布 onFieldInputChange 和 onFieldValueChange 这两个生命周期事件,在人为操作中正是调用 mutators.change 方法来修改状态的,如下:
visible vs display
visible 和 display 都可以控制字段的显示和隐藏,但他们在提交的时候的行为是不一样的。visible = false 的情况下,提交时对应字段值为 undefined。如下:
display = false 的情况下,提交的字段值还是存在,如下:
之所以有上述的差异,在于每次字段的更改后,会通过 produce 钩子去处理(字段状态变更的详细过程见“基础数据结构中的 Model”),当进行 state.visible 的变更时,代码中会自动将 value 的值设置为 undefined。如下: