在 Antd-From 组件表单域 onChange 回调中 setFieldsValue 修改自身表单域 value 无效的问题

Posted by Harry on 2019-12-22
Words 1.3k and Reading Time 4 Minutes
Viewed Times

现象与问题

如题,我们先来看看以下两个例子。
例子一:给 fieldKeyusername 的输入框输入字母,将其转化成大写字母显示出来(通过 setFieldsValue)。实际操作的时候,我对另外一个 fieldKeynickname 的输入框也进行了赋值,以验证仅对当前操作表单域赋值失败的问题。

输出结果证实:当通过 fieldKeyusername 的输入框 onChange 回调方法给自身表单域设置值的时候是无效的,而不影响与之不同的表单域 nickname 的赋值。

例子二:跟例子一是差不多的代码,主要的区别是我们在 onChange 回调中尝试引入 setTimeout(func, 0)[1] 这种非同步的方式来调用 setFieldsValue。这么做的原因主要是,Antd-Form 组件本身是将一个个输入控件包裹一层再通过 setState 的机制来使输入控件变为受控组件。组件在 onChange 回调的时候一般是 Form 组件收集最新 value 的时机,所以一般在这个回调 Form 组件会根据情况进行 setState,再加上我们在 onChange 中又调用了一遍 setFieldsValue,所以我们这里可以猜测的结果是:Form 组件在调用了我们的 onChange 后,再调用自己内部的 setFieldsValue 来同步页面数据,这样的话我们执行的 setFieldsValue 就相当于被覆盖掉了。

输出结果证实:当我们通过 setTimeout(func, 0)setFieldsValue 作为异步回调逻辑放到队列当中,那么可以确保我们的 setFieldsValue 会被有效的执行(不会发生覆盖),这样执行结果也跟我们预期的一致了。但这样的方式带来的坏处也显而易见,就是修改一次会触发两次 render

注:此类问题对应我们公司 React 项目中自定义的 InputGroup 组件的一些联动设置值的问题。

阅读源码一探究竟

在上一部分中,我们对 Antd-Form 组件的行为进行猜测,但具体是不是这样,还需要从源码中得知,毕竟在我们公司项目中碰到此类问题还不少,觉得是时候深究一番了。

众所周知,Antd 中大部分组件的主要实现来自于 react-component 这个组(也是来自于阿里巴巴的前端工程师之手), Antd 将组件 api 进行定制以及优化后给到开发者进行开发。我们要找 Antd-Form 源码实则应该找的是 react-component/form 这个项目的源码。

首先,我们可以找到几个核心 api 的实现在 createBaseForm.js 这个文件里。近而,我们可以发现以下几个非常重要的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 对应到我们例子一的话,这里的三个参数分别是:'username', 'onChange', event
onCollectCommon(name, action, args) {
const fieldMeta = this.fieldsStore.getFieldMeta(name);
if (fieldMeta[action]) {
// 相当于执行了 onChange(...args),这里会触发 setFieldsValue 操作
fieldMeta[action](...args);
} ...
...
},

// 当有新的状态变化时会触发 Form 组件重新收集值
onCollect(name_, action, ...args) {
// 对 username 的输入做出响应 this.onCollectCommon('username', 'onChange', args)
// 此处先执行了用户自定义的 onChange 带件,即先调用调用了用户自定义的 setFieldsValue
const { name, field, fieldMeta } = this.onCollectCommon(name_, action, args);
...
// 更新 fieldStore 中 username 的值,并执行 forceUpdate
// 此为后续调用,即用户自定义的会被后调用的这个 setFields({ username: newField }) 覆盖
this.setFields({ [name]: newField });
},

setFieldsValue(changedValues, callback) {
...
// 更新 fieldStore 中 username 的值,并执行 forceUpdate
this.setFields(newFields, callback);
...
},

setFields(maybeNestedFields, callback) {
...
// forceUpdate 类似于 setState,同步代码段里多个 forceUpdate 也会被优化最终只 render 一次
this.forceUpdate(callback);
},

看了源代码后,发现与一开始猜测的八九不离十,只是官方很巧妙地用了 forceUpdate 避开了不必要生命周期调用这点没有预料到,好在 forceUpdatesetState 的合并行为是一致的。

最终,我们可以确定,在 Antd-From 组件表单域 onChange 回调中 setFieldsValue 修改自身表单域 value 无效的问题是因为用户自定义的 setFieldsValue 先于 createBaseForm.js 中同步状态值的 setFields调用,导致用户对于同个 setFields 的修改并不生效

更优雅的解决方案

那有没有对于此类问题推荐的解决方案呢,其实是有的,如果我们认真阅读 Antd-From 组件文档,可以发现如下 api:

1
2
参数  |  说明  |  类型
options.getValueFromEvent | 可以把 onChange 的参数(如 event)转化为控件的值 | function(..args)

例子三:使用 options.getValueFromEvent 不仅简化了代码,还确保了不会触发二次 render。

Reference

[1] “如果以0毫秒的超时时间来调用setTimeout(),那么指定的函数不会立刻执行。相反,会把它放到队列中,等到前面处于等待状态的事件处理程序全部执行完成后,再“立即”调用它。”
摘录来自: (美)David Flanagan. “JavaScript权威指南(原书第6版)”。 iBooks.


This is copyright.