React源碼解析之React.children.map()

NO IMAGE
React源碼解析之React.children.map()

一、例子

function ChildrenDemo(props) {
  console.log(props.children, 'children30');
  console.log(React.Children.map(props.children, item => [item, [item, [item]]]), 'children31');
  // console.log(React.Children.map(props.children,item=>item),'children31')
  return props.children;
}

export default ()=>(
  <ChildrenDemo>
    <span key={'.0/'}>1</span>
    <span>2</span>
  </ChildrenDemo>
)

props.children :

React源碼解析之React.children.map()

React.Children.map(props.children, item => [item, [item, [item]]] :

React源碼解析之React.children.map()

看到一個有趣的現象,就是多層嵌套的數組[item, [item, [item]]]經過map()後,平鋪成[item,item,item]了,接下來以該例解析React.Child.map()

二、React.Children.map()
作用:
zh-hans.reactjs.org/docs/react-…

源碼:

// React.Children.map(props.children,item=>[item,[item,] ])
function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  //進行基本的判斷和初始化後,調用該方法
  //props.children,[],null,(item)=>{return [item,[item,] ]},undefined
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}

export {
  //as就是重命名了,map即mapChildren
  forEachChildren as forEach,
  mapChildren as map,
  countChildren as count,
  onlyChild as only,
  toArray,
};

解析:
注意result,該數組在裡面滾了一圈後,會return結果

三、mapIntoWithKeyPrefixInternal()
作用:
getPooledTraverseContext()/traverseAllChildren()/releaseTraverseContext()的包裹器

源碼:

//第一次:props.children , [] , null , (item)=>{return [item,[item,] ]} , undefined
//第二次:[item,[item,] ] , [] , .0 , c => c , undefined
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  let escapedPrefix = '';
  //如果字符串中有連續多個 / 的話,在匹配的字串後再加 /
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }
  //從pool中找一個對象
  //[],'',(item)=>{return [item,[item,] ]},undefined

  //traverseContext=
  // {
  //  result:[],
  //  keyPrefix:'',
  //  func:(item)=>{return [item,[item,] ]},
  //  context:undefined,
  //  count:0,
  // }
  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  //將嵌套的數組展平
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  releaseTraverseContext(traverseContext);
}

解析:

escapeUserProvidedKey()
這個函數一般是第二層遞歸時,會用到

作用:
/後再加一個/

源碼:

const userProvidedKeyEscapeRegex = /\/+/g;
function escapeUserProvidedKey(text) {
  //如果字符串中有連續多個 / 的話,在匹配的字串後再加 /
  return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/');
}

解析:
react對key定義的一個規則:
如果字符串中有連續多個/的話,在匹配的字串後再加/

例:

let a='aa/a/'
console.log(a.replace(/\/+/g, '$&/')); //  aa//a//

getPooledTraverseContext()

作用:
創建一個對象池,複用Object,從而減少很多對象創建帶來的內存佔用和gc(垃圾回收)的損耗

源碼:

//對象池的最大容量為10
const POOL_SIZE = 10;
//對象池
const traverseContextPool = [];
//[],'',(item)=>{return [item,[item,] ]},undefined
function getPooledTraverseContext(
  mapResult,
  keyPrefix,
  mapFunction,
  mapContext,
) {
  //如果對象池內存在對象,則出隊一個對象,
  //並將arguments的值賦給對象屬性
  //最後返回該對象
  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 {
    //{
    // result:[],
    // keyPrefix:'',
    // func:(item)=>{return [item,[item,] ]},
    // context:undefined,
    // count:0,
    // }
    return {
      result: mapResult,
      keyPrefix: keyPrefix,
      func: mapFunction,
      context: mapContext,
      count: 0,
    };
  }
}

解析:
在每次map()的過程中,每次遞歸都會用到traverseContext對象,
創建traverseContextPool對象池的目的,就是複用裡面的對象,
以減少內存消耗
,並且在map()結束時,
將複用的對象初始化,並push進對象池中(releaseTraverseContext),以供下次map()時使用

mapSingleChildIntoContext()
mapSingleChildIntoContexttraverseAllChildren(children, mapSingleChildIntoContext, traverseContext)的第二個參數,為避免講traverseAllChildren要調頭看這個 API,就先分析下

作用:
遞歸仍是數組的child
將單個ReactElementchild加入result

源碼:

//bookKeeping:traverseContext=
// {
//  result:[],
//  keyPrefix:'',
//  func:(item)=>{return [item,[item,] ]},
//  context:undefined,
//  count:0,
// }

//child:<span>1<span/>

