yaofly2012 / note

Personal blog

Home Page:https://github.com/yaofly2012/note/issues

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Formik 引发的Out of Memory异常

yaofly2012 opened this issue · comments

commented

背景

测试反馈页面在录入信息时很卡,甚至浏览器偶尔还会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?