Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Formik 引发的Out of Memory异常 #280

Open
yaofly2012 opened this issue Jun 5, 2023 · 0 comments
Open

Formik 引发的Out of Memory异常 #280

yaofly2012 opened this issue Jun 5, 2023 · 0 comments
Labels
form 表单相关 源码

Comments

@yaofly2012
Copy link
Owner

yaofly2012 commented Jun 5, 2023

背景

测试反馈页面在录入信息时很卡,甚至浏览器偶尔还会crash。
image

原因

利用React Dev Tools发现values.touched属性值有点问题:
image
居然是个长度很大的数组,但我期望的结构是{ "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_VALUESET_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)

何不让开发指定数字命名的属性是属于数组还是对象呢?比如采用约定大于配置的原则这样规定:

  1. a[123].age 则表示a是个数组
  2. a.123.age 则表示a是个对象

参考

  1. Formik Issues filter by is:issue is:open Numeric
  2. Numeric Keys in field names cause formik to crash the browser #3701
  3. "JavaScript heap out of memory" when trying to create large Array
  4. How to Solve Heap Out Of Memory Error in JavaScript?
@yaofly2012 yaofly2012 added form 表单相关 源码 labels Jun 5, 2023
@yaofly2012 yaofly2012 changed the title Formik 引发的内存不足异常 Formik 引发的Out of Memory异常 Jun 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
form 表单相关 源码
Projects
None yet
Development

No branches or pull requests

1 participant