抹桥的博客
Language
Home
Archive
About
GitHub
Language
主题色
250
1553 words
8 minutes
A Look at React's Source Code - ReactChildren

Imported Modules#

var ReactElement = require("ReactElement");

var emptyFunction = require("fbjs/lib/emptyFunction");
var invariant = require("fbjs/lib/invariant");

Let’s look at the ReactElement module; the other two are utility functions that we don’t need to concern ourselves with.

Exported Objects#

var ReactChildren = {
  forEach: forEachChildren,
  map: mapChildren,
  count: countChildren,
  toArray: toArray,
};

module.exports = ReactChildren;

Let’s examine these four APIs one by one.

forEach#

function forEachChildren(children, forEachFunc, forEachContext) {
  if (children == null) {
    return children;
  }
  var traverseContext = getPooledTraverseContext(
    null,
    null,
    forEachFunc,
    forEachContext,
  );
  traverseAllChildren(children, forEachSingleChild, traverseContext);
  releaseTraverseContext(traverseContext);
}

Parameters: children, forEachFunc, forEachContext. First, we obtain a traversal context object, traverseContext, via getPooledTraverseContext, and then call the traverseAllChildren method to traverse all descendant nodes of the provided children. Finally, we release the current traverseContext.

getPooledTraverseContext#

var POOL_SIZE = 10;
var traverseContextPool = [];
function getPooledTraverseContext(
  mapResult,
  keyPrefix,
  mapFunction,
  mapContext,
) {
  if (traverseContextPool.length) {
    var 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,
    };
  }
}

A traverseContextPool is defined to avoid the cost of recreating new objects every time, with a size of 10. The getPooledTraverseContext method accepts four parameters: mapResult, keyPrefix, mapFunction, mapContext. These are then assigned to traverseContext, and in addition, a counter property count is added.

traverseAllChildren#

function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0;
  }

  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
 
`traverseAllChildren` 只是个空壳,里面的 `traverseAllChildrenImpl` 才是真正的实现。
** traverseAllChildrenImpl **

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

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

  if (
    children === null ||
    type === 'string' ||
    type === 'number' ||
    // The following is inlined from ReactElement. This means we can optimize
    // some checks. React Fiber also inlines this logic for similar purposes.
    (type === 'object' && children.$typeof === REACT_ELEMENT_TYPE)
  ) {
    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 === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }

It accepts four parameters. First, it checks the type of the children parameter; if invalid, children is considered null. The subsequent series of checks means that when children is a single valid React element, the callback function is executed, and 1 is returned. Since this function will be called recursively later, this also serves as the exit point for the recursive calls.

var child;
var nextName;
var subtreeCount = 0; // Count of children found in the current subtree.
var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;

if (Array.isArray(children)) {
  for (var i = 0; i < children.length; i++) {
    child = children[i];
    nextName = nextNamePrefix + getComponentKey(child, i);
    subtreeCount += traverseAllChildrenImpl(
      child,
      nextName,
      callback,
      traverseContext,
    );
  }
} else {

When the provided children is an Array, this Children Array is iterated, and the current function traverseAllChildrenImpl is called for each element within it. It’s important to note the parameters passed: nextName is used to add a key value to the React element being traversed, and callback and traverseContext have the same values as the current function, ensuring that each child element can also apply the current callback and access the original traverseContext.

} else {
  var iteratorFn =
    (ITERATOR_SYMBOL && children[ITERATOR_SYMBOL]) ||
    children[FAUX_ITERATOR_SYMBOL];
  if (typeof iteratorFn === 'function') {
    if (__DEV__) {
      // Warn about using Maps as children
      if (iteratorFn === children.entries) {
        warning(
          didWarnAboutMaps,
          'Using Maps as children is unsupported and will likely yield ' +
            'unexpected results. Convert it to a sequence/iterable of keyed ' +
            'ReactElements instead.%s',
          getStackAddendum(),
        );
        didWarnAboutMaps = true;
      }
    }

    var iterator = iteratorFn.call(children);
    var step;
    var 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') {
    var addendum = '';
    if (__DEV__) {
      addendum =
        ' If you meant to render a collection of children, use an array ' +
        'instead.' +
        getStackAddendum();
    }
    var 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,
    );
  }
}

When it’s not an Array but an iterable object, the traverseAllChildrenImpl method is called recursively, similar to the above. For other cases, the child is considered invalid, and an error is reported.

  return subtreeCount;
}

Finally, it returns the count of all descendant elements. Overall, the purpose of the traverseAllChildrenImpl method is to traverse all descendant elements of the given children, call the callback method on each descendant element, and assign a unique key value within the current context to each element as a parameter to be passed.

