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之後的流程,且此時傳入的回調函數就只返回本身。

同時這段代碼還解釋了,對象池是不是一直只有一個對象的問題。

在當我們傳入的回調函數不返回一個數組時確實是這樣的,但當返回一個數組,甚至是多維數組時,在此處由於會多次重走流程,於是也會多次向對象池獲取對象,然而第一次獲取到的對象此時還未被放回對象池中。於是便會直接返回一個新的對象,當整個方法調用完成後,對象池中便會存在多個對象了。

流程圖

在這裏插入圖片描述

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