# react解析 React.Children(二)

感谢 yck: 剖剖析 React 源码解析 (opens new window),本篇文章是在读完他的文章的基础上,将他的文章进行拆解和加工,加入我自己的一下理解和例子,便于大家理解。觉得yck (opens new window)写的真的很棒 。React 版本为 16.8.6,关于源码的阅读,可以移步到yck react源码解析 (opens new window)

本文永久有效链接: react解析 React.Children(二) (opens new window)

在React实际开发中, React.Children 这个API我们虽然使用的比较少, 但是我们通过这个API可以操作children, 可以查看文档 (opens new window)

我们来看下这个API的神奇用法

React.Children.map(this.props.children, c => [[c, c]])
1

下面可以看一下它在项目中的实际用法

控制台打印渲染的节点和props,如下图 从上图可以得知,通过 c => [[c, c]] 转换以后节点变为了:

// 通过 c => [[c, c]] 转换以后
<div>
    <p>1</p>
    <p>1</p>
    <p>2</p>
    <p>2</p>
</div>
1
2
3
4
5
6
7

我们需要定位到 ReactChildren.js 文件,查看代码 (opens new window), React.Children.map 方法实际就是mapChildren函数,让我们来看看 mapChildren 内部到底是如何实现的吧!

function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  // 这里是处理 key,不关心也没事
  let escapedPrefix = '';
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }
  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  releaseTraverseContext(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

getPooledTraverseContext 和 releaseTraverseContext 中的代码, 引入了对象重用池的概念。这个概念的用处就是维护一个大小固定的对象重用池,每次从这个池子里取一个对象去赋值,用完之后就将对象上的属性清空然后丢回池子。维护这个池子的用意就是提高性能,避免频繁创建销毁多属性对象。

虽然在调用了traverseAllChildren函数,实际调用的是traverseAllChildrenImpl方法。

function traverseAllChildrenImpl( children, nameSoFar, callback,traverseContext ) {
  const type = typeof children;

  if (type === 'undefined' || type === 'boolean') {
    children = null;
  }

  let invokeCallback = false;

  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;
        }
    }
  }
  if (invokeCallback) {
    callback(
      traverseContext,
      children,
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }

  let child;
  let nextName;
  let subtreeCount = 0;
  const nextNamePrefix =
    nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
  if (Array.isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getComponentKey(child, i);
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext,
      );
    }
  } else {
    const iteratorFn = getIteratorFn(children);
    if (typeof iteratorFn === 'function') {
      const iterator = iteratorFn.call(children);
      let step;
      let ii = 0;
      while (!(step = iterator.next()).done) {
        child = step.value;
        nextName = nextNamePrefix + getComponentKey(child, ii++);
        subtreeCount += traverseAllChildrenImpl(
          child,
          nextName,
          callback,
          traverseContext,
        );
      }
    }
  }

  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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

这个函数首先 判断 children 的类型, 若children为数组,继续递归调用traverseAllChildrenImpl,直到处理成单个可渲染的节点,然后调用才能调用callback,也就是mapSingleChildIntoContext

最后让我们来读一下 mapSingleChildIntoContext 函数的实现。

function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;
  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    if (isValidElement(mappedChild)) {
      mappedChild = cloneAndReplaceKey(
        mappedChild,
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      );
    }
    result.push(mappedChild);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

mapSingleChildIntoContext函数其实就是调用React.Children.map(children, callback)中的callback. 如果map之后还是数组, 那么再次进入mapIntoWithKeyPrefixInternal, 那么这个时候我们就会再次从对象重用池里面去获取context, 而对象重用池的意义也就是在这里, 如果循环嵌套多了, 可以减少很多对象创建和gc的损耗. 如果不是数组, 判断返回值是否是有效的 Element, 验证通过的话就 clone 一份并且替换掉 key, 最后把返回值放入 result 中, result 其实也就是 mapChildren 的返回值.

下面是运行顺序:

mapChildren 函数
     |
    \|/
mapIntoWithKeyPrefixInternal 函数     
     |
    \|/
traverseAllChildrenImpl函数(循环成单个可渲染的节点,如果不是递归)
     |    
     |单个节点
    \|/mapSingleChildIntoContext函数(判断是否是有效Element, 验证通过就 clone 并且替换掉 key,
并值放入result,result就是map的返回值)
1
2
3
4
5
6
7
8
9
10
11

更多内容:

react解析: React.createElement(一) (opens new window)

react解析: React.Children(二) (opens new window)

react解析: render的FiberRoot(三) (opens new window)

参考:

yck: 剖剖析 React 源码 (opens new window)

Jokcy 的 《React 源码解析》: react.jokcy.me/ (opens new window)

ps: 顺便推一下自己的个人公众号:Yopai,有兴趣的可以关注,每周不定期更新,分享可以增加世界的快乐

🥰Me

男性,武汉工作,会点Web,擅长Javascript.

技术或工作问题交流,可联系微信:1169170165.

🚀小程序&公众号
🚀运行
2024年2月19日星期一下午2点01分
博客已运行:--天--时--分--秒
上次更新: 7/18/2023, 6:00:41 PM

评 论: