Fancy Front End Fancy Front End
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

让有意义的事变得有意思,让有意思的事变得有意义
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • plan 计划
  • 基础

  • 调和(Reconciliation)

  • 调度器(Scheduler)

  • 更新器(Updater)

  • 渲染器(Render)

  • hooks原理

  • 总结

  • React源码漂流记

    • 开始上手
    • Plan 计划
    • 前言
    • React 源码漂流记:ReactElement 与基础概念
    • React 源码漂流记:ReactChildren 与节点操纵
      • 目录
      • 前言
      • 什么是 React.Children?
      • map
      • forEach
      • count
      • toArray
      • only
      • cloneElement
      • 应用
      • 扩展
      • 总结
    • React 源码漂流记:React 整体结构和理念初认识
    • React 源码漂流记:React 调和器核心源码解读(一)
    • React 源码漂流记:React 调和器核心源码解读(二)
    • React 源码漂流记:React 调和器核心源码解读(三)
    • React 源码漂流记:React 调和器核心源码解读(四)
    • React 源码漂流记:React 调和器核心源码解读(五)
    • React 源码漂流记:React 调和器核心源码解读(六)
    • React 源码漂流记:React 调和器核心源码解读(七)
    • React 源码漂流记:React 调和器核心源码解读(八)
    • React 源码漂流记:React 调和器核心源码解读(九)
    • React 源码漂流记:React 调和器核心源码解读(十)
    • React 源码漂流记:React 调度器核心源码解读(一)
    • 带着原理重读 React 官方文档(一)
    • 带着原理重读 React 官方文档(二)
  • react
  • React源码漂流记
jonsam
2022-04-14
目录

React 源码漂流记:ReactChildren 与节点操纵

标签: React17精简

# 目录

  • 目录
  • 前言
    • 学习目标
  • 什么是 React.Children?
  • map
  • forEach
  • count
  • toArray
  • only
  • cloneElement
  • 应用
  • 扩展
    • ChildrenKey 是如何生成的?
    • ReactElement、JSX.Element 和 ReactNode 的区别
    • 原版代码中的池化模式的应用
  • 总结

# 前言

在上一篇文章中,我们分享了 ReactElement 与基础概念 的话题,对 JSX、ReactElement、VDOM、Component 等概念有了一定了解。今天这篇文章,我们来分享跟 ReactNode 相关的 ReactChildren 的工具函数的实现原理。

# 学习目标

  • 学习 ReactChildren 的工具函数的实现原理。
  • 加强对 ReactElement、ReactNode、ReactChildren、组件等概念的理解。
  • 学习使用 ReactChildren 工具函数操纵节点的进阶用法。

# 什么是 React.Children?

React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。

Children 指的就是组件中 props.children 的值。Children 的类型通常是 ReactNode。ReactNode 是组件的渲染模板执行后的结果(ClassComponent 中 render () 函数执行的结果或者 FunctionComponent 执行的结果)。

由此我们就可以知道为什么 ReactChildren 需要使用工具函数来操作了,这是因为:

  • ReactNode 不是数组,ReactNode 的类型很复杂,不能用数组的方法进行遍历、map 等操作。
  • 需要相应的工具函数来提供操作 ReactNode 的能力。

言归正传,从 React 的导出看,React Children 有如下四个方法:

// 操作ReactChildren的方法。ReactChildren不是数组。模拟数组的一些方法。
{ 
  map,
  forEach,
  count,
  toArray,
  only,
}
1
2
3
4
5
6
7
8

下面我们来详细展开这几个函数的源码,了解其实现原理。

# map

map 提供对 ReactChildren 的遍历映射操作。

map 内部由 mapChildren 函数实现。mapChildren 内部调用 mapIntoArray。

