深入理解React中的props.children
您好,我是沧沧凉凉,是一名前端开发者,目前在掘金、知乎以及个人博客上同步发表一些学习前端时遇到的趣事和知识,欢迎关注。
我们在封装组件的时候通常都会使用到props.children
这个属性,就像我一样,使用了React非常长的时间了,但是从来没有想过去看一下props.children
里面到底有什么。
那么你是否好奇props.children
里面到底是什么,我们直接看一下下面的3种情况,你可能就会对props.children
里面是什么有个大概的印象。
import React from 'react';
function TestComponent({ children }: { children?: React.ReactNode }) {
console.log(children);
return <>{children}</>;
}
export default () => {
return (
<>
<TestComponent />
<TestComponent>这是一个子元素</TestComponent>
<TestComponent>
<div>这是一个子元素</div>
</TestComponent>
<TestComponent>
<div>这是第一个个子元素</div>
<div>这是第二个子元素</div>
</TestComponent>
<TestComponent>
{new Array(3).fill(0).map((item, index) => (
<span key={index}/>
))}
<div>这是第一个个子元素</div>
<div>这是第二个子元素</div>
</TestComponent>
</>
);
};
运行一下代码,可以看到浏览器中会打印出下面的结果:
综上所诉props.children
的数据结构分为下面3种情况
- 当前组件没有子节点数据类型就是
undefined
。 - 有一个子节点数据类型就是
object
。 - 有多个子节点的时候数据类型为
array
。
React提供的方法
看过上面的例子后,就会明白其实props.children
的数据非常的不透明,拥有多种数据结构,如果要自己对props.children
遍历那么就得分别处理这些不同的情况。
而React可能考虑到了这一点,提供了下面的五种方法用于处理这种不透明的数据结构。
React.Children.map
最常用的方法之一,顾名思义,跟数组中的map方法如出一则,会返回一个新的数组。
如果 children
是一个数组,它将被遍历并为数组中的每个子节点调用该函数。如果子节点为 null
或是 undefined
,则此方法将返回 null
或是 undefined
,而不会返回数组。
即使children中只有一个元素,React.Children.map依然会返回一个数组。
该方法可以配合cloneElement
和isValidElement
向被包裹的React子元素中注入props,
import React from 'react';
function TestComponent({ children }: { children?: React.ReactNode }) {
const child = React.Children.map(children, (child) => {
// 是React元素的时候才进行注入
if (React.isValidElement(child))
return React.cloneElement(child, { name: '张三' });
return child;
});
return <>{child}</>;
}
function NameBox({ name }: { name?: string }) {
return <>{name}</>;
}
export default () => {
return (
<>
<TestComponent>
<NameBox />
</TestComponent>
</>
);
};
用React Developer Tools查看该组件中的props值:
注意:这种方法注入的props只会在子组件中,在孙组件中无法完成注入。
<TestComponent>
<NameBox />
<div>
<NameBox />
</div>
</TestComponent>
用React Developer Tools查看嵌套的NameBox组件中的props的值:
Children.forEach
和React.Children.map
一样,但是它不会返回一个新数组。
Children.count
返回 children
中的组件总数量,等同于通过 map
或 forEach
调用回调函数的次数。
Children.only
验证 children
是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误。
如果不是React元素,则也会报错:
import React from 'react';
function TestComponent({ children }: { children?: React.ReactNode }) {
const onlyChildren = React.Children.only(children);
return <>{onlyChildren}</>;
}
export default () => {
return (
<>
<TestComponent>
这是第一个个子元素
</TestComponent>
</>
);
};
Children.toArray
将 children
这个复杂的数据结构以数组的方式扁平展开并返回,并为每个子节点分配一个 key。当你想要在渲染函数中操作子节点的集合时,它会非常实用,特别是当你想要在向下传递 this.props.children
之前对内容重新排序或获取子集时。
React.Children.toArray()
在拉平展开子节点列表时,更改 key 值以保留嵌套数组的语义。也就是说,toArray
会为返回数组中的每个 key 添加前缀,以使得每个元素 key 的范围都限定在此函数入参数组的对象内。
其实从源码上看,该方法内部内部也是调用的这个方法React.Children.map,所以在React.Children.map中同样也会更改key值。
import React from "react";
function TestComponent({children}: { children?: React.ReactNode }) {
const child = React.Children.map(children, item => item);
return <>{child}</>;
}
export default () => {
return (
<>
<TestComponent>
{new Array(3).fill(0).map((item, index) => (
<span key={index}/>
))}
<div>
<div>
这是第一个个子元素
</div>
</div>
<div>这是第二个子元素</div>
</TestComponent>
</>
);
};
在webstorm的调试模式下查看:
源码分析
从源码上看,Children.forEach、Children.count、Children.toArray,这3个方法都是基于Children.map进行实现的,所以在这里仅仅只会分析Children.map是如何进行实现的。
那么React.Children.map到底是如何实现的呢?为什么它能够安全的遍历这种不透明的数据结构:
其实在源码中,map主要是通过mapIntoArray这个方法进行实现的,
function mapIntoArray(
children: ?ReactNodeList,
array: Array<React$Node>,
escapedPrefix: string,
nameSoFar: string,
callback: (?React$Node) => ?ReactNodeList,
): number
我个人觉得mapIntoArray这个函数还是挺繁琐的,有兴趣可以直接看一下源码,大致原理就是mapIntoArray方法内部进行了传入的children参数类型的各种判断,然后根据不同的情况进行递归,这里指的提一下的是,在mapIntoArray中,如果检测到children为React元素,那么会对该元素的key值做一定的处理。
源码里面还有这么一段:
if (children === null) {
invokeCallback = true;
} else {
switch (type) {
case 'string':
case 'number':
invokeCallback = true;
break;
case 'object':
switch (children.$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
invokeCallback = true;
}
}
}
当传入的children类型为object的时候,会去判断children.$$typeof,至于$$typeof这个值具体是干嘛的,我现在还没有深入研究,不过每一个React元素都有这个值。
这里涉及到了React提供的另一个方法createElement,所以这个$$typeof属性我们就放在讲解createElement这个方法的文章中。
最后
在读mapIntoArray源码的过程中,其实我依然有很多不理解的地方,比如$$typeof
这个属性,还有REACT_ELEMENT_TYPE
、REACT_PORTAL_TYPE
这两个常量是干什么,不过我感觉随着我对React的深入研究,这些问题终将在后面会得到解答。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!