//childKey:.0
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  //解構賦值
  const {result, keyPrefix, func, context} = bookKeeping;
  //func:(item)=>{return [item,[item,] ]},
  //item即<span>1<span/>
  //第二個參數bookKeeping.count++很有意思,壓根兒沒用到,但仍起到計數的作用
  let mappedChild = func.call(context, child, bookKeeping.count++);
  //如果根據React.Children.map()第二個參數callback,執行仍是一個數組的話,
  //遞歸調用mapIntoWithKeyPrefixInternal,繼續之前的步驟,
  //直到是單個ReactElement
  if (Array.isArray(mappedChild)) {
    //mappedChild:[item,[item,] ]
    //result:[]
    //childKey:.0
    //func:c => c
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  }
  //當mappedChild是單個ReactElement並且不為null的時候
  else if (mappedChild != null) {
    if (isValidElement(mappedChild)) {
      //賦給新對象除key外同樣的屬性,替換key屬性
      mappedChild = cloneAndReplaceKey(
        mappedChild,
        // Keep both the (mapped) and old keys if they differ, just as
        // traverseAllChildren used to do for objects as children
        //如果新老keys是不一樣的話,兩者都保留,像traverseAllChildren對待objects做的那樣
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      );
    }
    //result即map時,return的result
    result.push(mappedChild);
  }
}

解析:
(1)讓child調用func方法,所得的結果如果是數組的話繼續遞歸;如果是單個ReactElement的話,將其放入result數組中

(2)cloneAndReplaceKey()字如其名,就是賦給新對象除key外同樣的屬性,替換key屬性

簡單看下源碼:

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

  return newElement;
}

(3)isValidElement() 判斷是否為ReactElement
簡單看下源碼:

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

traverseAllChildren()

作用:
traverseAllChildrenImpl的觸發器

源碼:

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

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

traverseAllChildrenImpl()

作用:
核心遞歸函數,目的是展平嵌套數組

源碼:

// children, '', mapSingleChildIntoContext, traverseContext
function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  //traverseContext=
  // {
  //  result:[],
  //  keyPrefix:'',
  //  func:(item)=>{return [item,[item,] ]},
  //  context:undefined,
  //  count:0,
  // }
  traverseContext,
) {
  const type = typeof children;

  if (type === 'undefined' || type === 'boolean') {
    //以上所有的被認為是null
    // All of the above are perceived as null.
    children = null;
  }
  //調用func的flag
  let invokeCallback = false;

  if (children === null) {
    invokeCallback = true;
  } else {
    switch (type) {
      case 'string':
      case 'number':
        invokeCallback = true;
        break;
      case 'object':
        //如果props.children是單個ReactElement/PortalElement的話
        //遞歸traverseAllChildrenImpl時,<span>1<span/>和<span>2<span/>作為child
        //必會觸發invokeCallback=true
        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.
      //.$=0

      //<span>1<span/> key='.0'
      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++) {
      //<span>1</span>
      child = children[i];
      //不手動設置key的話第一層第一個是.0,第二個是.1
      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,
        );
      }
    }
    //如果是一個純對象的話,throw error
    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;
}

解析:
分為兩部分:
(1)childrenObject,並且$$typeofREACT_ELEMENT_TYPE/REACT_PORTAL_TYPE

調用callbackmapSingleChildIntoContext,複製除key外的屬性,替換key屬性,將其放入到result

(2)childrenArray
循環children,再用traverseAllChildrenImpl執行child

三、流程圖

React源碼解析之React.children.map()

四、根據React.Children.map()的算法出一道面試題

數組扁平化處理:
實現一個flatten方法,使得輸入一個數組,該數組裡面的元素也可以是數組,該方法會輸出一個扁平化的數組

// Example
let givenArr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
let outputArr = [1,2,2,3,4,5,5,6,7,8,9,11,12,12,13,14,10]
// 實現flatten方法使得flatten(givenArr)——>outputArr

解法一:根據上面的流程圖使用遞歸

function flatten(arr){
    var res = [];
    for(var i=0;i<arr.length;i++){
        if(Array.isArray(arr[i])){
            res = res.concat(flatten(arr[i]));
        }else{
            res.push(arr[i]);
        }
    }
    return res;
}

解法二:ES6

function flatten(array) {
      //只要數組中的元素有一個嵌套數組,就合併
      while(array.some(item=>Array.isArray(item)))
        array=[].concat(...array)

      console.log(array) //[1,2,2,3,4,5,5,6,7,8,9,11,12,12,13,14,10]
      return array
    }

React源碼解析之React.children.map()

(完)

相關文章

(iOS)向Hero致敬與分析(一)Double研究所

React之childExpirationTime

React源碼解析之flushWork

React源碼解析之scheduleWork(下)