深入React,做一个高阶练习生

智能合约
• 阅读 2613

从很久以前说起

很久以前,一个前端练习生,开源了一个不知名的组件库antd-doddle,还特意写了接入文档,文档地址, 真的是用做开源的态度,在认认真真做项目。这个组件库在公司内部差不多用了一年多,感觉还是把日常哪些简单琐碎的事简化了,有更多的时间去成长。这不,最近有点成长,又有点闲心,就用更React的思想重构了这个组件库中最重要的一个组件FormRender,为了保持兼容性,重构后的组件叫FormGroup

关联库地址: https://github.com/closertb/a...

关联文档地址: http://doc.closertb.site/guid...

先看看对比

下面是要实现的效果图
深入React,做一个高阶练习生
下图是老组件与重构后组件实现上面一个表单的代码对比图:红色部分为差异(伪代码)
深入React,做一个高阶练习生
重构后的组件在多写了一个表单项的基础上,两个组件的代码量看起来是差不多的。老组件必须在Class组件上用,新的可以直接在函数组件跟着Hooks一起用。从示例也可以看出两个在FormRender表单项逻辑编写这一块是一致的,差异表现在getFieldDecorator的绑定时机。
从React的思路来看,重构后的组件更像是React组件。

实现思路

重构组件的实现,其实就和两个React Api相关:

  • React.Children
  • React.cloneElement

先做一个初显得了解;

关于React.cloneElement

老规矩,首先看官方文档
API: 与createElement传参一致

React.cloneElement(
  type,
  [props],
  [...children]
)
以 element 元素为样板克隆并返回新的 React 元素。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。新的子元素将取代现有的子元素,而来自原始元素的 key 和 ref 将被保留。React.cloneElement() 几乎等同于:
<element.type {...element.props} {...props}>{children}</element.type>

但特别是:

使用cloneElement克隆的组件,会保留组件的 ref。这意味着当通过 ref 获取子节点时,你将不会意外地从你祖先节点上窃取它。相同的 ref 将添加到克隆后的新元素中。

虽然cloneElement在我们日常开发中不常用,但在组件库中,他是一个常客,说一个可能都见过但实现不为人熟知的API:getFieldDecorator,他的使用方式如下:

<FormItem label="原生组件" {...formItemLayout} >
  {getFieldDecorator的实现,其实是依赖('self', {
    initialValue: 'self name',
  })(
    <Input
      type="text"
    />
  )}
</FormItem>

你可能和我当时第一感觉相像,这个API是个高阶组件(函数):意指输入一个组件,返回一个修饰后的组件。但再仔细想想:不要在 render 方法中使用 HOC(来自官方文档),具体为啥:就是高阶组件返回的组件和输入组件指向的不是同一个地址,这在react中最忌讳的,意味每次render都得重新卸载,再挂载一次,耗费性能不说,状态还不能保持。所以getFieldDecorator的实现,其实是依赖cloneElement实现的。

关于React.Children

老规矩,首先看官方文档,每一个父节点,都有自己的子节点。在react中,父节点可以通过props.children得到自己的子元素。可能有人会为props.children可以为哪些类型?
接下来,举例说明:

 <div>
  <h3><div>这是个标题</div></h3>
  <FormGroup {...formProps}>
    <Row>
      {editFields.map(field=> <FormRender key={field.key} {...{ field, data }} />)}
      <Col span={12}>
        <FormItem label="原生组件" {...formItemLayout} >
          {getFieldDecorator('self', {
            initialValue: 'self name',
          })(
            <Input
              type="text"
            />
          )}
        </FormItem>
      </Col>
    </Row>
  </FormGroup>
  <div style={{ textAlign: 'center' }}>
    <Button onClick={handleSubmit}>提交</Button>
    <span style={{ marginLeft: 20 }}>文字</span>
    <span style={{ marginLeft: 20 }}>{3}</span>
    {false && <span style={{ marginLeft: 20 }}>3</span>}
    {null && <span style={{ marginLeft: 20 }}>3</span>}
    <span style={{ marginLeft: 20 }}>{undefined}</span>
  </div>
</div>

经过实践,得到一个结论:任何类型

  • Array: 数组,这是最多见的,比如这里最外围的div容器;

深入React,做一个高阶练习生

  • Object: 对象是仅此于数组出现频次的,比如这里的h3中的div节点

深入React,做一个高阶练习生

  • String: 字符串类型的节点也是非常常见的
  • Boolean:做条件判断,这个也是常用的
  • Number: 这个也是有的
  • Undefined:这个也是常有的是
  • null:这个是比删除中间节点更好的方式

上面四种基本类型,在上一张图中都有体现
深入React,做一个高阶练习生

React.Children提供了四个方法

  • map
  • forEach
  • count
  • only

这里我们只用到了map,其他三种有兴趣可以看官方文档。而React.Children.map与Array.map方法相似,语法:

React.Children.map(children, function[(thisArg)]);

children的类型没有要求一定是Array, 可以是上面讨论过的任意类型,官方的翻译是这样描述的:

If children is an array it will be traversed and the function will be called for each child in the array. If children is null or undefined, this method will return null or undefined rather than an array

