# React 源码解析(一)JSX 转换

React 源码系列的第一章,这一章从 jsx 代码转换开始,分析一下 react 的源码部分。

React 将源码划分成了很多个单独的包,我们主要分析这几个:

  • react
  • react-dom
  • react-reconciler
  • scheduler

关于 react 版本

此系列文章中,react 版本为 16.9.0,react-dom 版本为 16.9.0。

本篇文章提纲。

  • JSX 文件转换
  • React.Children 原理

# JSX 文件转换

我们在写 react 项目时,都会写 jsx 组件,jsx 组件经过编译后,会转成相应的 js 文件。

我们一起进入到 babel 的官方,看一下我们平时写的 jsx 会被编译成什么样子?

function Comp() {
  return <span>hello</span>;
}

function App() {
  return (
    <div id="test">
      <Comp />
      <ul>
        <li>1</li>
        <li>2</li>
      </ul>
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

经过 babel 编译后,生成如下代码:

function Comp() {
  return React.createElement('span', null, 'hello');
}

function App() {
  return React.createElement(
    'div',
    {
      id: 'test'
    },
    React.createElement(Comp, null),
    React.createElement(
      'ul',
      null,
      React.createElement('li', null, '1'),
      React.createElement('li', null, '2')
    )
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

可以看到,经过 babel 编译后的代码,是将 jsx 转换成 React.createElement 的函数调用模式。我们再来看一下 React.createElement 在做什么?

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner
  };
  return element;
};

function createElement(type, config, children) {
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

可以看到 React.createElement 函数返回的是一个特定格式的 js 对象。

# React.Children 原理

这里介绍一下 React.Children,这个方法封装了处理 props.children 的方法。

import { forEach, map, count, toArray, only } from './ReactChildren';
const React = {
  Children: {
    map,
    forEach,
    count,
    toArray,
    only
  }
};
1
2
3
4
5
6
7
8
9
10

以 React.Children.map 为例,具体的流程如下图所示。

cmd-react-children

# ContextPool

看完流程图,你可能有疑惑,contextPool 是用来干嘛的?

试想一下,如果每次构建节点,都要构建指定格式的 obj,用完之后,被系统垃圾回收掉。对于内存来说,就会不断进行内存分配,内存释放的过程,通过任务管理器查看内存状态时,就会看到锯齿形状的内存图。

为了避免这种情况,react 使用一个 contextPool 来进行复用对象复用,当需要构建 obj 时,直接从 contextPool 里获取第一个 obj,重新进行赋值就可以了,当使用完成后,再将这条数据内容清空,加入到 contextPool 里,以供下次使用。

contextPool 部分代码如下:

const POOL_SIZE = 10;
const traverseContextPool = [];

// 复用完毕之后,清空操作,如果 pool 小于 10,就会加入到 contextPool 中。
function releaseTraverseContext(traverseContext) {
  // 使用完成,清空对象
  traverseContext.result = null;
  traverseContext.keyPrefix = null;
  traverseContext.func = null;
  traverseContext.context = null;
  traverseContext.count = 0;
  if (traverseContextPool.length < POOL_SIZE) {
    // 加入到 pool
    traverseContextPool.push(traverseContext);
  }
}

// 获取 contextPool 中的对象
function getPooledTraverseContext(
  mapResult,
  keyPrefix,
  mapFunction,
  mapContext
) {
  if (traverseContextPool.length) {
    // 复用对象
    const traverseContext = traverseContextPool.pop();
    traverseContext.result = mapResult;
    traverseContext.keyPrefix = keyPrefix;
    traverseContext.func = mapFunction;
    traverseContext.context = mapContext;
    traverseContext.count = 0;
    return traverseContext;
  } else {
    // 第一次,返回新对象
    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
32
33
34
35
36
37
38
39
40
41
42
43
44

# 在 React.Children 中设置组件 key

顺着 React.Children.map 继续往下走,会自动设置 children 的 key 值,最后返回 map 后的数据。

设置后的一个 children 如下所示:

React.Children.map(this.props.children, children => [children]);
// children 的 key 会自动加上前缀
{
  type: 'span',
  key: '.0/.0',
  ref: null,
  props: Object,
  _owner: null,
  _store: Object
}
1
2
3
4
5
6
7
8
9
10

接下来我们看一下 React.Children.map 是如何设置 key 的?

// 代码已简化
function mapChildren(children, func, context) {
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  // 注意 mapSingleChildIntoContext
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
}

function traverseAllChildren(children, callback, traverseContext) {
  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}

function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback, // mapSingleChildIntoContext
  traverseContext
) {
  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 === '' ? '.' + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }
    // 设置 children 的 key
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getComponentKey(child, i);
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext
      );
    }
  }
  return subtreeCount;
}

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

  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    // 循环遍历 children
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    // 深拷贝一个child,并替换 key
    mappedChild = cloneAndReplaceKey(
      mappedChild,
      keyPrefix +
        (mappedChild.key && (!child || child.key !== mappedChild.key)
          ? escapeUserProvidedKey(mappedChild.key) + '/'
          : '') +
        childKey
    );
    // 返回 map 完成的 child
    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
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

# 总结

这一节分析了从 jsx 转换,到构建 js 对象的过程,其中还包括 React.Children 的原理,下一章继续分析 ReactDOM.render 构建 fiberRoot 的过程。

# 相关链接