美文网首页
React源码解析之React.children.map()

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

作者: 小进进不将就 | 来源:发表于2019-08-04 18:43 被阅读0次

    一、例子

    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.Children.map(props.children, item => [item, [item, [item]]] :

    看到一个有趣的现象,就是多层嵌套的数组[item, [item, [item]]]经过map()后,平铺成[item,item,item]了,接下来以该例解析React.Child.map()

    二、React.Children.map()
    作用:
    https://zh-hans.reactjs.org/docs/react-api.html#reactchildren

    源码:

    // 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.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()

          本文链接:https://www.haomeiwen.com/subject/dulprctx.html