说人话就是:除了null or undefined不会遍历(就是function函数不会被调用),直接返回;其他类型都会被转化成一个数组,并被遍历;

FormGroup的实现

FormGroup的出现,就是要提前收集Form的一些公共参数,比如getFieldDecorator,Layout,required这些,并把这些特性传递给FormRender,这样使用时,就不用每个FormRender都去传一遍这些相同的属性。基于前面的铺垫,现在想想FormGroup的实现,是不是特别简单,分三步:

  • 拦截children,
  • 识别childer中的FormRender组件,并将公共属性传递给他
  • 返回修饰后的children

看代码实现:

// 深度优先遍历 react children, 识别FormRender
function deepMap(children, extendProps) {
  return Children
    .map(children, (child) => {
      // 防止null,undefined,boolean
      if (!child) {
        return child;
      }
      const isDefine = typeof child.type === 'function';
      // 仅对FormRender组件做属性扩展, extendSymbol就是唯一身份识别
      if (isDefine && child.type.$type === extendSymbol) {
        return cloneElement(child, { extendProps });
      }
      if (child && child.props && child.props.children && typeof child.props.children === 'object') {
        return cloneElement(child, {
          children: deepMap(child.props.children, extendProps),
        });
      }
      return child;
    });
}

export default function FormGroup(constProps: GroupProps) {
  const { formItemLayout = layout, containerName, getFieldDecorator, required,
    Wrapper = WrapperDefault, withWrap = false, children, dynamicParams, ...others } = constProps;
  
  const extendProps = {
    formItemLayout,
    containerName,
    dynamicParams,
    getFieldDecorator,
    require: required,
    Wrapper,
    withWrap
  };

  return (
    <Form {...others}>
      {deepMap(children, extendProps)}
    </Form>);
}

FormGroup.FormRender = FormRender;

总结

至此,这个优化涉及到的知识就说完了.但是有些自己踩的坑还是要记录一下。

使用function.name做唯一识别

为了有针对性的做属性扩展,我开始写了这样一段代码:

if (isDefine && child.type.name === 'FormRender') {
  return cloneElement(child, { extendProps });
}

在本地跑demo测试时,代码正常,但打正式环境包运行时,FormRender报'can't read getFieldDecorator of undefined',最后排查,发现是代码压缩造成function.name被重命名;所以后面调整了方案,在全局做了一个extendSymbol唯一标识;

export const extendSymbol = Symbol('extend');

// 在FormRender.ts中
FormRender.$type = extendSymbol;

没有搞清楚props.chilren类型

为什么我在讲React.Children时,要特意提到props.chilren有哪些类型,因为我在做深度遍历时吃过亏,虽然React.Children(children)可以遍历任意类型,但child的类型也是任意的,所以有下面这些写法,对可选链语法甚是期待:

  // 防止null,undefined,boolean
  if (!child) {
    return child;
  }

  if (child && child.props && child.props.children && typeof child.props.children === 'object')

原文地址: https://github.com/closertb/c...

附赠一个简单的组件实现用例:Tab实现

import React, { useState, Children, cloneElement, useCallback } from 'react';

Tab.TabPane = ({ children, show = false }) => {
  console.log('show', show);
  return show ? children : null;
}

export default function Tab({ defaultValue, children }) {
  const [status, setStatus] = useState(defaultValue);
  const maps = new Map();
  const cloneChildren = Children.map(children, (child) => {
    const { value, name } = child.props;
    const show = value === status;
    maps.set(value, { value, name, show });
    return cloneElement(child, { show });
  });

  const handleCheck = useCallback((pre) => () => {
    setStatus(pre);
  }, []);

  console.log('map', Array.from(maps));
  return (
    <div className="tab-container">
      <ul className="tab-header">
          {Array.from(maps).map(([value, { name } ]) => (
            <li key={value} className={ value === status ? 'checked' : '' }>
              <a onClick={handleCheck(value)}>{name}</a>
            </li>
          ))}
      </ul>
      <div className="tab-content">
        {cloneChildren}
      </div>
    </div>
  );
}

使用:

// 伪代码
import React, { useState, useCallback } from 'react';
import style from './index.less';
import Tab from './Tab';

const { TabPane } = Tab;

function Component />({ con }) {
    return <div>{con}</div>;
}
const CMap = [
  ['水平垂直居中', <Component con="水平垂直居中" />],
  ['定位布局', <Component con="定位布局" />],
  ['层叠上下文', <Component con="层叠上下文" />],
];


export default function StyleTest() {
  return (
    <div className={style.wrapper}>
      <Tab defaultValue={0}>
        {CMap.map(([name, Component], index) => (
          <TabPane value={index} name={name} key={index}>
            <Component />
          </TabPane>
        ))}
      </Tab>
    </div>
  );
}
点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
4年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
4年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Easter79 Easter79
4年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
4年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
4年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Python进阶者 Python进阶者
2年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
智能合约
智能合约
Lv1
等待着大雪的纷纷扬扬,把每一个梦筑成洁白的巢。
文章
4
粉丝
0
获赞
0