【面试问到就是不会系列】React.Children与React.cloneElement杂谈

6,071 阅读5分钟

 介绍俩平常没使用的React API,近日踩雷了,遂借此篇提出来品品...

  首先这俩货同属于React的顶层API,即我们import React from 'react';后,可以通过React.xxx的方式来调用。

  再看官方文档对它们的划分:

  图中的几个API都是对React元素进行操作的,isValidElement就不赘述了,用来校验入参是否是一个合法的React元素,返回一个布尔值。

React.Children

  我们都知道在props对象中还有children这个属性。它能够从某种程度上减少我们在一个组件内的嵌套层级,可能这样描述有点抽象,举个栗子:

// 比如我们有个Modal模态框组件
export default class Modal extends React.Component {
    //...
}

// 有很多场景需要在Modal框内展示子组件的东西,最常见的结构类似下面

<Modal>
    <Content />
</Modal>

// 的确可以在定义Modal的文件内import子组件,但我们这是一个公共的组件,它仅是一个套套,所以通常会使用下面这种方案

render() {
    return (
        <div>
            {this.props.children}
        </div>
    )
}

  这样来说,我们的父组件就和可能传入的children解耦了,各个模块都是独立的,各司其职。更多的关于props.children的语法阐释可以阅读官方文档

  看到这里,我们也发现了一个问题,就是props.children对于我们开发者来说就是一个黑盒,我们对它可能传入的数据结构是不可知的(表达式、布尔、render function等等),如果我们没有对其进行操作,那其实没什么所谓。但只要我们对其进行操作了,比如下意识以为是个数组进行props.children.map这样的调用就要注意,非Array就直接报TypeError了。那怎么处理类似这样的情景呢?

  其实React.Children恰好就是为我们提供处理props.children数据结构能力的API。注意这里React.ChildrenChildren是大写

React.Children.map

  React.Children.map(children, function[(thisArg)])这个类方法能够cover前文我提到的未知数据结构下的遍历问题,只需要简单修改:

React.Children.map(props.children, child => {})

  可以看到这个API接收两个参数,第一个就是我们通常要处理的黑盒prop.children,第二个入参回调,其实就是我们遍历的元素上下文,通过它,我们能够进行定制化的操作。

  笔者结合源码得到当props.childrennullundefined时,最终会原值返回,其余情景则是返回一个数组。

React.Children.forEach

  跟React.Children.map类似,都是迭代操作,只不过这个不会返回数组。undefinednull时的判断逻辑同上。

React.Children.count

  返回其中内部元素数,其值与前面两个迭代方法的回调触发次数相等。

React.Children.only

  用于判断传入的children是否只有一个child。注意接收类型是React element。不能拿React.Children.map()返回的结果再去判断是几个child,因为此时你拿到的已然是一个Array类型。

React.Children.toArray

  这个API会将黑盒的props.children数据结构以扁平的Array结构暴露给我们,如下面这样:

  常用在往下传props时,重新排序或过滤部分children的情景。

React.cloneElement

  有了上面的铺垫,这个API的引入就比较自然了,前文中我们通过React.Children的类方法得到了访问本是黑盒的props.children的能力。React.cloneElement则是能让我们在操作React element时,进行浅层的新props merge,传入的新children则会替换旧的children。原elementkeyref都会保留。

  看下API定义:

React.cloneElement(
  element,
  [props],
  [...children]
)

  其实跟React.createElement的构造有点像:

React.createElement(
  type,
  [props],
  [...children]
)

  毕竟是拷贝返回一个新的组合元素,React.cloneElement处理element时可以大致理解成<element.type {...element.props} {...props}>{children}</element.type>

  那这个API到底有啥用呢?举一个场景:

<Tabs active=''>
    <Tab id='a' title='a'>
        Content: {Math.random()}
    </Tab>
    <Tab id='b' title='b'>
        Content: {Math.random()}
    </Tab>
    <Tab id='c' title='c'>
        Content: {Math.random()}
    </Tab>
</Tabs>

  我希望点击对应Tab的时候,再显示Content信息,并且不再修改以上组件结构(不额外在每个子组件上加onClickprops),实际展示类似下图:

  此时,我们已经了解了前文中介绍的API的能力,大致有两种解决方案,主体思路是一致的,区分在是不是每个子组件都挂一个回调亦或在父组件上挂一个事件代理,去判断。

  这里我使用HOOKS的函数式写法:

// 事件代理
const Tabs = props => {
    const { children, ...rest } = props;
    const [active, setActive] = useState(rest.active);
    let handleClick = e => {
        if (e.target.nodeName === 'A') {
            setActive(e.target.id);
        }
    }
    return (
        <header>
            <nav className={styles.nav}>
                <ul onClick={handleClick}>
                    {
                        React.Children.map(children, child => React.cloneElement(child, {active: active}))
                    }
                </ul>
            </nav>
        </header>
    )
}
// 每一个child 都绑定回调 通过cloneElement传props
const Tabs = props => {
    const { children, ...rest } = props;
    const [active, setActive] = useState(rest.active);
    let toggleActive = (e, id) => {
        e.preventDefault();
        setActive(id);
    }
    return (
        <header>
            <nav className={styles.nav}>
                <ul>
                    {
                        React.Children.map(children, child => React.cloneElement(child, {active: active, toggleActive: toggleActive}))
                    }
                </ul>
            </nav>
        </header>
    )
}

  主体思想都类似,就是把子组件需要的属性和回调函数通过cloneElement的方式merge进去。

  以上DEMO,可借此传送门移步。

小结

  React.Children提供了我们直接访问黑盒props.children数据结构的能力;

  React.cloneElement接收一个React element并支持往其中浅层合并props,替换旧children;笔者看来该API可以从一定程度上减少代码的重复书写,使组件标签表达更加清晰。