function mapChildren(
  children: ?ReactNodeList,
  func: MapFunc,
  context: mixed,
): ?Array<React$Node> {
  if (children == null) {
    return children;
  }
  const result = [];
  let count = 0;
  // 调用 mapIntoArray 遍历 children,并在回调中调用 func,回调的结果放入 result
  mapIntoArray(children, result, '', '', function(child) {
    return func.call(context, child, count++);
  });
  return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这个 mapIntoArray 比较复杂,它所支持的 children 的类型很多,基础类型包括 undefined、boolean、null、string、number 或者标记为 ReactElement 或者 ReactPortal 的 object。如果把基础类型标记为 T 的话,mapIntoArray 还支持 children 的类型为 T [] 或者迭代器 object。 后者是通过循环、迭代实现的,不再赘述,这里我们重点看基础类型。

function mapIntoArray(
  children: ?ReactNodeList,
  array: Array<React$Node>,
  escapedPrefix: string,
  nameSoFar: string,
  callback: (?React$Node) => ?ReactNodeList,
): number {
  // 如果是 undefined、boolean、null、string、number、标记为 object 的 ReactElement 或者 ReactPortal 等基础类型
  // invokeCallback 为 true,执行如下的逻辑
  if (invokeCallback) {
    const child = children;
    // 回调 callback
    let mappedChild = callback(child);
    // 当前层级的 key 的串
    const childKey =
      nameSoFar === '' ? SEPARATOR + getElementKey(child, 0) : nameSoFar;
    // 如果 map 返回是一个数组,则需要对数组继续递归
    if (isArray(mappedChild)) {
      // ......
      // 这里的 callback 是 c => c,说明返回的数组将会被 flat
      mapIntoArray(mappedChild, array, escapedChildKey, '', c => c);
    } else if (mappedChild != null) {
      // 检查 mappedChild 是否是合法的 ReactElement
      if (isValidElement(mappedChild)) {
        // 规整 mappedChild 的 key 值
        mappedChild = cloneAndReplaceKey(
          mappedChild,
          // 计算 mappedChild 的 key
          escapedPrefix +
            (mappedChild.key && (!child || child.key !== mappedChild.key)
              ? escapeUserProvidedKey('' + mappedChild.key) + '/'
              : '') +
            childKey,
        );
      }
      // 将 mappedChild 推入结果数组
      array.push(mappedChild);
    }
    // 返回 count 次数,即 callback 被调用的次数
    return 1;
  }

  // ......

  return subtreeCount;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

总结一下如上的函数:

  • 主要运用了递归、循环、迭代的方法遍历 children,并且执行 callback,如果 callback 返回数组,则继续递归。注意,callback 返回的数组将会被摊平。这主要是因为在渲染节点的时候,节点的嵌套结构是由 ReactElement.props.children 决定的,所有的多维节点数组对会被摊平。
  • 因为 map 的返回结果 result 是一个数组,因此其中的节点都需要设置唯一的 key 值。

# forEach

forEach 提供对 ReactChildren 的遍历操作。

forEach 是由 forEachChildren 实现的。有了上面 mapChildren 的基础,实现 forEach 就顺理成章了。

function forEachChildren(
  children: ?ReactNodeList,
  forEachFunc: ForEachFunc,
  forEachContext: mixed,
): void {
  mapChildren(
    children,
    function() {
      // arguments 表示接受 callback 回调的所有参数
      forEachFunc.apply(this, arguments);
    },
    forEachContext,
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

forEach 只需要在调用 map 时,舍弃 map 的返回值就可以了。

# count

count 计算 children 中的组件总数量。

count 内部由 countChildren 实现的。思路是在遍历过程中计算长度,除非传的是数组或者迭代器,否则返回的长度都是 1。

function countChildren(children: ?ReactNodeList): number {
  let n = 0;
  mapChildren(children, () => {
    n++;
  });
  return n;
}
1
2
3
4
5
6
7

# toArray

toArray 将 children 以数组形式返回。因为 mapChildren 的结果已经是数组了,所以直接返回。

function toArray(children: ?ReactNodeList): Array<React$Node> {
  return mapChildren(children, child => child) || [];
}
1
2
3

# only

only 验证 children 是否是单节点,如果是单节点将之返回,否则报错。

only 内部由 onlyChild 实现。代码如下:

function onlyChild<T>(children: T): T {
  if (!isValidElement(children)) {
    throw new Error(
      'React.Children.only expected to receive a single React element child.',
    );
  }
  return children;
}
1
2
3
4
5
6
7
8

# cloneElement

# 应用

在组件库等复杂的节点运用场景中常常会使用到 React.Children 工具,而且通常会与其他的节点操作 API 如 cloneElement 或者 createElement 一起使用。下面以 antd 代码中 Timeline 组件的一处使用场景作为示例:

// components/timeline/Timeline.tsx
// 对 truthyItems 中的节点进行修改
const items = React.Children.map(
  truthyItems,
  (ele: React.ReactElement<any>, idx) => {
    const pendingClass = idx === itemsCount - 2 ? lastCls : "";
    const readyClass = idx === itemsCount - 1 ? lastCls : "";
    // 克隆原节点并覆盖 className 属性
    return cloneElement(ele, {
      className: classNames([
        ele.props.className,
        !reverse && !!pending ? pendingClass : readyClass,
        getPositionCls(ele, idx),
      ]),
    });
  }
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 扩展

# ChildrenKey 是如何生成的?

从 mapIntoArray 中传递给 cloneAndReplaceKey 的 key 值的计算逻辑:

// key = 
const SEPARATOR = '.';
const SUBSEPARATOR = ':';
// 初始值为 '',用于 mappedChild
const escapedPrefix = escapeUserProvidedKey(childKey) + '/';
const nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
// 初始值为 '',用于数组和迭代器
const nameSoFar = nextNamePrefix + getElementKey(child, i);
const childKey = nameSoFar === '' ? SEPARATOR + getElementKey(child, 0) : nameSoFar;
const key = escapedPrefix +
            (mappedChild.key && (!child || child.key !== mappedChild.key) ? escapeUserProvidedKey('' + mappedChild.key) + '/' : '') +
            childKey;
1
2
3
4
5
6
7
8
9
10
11
12

分成如下几种情况:

  • 如果是 children 是普通类型,key1 = escapeUserProvidedKey (mappedChild.key) + '/' + SEPARATOR + randomKey;
  • 如果是 mappedChild 数组下的 children,key2 = escapeUserProvidedKey (SEPARATOR + randomKey + '/') + '/' + key1;
  • 如果是数组或者迭代器,key3 = escapeUserProvidedKey (mappedChild.key) + '/' + SUBSEPARATOR + randomKey;
  • 如果是数组或者迭代器下的 mappedChild 数组下的 children,key4 = escapeUserProvidedKey (SUBSEPARATOR + randomKey + '/') + '/' + key3;

这里的逻辑比较复杂,我们只需要知道 mapIntoArray 能够为结果数组中的节点生成唯一的 key 值即可。

如何生成 randomKey?

function getElementKey(element: any, index: number): string {
  // 如果组件有 key 值则使用
  if (typeof element === 'object' && element !== null && element.key != null) {
    return escape('' + element.key);
  }
  // 使用 36 进制,即 0-9-a-z。如 (35).toString(36) === 'z'
  return index.toString(36);
}
// 因为 key 中使用了固定的分隔符,所以用户传递的 key 需要 escape 做等意替换,并且添加前缀 `$`
function escape(key: string): string {
  const escapeRegex = /[=:]/g;
  const escaperLookup = {
    '=': '=0',
    ':': '=2',
  };
  const escapedString = key.replace(escapeRegex, function(match) {
    return escaperLookup[match];
  });

  return '$' + escapedString;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

通过 36 进制生成随机字符串是比较常见的一种生成 id、key 标识的方式。例如下面这个示例:

// 生成一个随机小写字母或者数字
const genSeed = () => (~~(Math.random()*36)).toString(36); // "s"

function randomStringGenerator(length){
   let s = '';
   while(s.length < length) s += genSeed();
   return s;
}
randomStringGenerator(6); // "6muky"
1
2
3
4
5
6
7
8
9

# ReactElement、JSX.Element 和 ReactNode 的区别

要直观地理解三者的区别,需要我们从 React 的类型声明文件中去找答案。下面是一些类型:

interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
  type: T;
  props: P;
  key: Key | null;
}
type ReactText = string | number;
type ReactChild = ReactElement | ReactText;
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

namespace JSX {
  interface Element extends React.ReactElement<any, any> { }
}
1
2
3
4
5
6
7
8
9
10
11
12

从这些类型可以看出来,三者是具有包含关系的,其中 ReactNode > JSX.Element > ReactElement 。

  • ReactNode:ReactNode 代表 React 节点,其类型中包含了 ReactChild,ReactChild 中又包含了 ReactElement。
  • JSX.Element:JSX.Element 和 ReactElement 基本一致,但是 JSX.Element 作为 JSX 的规范,它比 ReactNode 更加通用,因为 type 和 props 都被定义为 any。
  • ReactElement:ReactElement 是包含 type 、 props 、 key 等属性的 object,它是 DOM 的一种抽象表示,是 FunctionComponent (或者 ClassComponent 中 render 函数)执行的结果。

# 原版代码中的池化模式的应用

React 17 中对 ReactChildren 中的代码进行了重构,在 React 16 的旧版代码中有一个对池化模式的应用,我觉得很有意思,作为扩展分享给大家。

在源码中利用池化模式对 traverseContext 进行缓存,减少对象创建的成本。

// traverseContextPool 的 size
const POOL_SIZE = 10;
const traverseContextPool = [];
// 从 traverseContextPool 获取 traverseContext
function getPooledTraverseContext(
  mapResult, // 遍历结果数组
  keyPrefix, // traverseContext 的 key
  mapFunction, // 遍历回调函数
  mapContext, // 遍历的 context
) {
  // 如果当前缓存池非空
  if (traverseContextPool.length) {
    // 取出队尾的traverseContext
    const traverseContext = traverseContextPool.pop();
    traverseContext.result = mapResult;
    traverseContext.keyPrefix = keyPrefix;
    traverseContext.func = mapFunction;
    traverseContext.context = mapContext;
    traverseContext.count = 0;
    // 返回缓存的 traverseContext
    return traverseContext;
  } else {
    // 缓存池为空则新建一个 traverseContext,最多 10 个
    return {
      result: mapResult,
      keyPrefix: keyPrefix,
      func: mapFunction,
      context: mapContext,
      count: 0,
    };
  }
}
// 释放 traverseContext 到 traverseContextPool
function releaseTraverseContext(traverseContext) {
  traverseContext.result = null;
  traverseContext.keyPrefix = null;
  traverseContext.func = null;
  traverseContext.context = null;
  traverseContext.count = 0;
  if (traverseContextPool.length < POOL_SIZE) {
    traverseContextPool.push(traverseContext);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

池化模式在需要频繁创建对象(连接)的场景中可以参考。缓存池大小 POOL_SIZE 需要权衡考虑效率和内存问题。如果 POOL_SIZE 太小,就不能很好的起到缓存的效果,如果太大缓存池本身就需要占用太多内存,而且用不完的 context 对象也容易造成浪费和低效。

# 总结

本篇文章包含了如下的核心知识点,总结如下:

  • ReactNode、ReactElement、ReactChildren 等概念的深入理解。
  • React.children 工具函数:map,forEach,count,toArray,only 的原理。
  • 使用 ReactChildren 工具函数应用到组件库等复杂场景中操作节点。
编辑 (opens new window)
上次更新: 2022/07/22, 14:56:40
React 源码漂流记:ReactElement 与基础概念
React 源码漂流记:React 整体结构和理念初认识

← React 源码漂流记:ReactElement 与基础概念 React 源码漂流记:React 整体结构和理念初认识→

最近更新
01
渲染原理之组件结构与 JSX 编译
09-07
02
计划跟踪
09-06
03
开始上手
09-06
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy Front End | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式