Formik 引发的Out of Memory异常
yaofly2012 opened this issue · comments
背景
测试反馈页面在录入信息时很卡,甚至浏览器偶尔还会crash。
原因
利用React Dev Tools发现values.touched
属性值有点问题:
居然是个长度很大的数组,但我期望的结构是{ "xxxSet": {"53656222": true }}
,即53656222
只是数字命名的对象属性,不是数组的索引。看来得阅读下Formit setFieldTouch
源码看看具体逻辑。
setFieldTouch
内部调用Formik setIn
:
export function setIn(obj: any, path: string, value: any): any {
let res: any = clone(obj); // this keeps inheritance when obj is a class
let resVal: any = res;
let i = 0;
let pathArray = toPath(path);
for (; i < pathArray.length - 1; i++) {
const currentPath: string = pathArray[i];
let currentObj: any = getIn(obj, pathArray.slice(0, i + 1));
if (currentObj && (isObject(currentObj) || Array.isArray(currentObj))) {
resVal = resVal[currentPath] = clone(currentObj);
} else {
const nextPath: string = pathArray[i + 1];
resVal = resVal[currentPath] =
isInteger(nextPath) && Number(nextPath) >= 0 ? [] : {};
}
}
// Return original object if new value is the same as current
if ((i === 0 ? obj : resVal)[pathArray[i]] === value) {
return obj;
}
if (value === undefined) {
delete resVal[pathArray[i]];
} else {
resVal[pathArray[i]] = value;
}
// If the path array has a single element, the loop did not run.
// Deleting on `resVal` had no effect in this scenario, so we delete on the result instead.
if (i === 0 && value === undefined) {
delete res[pathArray[i]];
}
return res;
}
内部依赖了lodash的clone
, toPath
等函数。并且在实现里可以看到如果path
是数字就生产数组了:
} else {
const nextPath: string = pathArray[i + 1];
resVal = resVal[currentPath] =
isInteger(nextPath) && Number(nextPath) >= 0 ? [] : {};
}
}
如果数字命名的属性的数字非常大,则误产生的数组会导致clone
函数很慢,甚至造成out of memery。
setIn
是个Util函数,SET_FIELD_VALUE
和SET_FIELD_ERROR
也会依赖这个函数,他们也是存在类似问题。
解决方案
这个应该是Formik的Bug,好些Issues都些涉及这个问题Filter by is:issue is:open Numeric ,但至今还没解决该问题,只能采用其他方式绕过这个问题了。
解决方案1
那不用数字命名的属性呗,比如给数字命名的属性加个前缀。
解决方案2
在初始化Formit
时手动传initialTouched
对象,即明确touched
的结构,这样也可以绕过setIn
里根据属性名字类型自动生成对象和数组的逻辑resVal = resVal[currentPath] = isInteger(nextPath) && Number(nextPath) >= 0 ? [] : {};
。
比如针对上面提到的问题可以改成这样:
<Formik
initialValues={{
"dataSet": {}
}}
initialTouched={{
"dataSet": {}
}}
initialErrors={{
"dataSet": {}
}}
/>
这样touched.dataSet
的类型就明确是个对象了,Formik不会误生成数组。同样的也同时指定下initialErrors
对象。
注意
这个解决方案存在一个问题。state.values/touched/errors
存在覆盖赋值的场景(如下),此时要保证数字命名的属性结构不变,否则会导致setIn
时无法获取指定的结构。
switch (msg.type) {
case 'SET_VALUES':
return { ...state, values: msg.payload };
case 'SET_TOUCHED':
return { ...state, touched: msg.payload };
case 'SET_ERRORS':
if (isEqual(state.errors, msg.payload)) {
return state;
}
return { ...state, errors: msg.payload };
还是别用这个方案了吧,需要注意的点太多。
解决方案 Next (TODO)
何不让开发指定数字命名的属性是属于数组还是对象呢?比如采用约定大于配置的原则这样规定:
a[123].age
则表示a
是个数组a.123.age
则表示a
是个对象