React source code analysis -- ReactChildren.js

Keywords: Javascript node.js React

2021SC@SUDSC

2021SC@SUDSC

ReactChildren.js

As anyone who has read the source code knows, this js file exports five methods:

export {
  forEachChildren as forEach,
  mapChildren as map,
  countChildren as count,
  onlyChild as only,
  toArray,
};

Then in the React.js file:

const Children = {
  map,
  forEach,
  count,
  toArray,
  only,
};

Let me lead you to analyze this file.

import

The idea is clear. Let's first talk about the import part to see what is introduced in the ReactChildren file.

import type {ReactNodeList} from 'shared/ReactTypes';

import invariant from 'shared/invariant';
import isArray from 'shared/isArray';
import {
  getIteratorFn,
  REACT_ELEMENT_TYPE,
  REACT_PORTAL_TYPE,
} from 'shared/ReactSymbols';

import {isValidElement, cloneAndReplaceKey} from './ReactElement';
  1. First, the ReactNodeList in ReactTypes is introduced
export type ReactNodeList = ReactEmpty | React$Node;

These are built-in private types of flow, which are defined in flow, so you can't find the definition in React. Here, I put what I found, where React$Node is Definitions on the official website , or from this see

declare type React$Node =
  | null
  | boolean
  | number
  | string
  | React$Element<any>
  | React$Portal
  | Iterable<?React$Node>;
  1. Next, we introduce our old friend invariant. For its usefulness, please see my last blog
  2. isArray.js
declare function isArray(a: mixed): boolean %checks(Array.isArray(a));

const isArrayImpl = Array.isArray;

// eslint-disable-next-line no-redeclare
function isArray(a: mixed): boolean {
  return isArrayImpl(a);
}

export default isArray;

The isArray method is very simple. It is used to check whether it is an array, but this form is very strange. The reason why this form is used is that the function for judging parameters is directly defined in Flow, which will report an error:

function truthy(a, b): boolean {   // report errors
  return a && b;
}

Therefore, you need to mark this function with% checks, which is a check function:

function truthy(a, b): boolean %checks { // No error reporting
  return a && b;
}
  1. ReactSymbols.js
const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
const FAUX_ITERATOR_SYMBOL = '@@iterator';

export function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator<*> {
  if (maybeIterable === null || typeof maybeIterable !== 'object') {
    return null;
  }
  const maybeIterator =
    (MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL]) ||
    maybeIterable[FAUX_ITERATOR_SYMBOL];
  if (typeof maybeIterator === 'function') {
    return maybeIterator;
  }
  return null;
}

getIteratorFn method is only called once in the whole ReactChildren file. It is aimed at those cases that are not Array type but have iterators and can be traversed. This is not the case with normal code and can be basically ignored.
5. ReactElement.js

export function cloneAndReplaceKey(oldElement, newKey) {
  const newElement = ReactElement(
    oldElement.type,
    newKey,
    oldElement.ref,
    oldElement._self,
    oldElement._source,
    oldElement._owner,
    oldElement.props,
  );

  return newElement;
}

Clone an old react element, set a new key to the new react element, and call the ReactElement method (see the next blog for the analysis of this method)

export function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
}

Judge whether an object is a legal react element and whether its $$typeof attribute is REACT_ELEMENT_TYPE

map

function mapChildren(
  children: ?ReactNodeList,
  func: MapFunc,
  context: mixed,
): ?Array<React$Node> {
  if (children == null) {
    return children;
  }
  const result = [];
  let count = 0;
  mapIntoArray(children, result, '', '', function(child) {
    return func.call(context, child, count++);
  });
  return result;
}

Similar to array.map, but with the following differences:
1. The returned result must be a one-dimensional array, and the multi-dimensional array will be automatically flattened
2. For each returned node, if isValidElement(el) === true, a key will be added to it. If the element already has a key, a new key will be regenerated.

Usage of map: the first parameter is the children to be traversed, the second parameter is the function to be traversed, and the third parameter is context, which is used when the traversal function is executed.
If children == null, it will be returned directly, and the traversed elements will be thrown into the result and finally returned