Returning to the forEach method:

traverseAllChildren(children, forEachSingleChild, traverseContext);

This line means traversing all descendant elements of the given children and calling the forEachSingleChild method on them.

function forEachSingleChild(bookKeeping, child, name) {
  var { func, context } = bookKeeping;
  func.call(context, child, bookKeeping.count++);
}

This passed callback method, forEachSingleChild, retrieves func and context from the bookKeeping parameter (which is traverseContext), and then calls func with context as its this context, and child and the counter count as arguments. Here, func is the forEachFunc parameter of forEachChildren, which is the function ultimately provided by the end-user.

releaseTraverseContext(traverseContext);

Releasing the current traverseContext means setting all its properties to null and placing it back into traverseContextPool for future use, thereby improving efficiency.

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);
  }
}

map#

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

Calls func on the descendant elements of the provided children and returns a collection of the results of calling func.

mapIntoWithKeyPrefixInternal#

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  var escapedPrefix = "";
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + "/";
  }
  var traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  releaseTraverseContext(traverseContext);
}

First, if a prefix is provided, it is escaped to be used as the prefix key for traverseContext. Then, a traverseContext object is obtained. Similar to forEachChildren, all descendant elements of children are traversed, and the given callback function is executed. Finally, traverseContext is released.

The only difference is this callback method: mapSingleChildIntoContext.

mapSingleChildIntoContext#

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

  var mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(
      mappedChild,
      result,
      childKey,
      emptyFunction.thatReturnsArgument,
    );
  } else if (mappedChild != null) {
    if (ReactElement.isValidElement(mappedChild)) {
      mappedChild = ReactElement.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.push(mappedChild);
  }
}

This method is very similar to forEachSingleChildren above. It retrieves result, keyPrefix, func, and context from bookKeeping. result is actually the empty array initially defined in mapChildren, keyPrefix is the escapedPrefix from mapIntoWithKeyPrefixInternal, and func and context are the corresponding parameters of mapChildren.

First, mappedChild is defined as the return value of the user-provided mapFunc function call. Then, it checks if this return value mappedChild is an Array. If it is, the mapIntoWithKeyPrefixInternal method is called recursively. Otherwise, if it’s not null and is a valid React element, a cloned element of mappedChild (with a new key value composed of keyPrefix, the user-assigned key (mappedChild.key), and the original childkey) is used as the map result and pushed into result.

The entire mapChildren method calls the mapFunc method on each descendant element of the provided children, sets a new key for the returned result, and finally places each executed result mappedChild into a list to be returned to the user.

countChildren#

function countChildren(children, context) {
  return traverseAllChildren(children, emptyFunction.thatReturnsNull, null);
}

This is very simple; it just returns the count of all descendant nodes by traversing them. emptyFunction.thatReturnsNull This is a function that returns null.

toArray#

function toArray(children) {
  var result = [];
  mapIntoWithKeyPrefixInternal(
    children,
    result,
    null,
    emptyFunction.thatReturnsArgument,
  );
  return result;
}

If you understand mapIntoWithKeyPrefixInternal above, then this is also very simple. emptyFunction.thatReturnsArgument is a function that returns its first argument.

** mapSingleChildIntoContext **

var mappedChild = func.call(context, child, bookKeeping.count++);

So this line simply returns the child itself. The result is then placed into result, and finally, all results are returned to the caller.

Summary#

ReactChildren has four APIs, and these four APIs primarily rely on two methods: traverseAllChildrenImpl and mapSingleChildIntoContext. Other methods are composite calls built upon these. Another noteworthy point is the use of the object pool, traverseContextPool. I believe this is because traverseContext objects are frequently created due to recursive calls, and repeatedly creating new objects requires reallocating memory in the heap, which is costly. Therefore, an object pool was introduced to improve performance.

Related Articles#

  • {% post_link react-source-code-analyze-1 React Source Code Analysis - Entry File %}
  • {% post_link react-source-code-analyze-2 React Source Code Analysis - ReactBaseClasses %}
  • {% post_link react-source-code-analyze-3 React Source Code Analysis - ReactChildren %}
  • {% post_link react-source-code-analyze-4 React Source Code Analysis - ReactElement %}
  • {% post_link react-source-code-analyze-5 React Source Code Analysis - onlyChildren %}

This article was published on September 21, 2017 and last updated on September 21, 2017, 2937 days ago. The content may be outdated.

A Look at React's Source Code - ReactChildren
https://blog.kisnows.com/en-US/2017/09/21/react-source-code-analyze-3/
Author
Kisnows
Published at
2017-09-21
License
CC BY-NC-ND 4.0