React源码阅读—React.Children.map

React.Children.map

先来看这样一段代码:

import React from 'react';
function Father(props){
    console.log(props.children);
    console.log(React.Children.map(props.children, item=>[item, item]));
    return props.children;
}

export default () => (
    <Father>
        <div>hello</div>
        <div>hello</div>
        <div>hello</div>
    </Father>
);

代码很简单,在页面上只是显示了三行"hello"而已。重点不在这里,而是在于通过console.log在控制台输出的内容。

我们知道可以通过组件的props.children获得该组件的子组件。那么可以想到console.log(props.children);预期的结果应该是打印出一个长度为3的数组。

在这里插入图片描述
事实也确实如此,控制台中输出了一个长度为3的ReactElement数组。

那么console.log(React.Children.map(props.children, item=>[item, item]));会在控制台输出什么呢?

我们可能会猜想它应该与数组的map方法一样,返回一个长度为3的二维数组,每个元素又是一个长度为2的ReactElement数组。

[外链图片转存失败(img-TllfUeS0-1566355435380)(https://note.youdao.com/src/56F110D12C694C76B14B90D5E062FE0D)]

可事实却并不是这样,控制台输出的是一个长度为6的ReactElement数组。可以想见,React.map将我们设想中的二维数组给降维了。

具体过程是怎样,需要看看源码。

源码阅读

React.js

首先在源码目录下的React.js可以发现map是在此处成为React.Children的一个方法的。

在这里插入图片描述

接着可以发现它真正的定义是在同目录下的ReactChildren.js文件中。
在这里插入图片描述

ReactChildren.js

ReactChildren.js文件的最尾部,可以看到map方法在此处被导出,其真正的名字应该是mapChildren
在这里插入图片描述

mapChildren()

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

这个方法接受3个参数childrenfunccontext

children就是将要被遍历的子组件数组,func是对单个子组件需要执行的函数,context则是func执行时this指针所指向的对象。

mapIntoWithKeyPrefixInternal()

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

这个方法本身并没有什么可说的,值得说的是其中被调用的getPooledTraverseContexttraverseAllChildrenreleaseTraverseContext

getPooledTraverseContext()

getPooledTraverseContext()

const POOL_SIZE = 10;
const traverseContextPool = [];

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

traverseContextPool是文件中定义的对象池,POOL_SIZE则定义了对象池的大小。

getPooledTraverseContext方法就是从对象池中获得一个context对象。

逻辑是若对象池已存在对象则pop出来,并将我们调用React.map时传入的一些参数赋值给context对象中相应的属性。

若对象池为空,则直接返回一个新的context对象。

对象池

对象池的作用是避免频繁的创建和销毁,以避免不必要的性能消耗和内存抖动的问题。

traverseAllChildren()

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

  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}

这个方法用以判断传入的子组件是否为空,不是空的再继续调用traverseAllChildrenImpl方法。
值得注意的是,参数中的callback并不是我们传入的回调函数,而是之前在mapIntoWithKeyPrefixInternal中传入的mapSingleChildIntoContext

releaseTraverseContext()

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

在前面通过getPooledTraverseContext获得到的context对象在使用过后,会通过这个方法将对象内属性清空并重新放入对象池中(当对象池还有空间时)。

看到这里可能大家会有一个疑问。最开始的时候对象池为空,于是直接返回了一个新的context对象,使用完之后,通过
releaseTraverseContext方法放回对象池中。而在又一次从对象池中获取对象的过程中,获得到的正是我们刚刚放进去的那一个,那么岂不是对象池中始终只有一个对象?我们接着往下看。

traverseAllChildrenImpl()

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

  let child;
  let nextName;
  let subtreeCount = 0; // Count of children found in the current subtree.
  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') {
      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.',
          );
          didWarnAboutMaps = true;
        }
      }

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

这部分代码算得上是React.map实现的核心之一,我们逐段逐段的看。

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.$$typeof) {
          case REACT_ELEMENT_TYPE:
          case REACT_PORTAL_TYPE:
            invokeCallback = true;
        }
    }
  }

这段代码是用来判断传入的子组件的,主要是判断是不是单个对象以及类型。如果满足条件,则将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 === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }

若上面的代码将invokeCallback置为true,则调用callback,注意此处的callback不是我们传入的回调函数,而是mapSingleChildIntoContext


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

这段代码则是用以处理传入的是数组的情况的。若为数组,则会遍历数组,并将数组中的每一个元素作为traverseAllChildrenImpl的第一个参数,递归的调用自身。

mapSingleChildIntoContext

这个方法是React.map实现的另一个核心。

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

在这个方法里,我们从context对象中获取到调用React.map时传入的回调函数,并执行。

下面这段代码解释了一开始的问题,为什么我们调用React.map,尽管预期是一个二维数组,而返回的却是一个一维数组。

if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
}

如果执行我们传入的回调函数后返回的是一个数组,那么则会将这个数组作为参数,重新走一遍调用React.map之后的流程,且此时传入的回调函数就只返回本身。

同时这段代码还解释了,对象池是不是一直只有一个对象的问题。

在当我们传入的回调函数不返回一个数组时确实是这样的,但当返回一个数组,甚至是多维数组时,在此处由于会多次重走流程,于是也会多次向对象池获取对象,然而第一次获取到的对象此时还未被放回对象池中。于是便会直接返回一个新的对象,当整个方法调用完成后,对象池中便会存在多个对象了。

流程图

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章