function mapIntoArray(
  children: ?ReactNodeList,
  array: Array<React$Node>,
  escapedPrefix: string,
  nameSoFar: string,
  callback: (?React$Node) => ?ReactNodeList,
): number {
  const type = typeof children;

  if (type === 'undefined' || type === 'boolean') {
    // All of the above are perceived as null.
    children = null;
  }
  let invokeCallback = false;
  if (children === null) {
    invokeCallback = true;
  } else {
    switch (type) {
      case 'string':
      case 'number':
        invokeCallback = true;
        break;
      case 'object':
        switch ((children: any).$$typeof) {
          case REACT_ELEMENT_TYPE:
          case REACT_PORTAL_TYPE:
            invokeCallback = true;
        }
    }
  }

  if (invokeCallback) {
    const child = children;
    let mappedChild = callback(child);
    // 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:
    const childKey =
      nameSoFar === '' ? SEPARATOR + getElementKey(child, 0) : nameSoFar;
    if (isArray(mappedChild)) {
      let escapedChildKey = '';
      if (childKey != null) {
        escapedChildKey = escapeUserProvidedKey(childKey) + '/';
      }
      mapIntoArray(mappedChild, array, escapedChildKey, '', c => c);
    } else if (mappedChild != null) {
      if (isValidElement(mappedChild)) {
        mappedChild = cloneAndReplaceKey(
          mappedChild,
          // Keep both the (mapped) and old keys if they differ, just as
          // traverseAllChildren used to do for objects as children
          escapedPrefix +
            // $FlowFixMe Flow incorrectly thinks React.Portal doesn't have a key
            (mappedChild.key && (!child || child.key !== mappedChild.key)
              ? // $FlowFixMe Flow incorrectly thinks existing element's key can be a number
                escapeUserProvidedKey('' + mappedChild.key) + '/'
              : '') +
            childKey,
        );
      }
      array.push(mappedChild);
    }
    return 1;
  }
}

This function is complex, and the function signature is like this
Children children to process
result stores an array of processed children
escapedPrefix is the current key
nameSoFar is the parent key, which will be spliced and passed layer by layer, separated by ':'
callback is:

function(child) {
    return func.call(context, child, count++);
 }

If the type of current children is:
1.undefined and boolean will become null
2.string, number and $$typeof are REACT_PORTAL_TYPE or REACT_ELEMENT_TYPE will be processed. First, judge whether it is an array. If so, call itself again until all child elements can be retrieved. See the following code for details. At the same time, this also avoids the problem of creating a large number of objects in the face of multiple irregular nesting, ex:[1,[2,[3,4]]]. At the same time, note that the callback has changed to C = > C, which means function (c) {return c}; If it is not an array and the return value is not empty, judge whether the return value is a valid Element. If yes, clone the Element, replace the key, and put it into the array.
3. If it is an array, perform the following processing:

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

  if (isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getElementKey(child, i);
      subtreeCount += mapIntoArray(
        child,
        array,
        escapedPrefix,
        nextName,
        callback,
      );
    }
  } 

From the above analysis, we know this Code: when the node is an array, we start to traverse the array and recursively execute each element in the array, which is why we need to verify whether it is an array.

else {
    const iteratorFn = getIteratorFn(children);
    if (typeof iteratorFn === 'function') {
      const iterableChildren: Iterable<React$Node> & {
        entries: any,
      } = (children: any);

      if (__DEV__) {
        // Warn about using Maps as children
        if (iteratorFn === iterableChildren.entries) {
          if (!didWarnAboutMaps) {
            console.warn(
              'Using Maps as children is not supported. ' +
                'Use an array of keyed ReactElements instead.',
            );
          }
          didWarnAboutMaps = true;
        }
      }

      const iterator = iteratorFn.call(iterableChildren);
      let step;
      let ii = 0;
      while (!(step = iterator.next()).done) {
        child = step.value;
        nextName = nextNamePrefix + getElementKey(child, ii++);
        subtreeCount += mapIntoArray(
          child,
          array,
          escapedPrefix,
          nextName,
          callback,
        );
      }
    } else if (type === 'object') {
      const childrenString = '' + (children: any);
      invariant(
        false,
        'Objects are not valid as a React child (found: %s). ' +
          'If you meant to render a collection of children, use an array ' +
          'instead.',
        childrenString === '[object Object]'
          ? 'object with keys {' + Object.keys((children: any)).join(', ') + '}'
          : childrenString,
      );
    }
  }

If it is not an array, let's see if children can support iteration. Fetch it through obj[Symbol.iterator]. It is correct only if the extracted object is a function type. Then get the iterator of children, execute the iterator, pass the iteration result into itself, and call its own processing again
In general, the core function of this function is to spread the passed in children array into a single node through traversal, and then push it into the array.

forEach

function forEachChildren(
  children: ?ReactNodeList,
  forEachFunc: ForEachFunc,
  forEachContext: mixed,
): void {
  mapChildren(
    children,
    function() {
      forEachFunc.apply(this, arguments);
      // Don't return anything.
    },
    forEachContext,
  );
}

Similar to array.forEach. forEach only needs to traverse and does not need to return an array

count

function countChildren(children: ?ReactNodeList): number {
  let n = 0;
  mapChildren(children, () => {
    n++;
    // Don't return anything
  });
  return n;
}

Calculating the number of children is simple and has nothing to say.

only

function onlyChild<T>(children: T): T {
  invariant(
    isValidElement(children),
    'React.Children.only expected to receive a single React element child.',
  );
  return children;
}

If the parameter is a ReactElement, it is returned directly. Otherwise, an error is reported. It is used in the test, and the formal code is useless.

toArray

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

To convert children into Array, you need to use when sorting children

Posted by Codewarrior123 on Thu, 14 Oct 2021 11:58:46 -0700