antd3 form getFieldDecorator踩坑之路

背景

因为用户体验和功能缺失等原因,产品要对房源发布功能做重构,在做新需求的时候考虑到有些状态和业务逻辑不需要暴露给父组件,所以就使用函数式组件对antd的数据录入组件(Select,Upload等)做了一下封装,注意这里用的是函数组件做的二次封装,入坑的原因也恰恰因为是函数组件,当然这也是后来才知道的。闲话少说,直接上代码:

import React, {useState, forwardRef} from 'react'
import {Form, Input, Button} from 'antd'

const CustomInput = forwardRef(({onChange, value}, ref) => {
  const handleChange = (e) => {
    onChange(e.target.value)
  }
  return (
    
  )
})
const Sub1 = ({form}) => {
  return ({form.getFieldDecorator('jzsHouseName')()})
}
const Sub2 = ({form}) => {
  return ({form.getFieldDecorator('fssHouseName')()})
}
const Super = ({form}) => {
  const [type, setType] = useState(0)
  const onSubmit = (e) => {
    e.preventDefault();
    form.validateFieldsAndScroll((err, values) => {
      console.log(values);
    })
  }
  return (
    
{type === 0 ? () : ()} ) } export default Form.create()(Super);
演示效果.gif

怎么跟我想的不一样.jpeg

问题场景及描述

1、复现场景:antd3 form.getFieldDecorator传入自定义函数组件
2、问题描述:从演示示例的console可以看出,当子组件销毁时绑定到form上的字段并没有清除,如果是必填字段没有被清除,会导致form.validateFieldsAndScroll一直校验不通过,如果是相同字段,会把前一个组件的值带到后一个组件中。当时由于项目工期比较紧张,临时解决方案是在子组件内部创建form实例,这样子组件销毁时,form实例也会销毁,然后父组件onSubmit的时候,通过ref获取子组件form实例。详见:https://3x.ant.design/components/form-cn/#如何在函数组件中拿到-form-实例。多form实例的解决方案,虽然解决了眼下的问题,但是如果子组件内部也有根据不同条件渲染不同组件的情况,还是会出现上面的问题,所以要想从根本上解决问题,只能从源码中找答案了。

填坑

首先clone一份antd源码到本地(调试问题的分支是3.x-stable),在antd3的form源码中,并没有找到getFieldDecorator方法的具体实现,因为antd form的底层依赖了rc-form,所以还需要看一下rc-form的源码。好在安装完依赖后,在node_modules看到的rc-form只是被简单的编译,不会对调试带来影响。后面为了阅读方便,还是直接贴rc-form源码吧。

function saveRef() {

}

function getFieldProps(name, usersFieldOption = {}) {
  // 核心代码
  const inputProps = {
    // ...
    ref: saveRef.bind(this, name),
  };
  return inputProps;
}

function getFieldDecorator(name, fieldOption) {
  const props = getFieldProps(name, fieldOption);
  return fieldElem => {
    // 核心代码
    const decoratedFieldElem = React.cloneElement(fieldElem, {
      ...props,
      ...this.fieldsStore.getFieldValuePropValue(fieldMeta),
    });
    return supportRef(fieldElem) ? (
      decoratedFieldElem
    ) : (
      
        {decoratedFieldElem}
      
    );
  };
}

通过getFieldDecorator方法,我们只能知道在getFieldDecorator闭包内部通过React.cloneElement对传入的组件动态注入了一些props属性,但是还看不出组件销毁后,rc-form是怎么清除多余字段的。因为上文提到getFieldDecorator假如传入的是antd组件,组件销毁了,绑定到form上的字段也会被删除,所以在rc-form某个地方肯定对form上的fields做了删除操作。直接全局搜一下delete关键字,果不其然在clearField方法里面搜到了。既然找到了删除字段的方法,那么在方法里面打个断点调试一下不就知道问题所在了。心动不如行动,首先在antd form => demo文件夹下新建一个markdown文件,把上面有问题的代码贴过去;然后切换切换房源类型,发现没有进断点,因为antd组件是没问题的,我们把CustomInput换成antd
Input,这下断点进来了,并且从断点的调用栈可以看出在saveRef内部调用了clearField;

