1. 使用方法
React.Children.map(children, function[(thisArg)])
2. 方法解释
在 children 里的每个直接子节点上调用一个函数,并将 this 设置为 thisArg。如果 children 是一个数组,它将被遍历并为数组中的每个子节点调用该函数。如果子节点为 null 或是 undefined,则此方法将返回 null 或是 undefined,而不会返回数组。
3. 注意
如果 children 是一个 Fragment 对象,它将被视为单一子节点的情况处理,而不会被遍历。
4. React.Children.map 源码
4.1 mapChildren 源码
\packages\react\src\ReactChildren.js
function mapChildren(
children: ?ReactNodeList,
func: MapFunc,
context: mixed,
): ?Array<React$Node> {
// 判断传入的子元素列表是否是 null 或 undefined,条件成立,直接返回 children
if (children == null) {
// $FlowFixMe limitation refining abstract types in Flow
return children;
}
// 存储处理后的 child 列表
const result: Array<React$Node> = [];
// 每一个 child 的索引
let count = 0;
// 将 children 中的每个元素映射到数组 result 中,并在映射过程中应用 func 函数
mapIntoArray(children, result, '', '', function (child) {
// 调用映射函数处理 child,并返回处理结果
return func.call(context, child, count++);
});
return result;
}
4.2 mapChildren 函数解释
- children 是任意类型的子元素列表。
- func 操作子元素的映射函数,接受两个参数:子元素和索引,并返回一个映射后的结果。
- context 执行 func 映射函数的上下文。
- 检查 children 是否为 null 或 undefined,如果是,则直接返回 children。
- 创建一个名为 result 的空数组,用于存储处理后的子元素。
- 初始化一个计数器 count,用于记录当前处理的子元素的索引。
- 使用 mapIntoArray 函数遍历 children,并将每个子元素转换为数组形式存储在 result 中。mapIntoArray 函数还接受一个回调函数,该回调函数会调用 func 函数,并传入当前子元素和计数器的值。
- 返回处理后的 result 数组。
4.3 mapIntoArray 函数的实现
function mapIntoArray(
children: ?ReactNodeList,
array: Array<React$Node>,
escapedPrefix: string,
nameSoFar: string,
callback: (?React$Node) => ?ReactNodeList,
): number {
// 使用 typeof 获取 children 的类型
const type = typeof children;
// 判断 type 是否是 undefined 或者 boolean,跳转成立,将 children 赋值 null
if (type === 'undefined' || type === 'boolean') {
// All of the above are perceived as null.
children = null;
}
// 是否调用回调函数标记
let invokeCallback = false;
// 如果 children 是 null,则调用回调
if (children === null) {
invokeCallback = true;
} else {
// 判断 type 是 bigint、string、number 直接调用回调
switch (type) {
case 'bigint':
case 'string':
case 'number':
invokeCallback = true;
break;
case 'object':
// 如果 type 是 object,继续判断 $$typeof 的值
switch ((children: any).$$typeof) {
// 如果 $$typeof 是 REACT_ELEMENT_TYPE、REACT_PORTAL_TYPE直接进入回调
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
invokeCallback = true;
break;
// 如果 $$typeof 是 REACT_LAZY_TYPE 进行递归调用
case REACT_LAZY_TYPE:
const payload = (children: any)._payload;
const init = (children: any)._init;
return mapIntoArray(
init(payload),
array,
escapedPrefix,
nameSoFar,
callback,
);
}
}
}
// 如果 invokeCallback 是 true,直接调用回调
if (invokeCallback) {
const child = children;
// 使用回调函数处理 child,然后赋值给 mappedChild
let mappedChild = callback(child);
// If it's the only child, treat the name as if it was wrapped in an array
// so that it's consistent if the number of children grows:
// 获取子元素的key,如果 nameSoFar 是空,直接使用 SEPARATOR + getElementKey(child, 0) 否则使用 nameSoFar
const childKey =
nameSoFar === '' ? SEPARATOR + getElementKey(child, 0) : nameSoFar;
// 判断处理后的子元素是否是数组【Array.isArray】
if (isArray(mappedChild)) {
let escapedChildKey = '';
// 如果 childKey 不为 null,则将 childKey 的单/替换为//,结果赋值给 escapedChildKey
if (childKey != null) {
escapedChildKey = escapeUserProvidedKey(childKey) + '/';
}
// 如果是数组,递归调用 mapIntoArray
mapIntoArray(mappedChild, array, escapedChildKey, '', c => c);
} else if (mappedChild != null) {
// 如果是 react 元素
if (isValidElement(mappedChild)) {
// 复制一份新的子元素
const newChild = cloneAndReplaceKey(
mappedChild,
// Keep both the (mapped) and old keys if they differ, just as
// traverseAllChildren used to do for objects as children
escapedPrefix +
// $FlowFixMe[incompatible-type] Flow incorrectly thinks React.Portal doesn't have a key
(mappedChild.key != null &&
(!child || child.key !== mappedChild.key)
? escapeUserProvidedKey(
// $FlowFixMe[unsafe-addition]
'' + mappedChild.key, // eslint-disable-line react-internal/safe-string-coercion
) + '/'
: '') +
childKey,
);
mappedChild = newChild;
}
// 将子元素放入存储的数组中
array.push(mappedChild);
}
return 1;
}
let child;
let nextName;
let subtreeCount = 0; // Count of children found in the current subtree.
const nextNamePrefix =
nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
// 如果 invokeCallback 为 false,
// children 是否为数组
if (isArray(children)) {
// 遍历 children 中每个元素,调用 mapIntoArray 函数,并将返回的子树计数累加到 subtreeCount 中
for (let i = 0; i < children.length; i++) {
child = children[i];
nextName = nextNamePrefix + getElementKey(child, i);
subtreeCount += mapIntoArray(
child,
array,
escapedPrefix,
nextName,
callback,
);
}
} else {
// children 是一个可迭代对象,则使用 iteratorFn 函数获取迭代器,并遍历每个元素,调用 mapIntoArray 函数,并将返回的子树计数累加到 subtreeCount 中
const iteratorFn = getIteratorFn(children);
if (typeof iteratorFn === 'function') {
const iterableChildren: Iterable<React$Node> & {
entries: any,
} = (children: any);
const iterator = iteratorFn.call(iterableChildren);
let step;
let ii = 0;
// $FlowFixMe[incompatible-use] `iteratorFn` might return null according to typing.
while (!(step = iterator.next()).done) {
child = step.value;
nextName = nextNamePrefix + getElementKey(child, ii++);
subtreeCount += mapIntoArray(
child,
array,
escapedPrefix,
nextName,
callback,
);
}
} else if (type === 'object') {
// children 是一个对象,则检查其是否有 then 方法,如果有,则将其视为一个 Promise,并递归调用 mapIntoArray 函数
if (typeof (children: any).then === 'function') {
return mapIntoArray(
resolveThenable((children: any)),
array,
escapedPrefix,
nameSoFar,
callback,
);
}
// children 是一个对象,但没有 then 方法,则将其转换为字符串,并抛出一个错误,因为对象不能作为 React 子元素
// eslint-disable-next-line react-internal/safe-string-coercion
const childrenString = String((children: any));
throw new Error(
`Objects are not valid as a React child (found: ${
childrenString === '[object Object]'
? 'object with keys {' +
Object.keys((children: any)).join(', ') +
'}'
: childrenString
}). ` +
'If you meant to render a collection of children, use an array ' +
'instead.',
);
}
}
// 返回 subtreeCount,表示处理的子元素总数
return subtreeCount;
}
4.4 总结
目前是第一次看这个函数的实现,知道他大概干了什么,也大概明白每一步要干什么,但是目前还不清除他为什么要这么干。为什么要来看他的实现呢?一个是只有知道他是怎么实现的,才能知道在调用这个方法时,那些情况会报错,为什么报错,能够快速处理。使用这个方法开发功能的时候能够更加得心应手。
5. React.Children.map 应用
- swiper 组件的开发,或者说类 swiper 组件的开发,都需要在 swiper 组件内部获取他的子元素,然后再再子元素的外层添加一个盒子,然后在盒子上实现我们需要的动画效果,比如渐入渐出、放大缩小,滑动等动画效果;
- 目前想到可以使用的就是瀑布流展示效果,动态获取每一列的高度,然后在动态的分配下一个子元素在那一列展示;
- 其他的应用应该还有很多,只是我在开发中遇到的比较少,以后有新的想法,再来补充!
6. 实现渐入动画效果的轮播效果
7. 场景
- css 效果实现:可以看到效果图中就是一个图片从小到大的切换动画效果,这个效果很简单,使用 css 的 transform 的 scale 来实现图片的从小到大的效果,切换就更加简单了,不管是 opacity 还是 visibility 都可以实现图片的隐藏和显示的切换。
- React.Children.map :使用 React.Children.map 获取子元素,然后给子元素添加一层,在添加这一层实现 css 的动画效果。
8. 获取子元素列表和数量
8.1 实现分析
- 申明一个子元素计数器 childCount;
- 调用 React.Children.map 函数对 props.children 进行处理;
- 调用 React.isValidElement 判断是否是 react 元素,不是就直接返回 null;
- 是就计数器 childCount 累加,同时返回子元素;
- 最后返回处理后的子元素列表和子元素数量。
8.2 实现代码
const { childs, childCount } = useMemo(() => {
let childCount = 0;
const childs = React.Children.map(props.children, (child) => {
if (!React.isValidElement(child)) return null;
childCount++;
return child;
})
return {
childs,
childCount,
}
}, [props.children])
9. 切换效果实现
9.1 实现分析
- 判断倒计时的计时器是否存在,如果存在就清除计时器;
- 计时器启动,duration 默认 3s 执行一次;
- 计算当前显示图片的索引;
- 将需要显示图片的索引设置,进行UI渲染;
- 当次动画切换完成,调用监听动画完成事件。
9.2 实现代码
// 切换下一张图片
const changeImage = () => {
if(timer.current){
clearTimeout(timer.current)
}
timer.current = setTimeout(() => {
let value = ++current % childCount;
setCurrent(value)
props?.onChange?.(value)
changeImage()
},duration)
}
10. 触发动画
10.1 实现分析
- 监听 props.children 是否发生改变;
- 发生改变时,设置展示默认索引的图片;
- 判断子元素的数量,如果不大于1就不进行切换;
- 执行动画切换效果函数;
- 当组件卸载时,清除动画切换的计时器。
10.2 实现代码
// 监听数据
useEffect(() => {
setCurrent(defaultValue)
if(childCount > 1){
changeImage()
return () => {
clearTimeout(timer.current)
}
}
},[props.children])
11. HTML 实现
return <View
style={style}
className={api.classNames({
'rui-fade-in-components-content': true,
[className]: className
})}>
{React.Children.map(childs, (child, index) => {
return (
<View
style={style}
className={api.classNames({
"rui-fade-in-item": true,
"rui-active": current === index,
[className]: className
})}
key={index}
>
{child}
</View>
)
})}
</View>
注意:此处循环子组件,还是需要使用 React.Children.map,在子组件外层添加一层动画切换的 div。
12. CSS 实现
// index.scss
.rui-fade-in-components-content{
position: relative;
.rui-fade-in-item:not(:first-child){
position: absolute;
top: 0;
left: 0;
}
.rui-fade-in-item{
transition: transform 0.7s;
opacity: 0;
transform: scale(0.9);
z-index: 0;
&.rui-active{
opacity: 1;
transform: scale(1);
z-index: 1;
}
}
}
注意:position: absolute 会导致内容脱离文档流,因此我在此处的处理是保留第一个子元素在原位置,其他子元素设置定位。为什么要设置定位呢?由于渐入或放大效果都是在统一位置展示图片,因此需要将所有的子元素定位放在一起,但是此处需要注意,如果存在点击事件的话,需要设置 z-index 的层级,当前元素层级比其他元素层级高,这样点击事件才不会出错。
13. RuiFadeIn 的完整代码
// RuiFadeIn.jsx
import { View } from '@tarojs/components';
import api from '@utils/api';
import './index.scss';
import React,{ useEffect, useRef, useState, useMemo } from 'react';
const RuiFadeIn = (props) => {
let { className, style, duration = 3000, defaultValue = 0 } = props;
let timer = useRef(null)
let [current, setCurrent] = useState(defaultValue);
const { childs, childCount } = useMemo(() => {
let childCount = 0;
const childs = React.Children.map(props.children, (child) => {
if (!React.isValidElement(child)) return null;
childCount++;
return child;
})
return {
childs,
childCount,
}
}, [props.children])
// 监听数据
useEffect(() => {
setCurrent(defaultValue)
if(childCount > 1){
changeImage()
return () => {
clearTimeout(timer.current)
}
}
},[props.children])
// 切换下一张图片
const changeImage = () => {
if(timer.current){
clearTimeout(timer.current)
}
timer.current = setTimeout(() => {
let value = ++current % childCount;
setCurrent(value)
props?.onChange?.(value)
changeImage()
},duration)
}
return <View
style={style}
className={api.classNames({
'rui-fade-in-components-content': true,
[className]: className
})}>
{React.Children.map(childs, (child, index) => {
return (
<View
style={style}
className={api.classNames({
"rui-fade-in-item": true,
"rui-active": current === index,
[className]: className
})}
key={index}
>
{child}
</View>
)
})}
</View>
}
export default RuiFadeIn;
14. RuiFadeIn.Item 实现
// RuiFadeInItem.jsx
import { View } from '@tarojs/components';
const RuiFadeInItem = (props) => {
let { style, className } = props;
return <View className={className} style={style} onClick={props?.onClick}>{props?.children}</View>
}
export default RuiFadeInItem;
15. 导出 RuiFadeIn 和 RuiFadeIn.Item
// index.js
import RuiFadeIn from "./RuiFadeIn";
import RuiFadeInItem from "./RuiFadeInItem";
const InnerFadeIn = RuiFadeIn;
InnerFadeIn.Item = RuiFadeInItem;
export default InnerFadeIn;
16. RuiFadeIn 组件封装的文件
17. 使用
return <RuiFadeIn className='rui-card-list-content'>
{
list.map((item, index) => {
return <RuiFadeIn.Item key={item.image}
className='rui-card-list-content'
onClick={handleImage.bind(null, item)}>
<Image src={item.image} className="rui-card-goods-img"></Image>
<View className='rui-line1 rui-card-goods-name'>{item.name}</View>
</RuiFadeIn.Item>
})
}
</RuiFadeIn>
18. 期望效果图
19. 总结
- 组件的实现其实没有太大的难度,就是最近看源码,看到了 React.Children.map 的实现,因此使用这个效果用一下;
- 需要注意的是隐藏显示不要直接操作DOM的显示隐藏,因为DOM的改变会导致图片的重新加载,会出现闪动的效果,这是回流导致的图片重新加载;
- 在组件内渲染子组件的时候依然需要使用 React.Children.map 来循环子组件。