资讯专栏INFORMATION COLUMN

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

kuangcaibao / 2578人阅读

摘要:一例子看到一个有趣的现象,就是多层嵌套的数组经过后,平铺成了,接下来以该例解析二作用源码进行基本的判断和初始化后,调用该方法就是重命名了,即解析注意,该数组在里面滚了一圈后,会结果三作用的包裹器源码第一次第二次如果字符串中有连续多个的话

一、例子

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 ()=>(
  
    1
    2
  
)

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:1

//childKey:.0
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  //解构赋值
  const {result, keyPrefix, func, context} = bookKeeping;
  //func:(item)=>{return [item,[item,] ]},
  //item即1
  //第二个参数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时,12作为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

      //1 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++) {
      //1
      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

调用callback mapSingleChildIntoContext ,复制除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

解法二: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
    }

(完)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106359.html

相关文章

  • 基于React版本16.4的源码解析(一)

    摘要:本次分析的源码采用的是的版本核心接口提供了处理的工具集我们先来看看做了什么事情即当为空时,返回不为时调用,最终返回一个数组这里说一下,可以通过传入的对所有子组件进行操作,具体使用方法看下图参数通过配合的例子把父组件的赋值给每个子组件我们先不 本次分析的源码采用的是16.4.1的版本 核心接口 showImg(https://segmentfault.com/img/bVbeT9f?w=...

    joywek 评论0 收藏0
  • 剖析 React 源码:先热个身

    摘要:我们先来看下这个函数的一些神奇用法对于上述代码,也就是函数来说返回值是。不管你第二个参数的函数返回值是几维嵌套数组,函数都能帮你摊平到一维数组,并且每次遍历后返回的数组中的元素个数代表了同一个节点需要复制几次。这是我的 React 源码解读课的第一篇文章,首先来说说为啥要写这个系列文章: 现在工作中基本都用 React 了,由此想了解下内部原理 市面上 Vue 的源码解读数不胜数,但是反观...

    sean 评论0 收藏0
  • antd源码解读(3)- Button

    Button Button包括了两个组件,Button与ButtonGroup。 ButtonProps 看一个组件首先看的是他的传参也就是props,所以我们这里先看Button组件的ButtonProps export type ButtonType = primary | ghost | dashed | danger; export type ButtonShape = circl...

    miguel.jiang 评论0 收藏0
  • React 设计模式和场景分析

    摘要:这一周连续发表了两篇关于的文章组件复用那些事儿实现按需加载轮子应用设计之道化妙用其中涉及到组件复用轮子设计相关话题,并配合相关场景实例进行了分析。 showImg(https://segmentfault.com/img/remote/1460000014482098); 这一周连续发表了两篇关于 React 的文章: 组件复用那些事儿 - React 实现按需加载轮子 React ...

    avwu 评论0 收藏0

发表评论

0条评论

kuangcaibao

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<