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 计划
  • 基础

    • 开始上手
    • 章节说明
    • F&Q
    • ReactChildren
      • 目录
      • map
      • forEach
      • count
      • toArray
      • only
    • ReactElement
  • 调和(Reconciliation)

  • 调度器(Scheduler)

  • 更新器(Updater)

  • 渲染器(Render)

  • hooks原理

  • 总结

  • React源码漂流记

  • react
  • 基础
jonsam
2022-04-14
目录

ReactChildren

# 目录

  • 目录
  • map
    • traverseContext
    • traverseAllChildren
    • ChildrenKey 的维护
  • forEach
  • count
  • toArray
  • only

React Children 有如下四个方法:

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

# map

map 内部调用 mapChildren 方法。

function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}
1
2
3
4
5
6
7
8

这个 mapIntoWithKeyPrefixInternal 很有意思,我们来看看。

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  let escapedPrefix = '';
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }
  // 从缓存池中获取 traverseContext,此时并没有加入 traverseContextPool
  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  // 遍历 children 执行回调,并且将结果加入到 mapResult。
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  // 释放当前遍历的traverseContext。
  releaseTraverseContext(traverseContext);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 这里使用了 traverseContext 的缓存池,目的是避免大量的创建对象耗费内存。
  • traverseAllChildren 这里将 mapSingleChildIntoContext 抽离出来,便于复用。
  • traverseContextPool 里只存未使用的空的 traverseContext,在 releaseTraverseContext 中加入缓存池。

# traverseContext

从缓存池中获取 context:

// 遍历环境缓存池
const POOL_SIZE = 10;
const traverseContextPool = [];
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,
    };
  }
}
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

释放 context 到缓存池:

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
  • 这种写法在需要频繁创建对象的场景中可以参考。缓存池大小 POOL_SIZE 需要权衡考虑效率和内存问题。如果 POOL_SIZE 太小,就不能很好的起到缓存的效果,如果太大缓存池本身就需要占用太多内存,而且用不完的 context 对象也容易造成浪费和低效。

# traverseAllChildren

traverseAllChildren 内部由 traverseAllChildrenImpl 实现,主要作用是遍历目标 children,调用 callback,维护 children 的 key 值。

// 返回子代数量
function traverseAllChildrenImpl(
  children, // 遍历目标
  nameSoFar,
  callback, // mapSingleChildIntoContext 内部的遍历回调器
  traverseContext,
) {
  const type = typeof children;

  if (type === 'undefined' || type === 'boolean') {
    // All of the above are perceived as null.
    children = null;
  }

  // 为 true 表示不需要进一步处理,可以直接 callback。(null,string,number,Element,Portal)。
  // 因为只有一个元素,只 callback 一次。
  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,
      // 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.
      // nameSoFar 初始化,children 不是数组,获取 key 值
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }

  let child;
  let nextName;
  let subtreeCount = 0; // Count of children found in the current subtree.
  // 如:.j:
  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 {
    // 针对不是数组但内部实现了迭代器的 children。
    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,
        );
      }
    } else if (type === 'object') {
      // 如果传入 children 是对象,则报错。
      let addendum = '';
      if (__DEV__) {
        addendum =
          ' If you meant to render a collection of children, use an array ' +
          'instead.' +
          ReactDebugCurrentFrame.getStackAddendum();
      }
      const childrenString = '' + children;
      invariant(
        false,
        'Objects are not valid as a React child (found: %s).%s',
        childrenString === '[object Object]'
          ? 'object with keys {' + Object.keys(children).join(', ') + '}'
          : childrenString,
        addendum,
      );
    }
  }

  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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
  • traverseAllChildrenImpl 如果发现 children 是数组则会递归遍历,最终将 children 展平(包括多为数组),执行 callback。 traverseAllChildrenImpl 只回调 children 的叶子节点。

# mapSingleChildIntoContext

map 所使用的 contextMap 是 mapSingleChildIntoContext,这里才真正调用用户传入的回调,并且返回处理后的节点。

function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;

  // 调用用户的回调函数
  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    // 如果返回了数组,继续进行 map
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    // 是否是 ReactElement
    if (isValidElement(mappedChild)) {
      // 处理mappedChild的 key 值
      mappedChild = cloneAndReplaceKey(
        mappedChild,
        // Keep both the (mapped) and old keys if they differ, just as
        // traverseAllChildren used to do for objects as children
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      );
    }
    // 加入result数组
    result.push(mappedChild);
  }
}
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
  • 这里在回调时,如果用户传回来的还是数组,就继续 map ,只有 用户回调的不是数组且为合法的 ReactElement 时,才会被放入 result 中。result 是 map 的返回值。

# ChildrenKey 的维护

key 所使用的分隔符:

const SEPARATOR = '.';
const SUBSEPARATOR = ':';
1
2

生成 key 值的算法:

function getComponentKey(component, index) {
  // Do some typechecking here since we call this blindly. We want to ensure
  // that we don't block potential future ES APIs.
  // 如果组件有 key 值则使用
  if (
    typeof component === 'object' &&
    component !== null &&
    component.key != null
  ) {
    // Explicit key
    return escape(component.key);
  }
  // Implicit key determined by the index in the set
  // 使用 36 进制,即 0-9-a-z。(35).toString(36) === 'z'。
  return index.toString(36);
}

function escape(key) {
  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
22
23
24
25
26
27
28
29
  • 因为 key 中使用了固定的分隔符,所以用户传递的 key 需要 escape 做等意替换,并且添加前缀 $ 。

key 值的命名方法:

.key => .key:key1 => .key:key1:key2 ...
1

# forEach

forEach 内部由 forEachChildren 实现。代码如下:

function forEachChildren(children, forEachFunc, forEachContext) {
  if (children == null) {
    return children;
  }
  const traverseContext = getPooledTraverseContext(
    null,
    null,
    forEachFunc,
    forEachContext,
  );
  traverseAllChildren(children, forEachSingleChild, traverseContext);
  releaseTraverseContext(traverseContext);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

由此可见:

  • forEach 相比于 mapIntoWithKeyPrefixInternal,只是 contextMap 修改成了 forEachSingleChild ,其他代码并未变化。
  • forEach 和 map 的区别是:forEach 没有返回值;不接受用户回调的结果。

forEachSingleChild 的处理也很简单,只是调用了回调:

function forEachSingleChild(bookKeeping, child, name) {
  const {func, context} = bookKeeping;
  func.call(context, child, bookKeeping.count++);
}
1
2
3
4

# count

count 内部由 countChildren 实现,主要作用是返回拉平后的 children 的叶子节点的数量。

function countChildren(children) {
  return traverseAllChildren(children, () => null, null);
}
1
2
3

# toArray

toArray 将 children 以数组形式返回。这里内部不需要执行回调,因此 contextMap 为 null,

function toArray(children) {
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, child => child);
  return result;
}
1
2
3
4
5

# only

only 内部由 onlyChild 实现。only 验证 children 是否是单节点,并将之返回。代码如下:

function onlyChild(children) {
  invariant(
    isValidElement(children),
    'React.Children.only expected to receive a single React element child.',
  );
  return children;
}
1
2
3
4
5
6
7
编辑 (opens new window)
上次更新: 2022/04/15, 00:23:56
F&Q
ReactElement

← F&Q ReactElement→

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