function saveRef(name, component) {
  // 组件销毁
  if (!component) {
    const fieldMeta = this.fieldsStore.getFieldMeta(name);
    //antd文档:getFieldDecorator(id, options) 参数 option.preserve  即便字段不再使用,
    //也保留该字段的值
    if (!fieldMeta.preserve) {  // 并且preserve不为true
      this.clearField(name);
    }
  }
}

//清除字段
function clearField(name) {
  // 主要的删除逻辑在createFieldsStore.js里的clearField方法中
  // this.fieldsStore.clearField(name); 
  delete this.instances[name];
  delete this.cachedBind[name];
}

我们再回过头来看一下前面的代码,在getFieldProps方法中把saveRef赋值给了inputProps.ref,在getFieldDecorator中通过React.cloneElement把inputProps注入到了我们传入的组件里。然后在组件销毁的时候,react内部会判断ref是否是函数,如果是函数就会执行ref方法。聪明如你,此时肯定知道为什么用antd组件(类组件)没问题而封装后的函数组件会有问题了,没错,问题就出在ref上面。因为我们自定义的函数组件虽然接收了ref参数,但是并没有挂载到dom上,所以在组件销毁的时候不会执行ref方法。现在我们把CustomInput组件的ref挂载到Input上再来看一下效果:


正确效果.gif

ok,现在是我们期望的效果了。这里解释一下为什么没有挂载ref,因为ref对于我们封装的组件来说并没有实际作用,而且当时不太清楚form.getFieldDecorator往我们传入的组件内注入ref的真实用意,antd form文档也没有说需要把ref挂载到函数组件的dom上( 强行甩锅 ),本来想偷个懒,结果懒没偷成还给自己挖了一个坑。

forwardRef拓展

关于函数组件使用ref,react官方给的解释是因为函数组件没有实例,所以不能直接使用ref。我们知道函数组件转发ref需要用到forwardRef,但是forwardRef做了什么以及react内部是怎么把ref从props里面分离出去的,带着好奇心,就去翻了一下react源码,forwardRef相关的核心代码整理如下,感兴趣的同学也可以去读一下react源码。

//forwardRef函数接收函数组件,返回elementType对象
function forwardRef(render) {
  const elementType = {
    $$typeof: REACT_FORWARD_REF_TYPE,
    render,
  };
  return elementType;
}

var RESERVED_PROPS = {
  key: true,
  ref: true,
  __self: true,
  __source: true
};

// Create and return a new ReactElement of the given type
function createElement(type, props, children) { // confi
  var newProps = {};
  var key = null;
  var ref = null;
  var self = null;
  var source = null;

  if (props != null) {
    if (hasValidRef(config)) {
      ref = props.ref;
    }
    if (hasValidKey(config)) {
      key = '' + props.key;
    }
    // Remaining properties are added to a new props object
    for (propName in props) {
      // 给newProps赋值并把ref过滤掉(类组件也会过滤掉ref)
      if (hasOwnProperty$1.call(props, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
        newProps[propName] = props[propName];
      }
    }
  }
  return ReactElement(type, key, ref, newProps);
}

function updateForwardRef(Component, nextProps) {
  var render = Component.render; // Component => forwardRef函数返回的elementType
  var ref = workInProgress.ref; //workInProgress  FiberNode
  renderWithHooks(render, nextProps, ref)
}

function renderWithHooks(render, props, refOrContext) {
  var children = render(props, refOrContext); // 执行render回调 传入props 和 ref 
  return children;
}

总结

之前百思不得其解的问题,在看源码的过程中,慢慢的柳暗花明。现在再来看这个问题,感觉so easy,但是在当时还是给我带来了一些困扰的,以至于后来只能先使用变通方案来保证项目按时上线。只要思想不滑坡,办法总比困难多。对于从源码找答案这件事,我个人感觉还是需要一定技巧的,因为大部分开源框架或者类库代码量都是挺大的,我们没办法在短时间内熟悉作者的设计理念和思想,所以我们要对自己的问题做一下关键息提取(例如上面的问题,我们提取几个关键信息:delete、getFieldDecorator 、componentWillUnmount等),带着这些关键信息去源码中寻找答案,效率可能会更高。

你可能感兴趣的:(antd3 form getFieldDecorator踩坑之路)