React源码解析(二):关于React.Children,你了解多少?


前言

上一篇文章说了React的一部分基础API,今天这一篇文章说一下React.children

React.children

在React.js中,摘取出Children,其中罗列了React.children的几个方法

const React = {
  Children: {
    map,
    forEach,
    count,
    toArray,
    only,
  },
}
  • map
  • forEach
  • count
  • toArray
  • only

几种方法的使用

有这样一段代码

import React from 'react';

function ChildrenDemo(props{
    console.log(props.children, 'props.children');
    console.log(React.Children.map(props.children, item => item), 'map');
    console.log(React.Children.map(props.children, item => [item, [item, item]]), 'map');
    console.log(React.Children.forEach(props.children, item => item), 'forEach');
    console.log(React.Children.forEach(props.children, item => [item, [item, item]]), 'forEach');
    console.log(React.Children.toArray(props.children), 'toArray');
    console.log(React.Children.count(props.children), 'count');
    console.log(React.Children.only(props.children[0]), 'only');
    return props.children
}

export default () => (
    <ChildrenDemo>
        <div>1</div>
        <span>2</span>
    </ChildrenDemo>

)

我们看一下,控制台的输出结果:

我们看到

  • map方法:根据传参的函数不同,输出的结果也不同,它的结果是一个数组,里面包含了各个节点的信息
  • forEach方法:无论传递什么参数,返回的结果都是undefined
  • toArray方法:返回了一个数组,里面包含着各个节点的信息
  • count方法:返回了一个数字,这个数字是节点的个数
  • only方法:我们传了一个节点给only方法,同时返回了节点信息

看到这里,大家心里肯定有一个疑问,为什么会返回这些结果呢?下面,我们通过分析源码来获得我们想要的答案

源码分析

我们通过断点的方式一步一步地分析源码流程,并在最后绘制出流程图,加深理解

PS:引用react的时候引入react打包之后的文件(react.development.js)

import React from './react.development.js';

map方法(item => item)

import React from './react.development.js';

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

export default () => (
    <ChildrenDemo>
        <div>1</div>
        <span>2</span>
    </ChildrenDemo>

)

在react.development.js中,找到关于map方法的所有函数,在需要的地方打上断点,我们看它是如何执行的。

通过断点,map方法先执行mapChildren函数

function mapChildren(children, func, context{
    //判断传入的children是否为null
    if (children == null) {
        // 为null,直接返回children
        return children;
    }
    // 不为null,定义一个result
    var result = [];
    // 调用函数,并传入相应的五个参数
    mapIntoWithKeyPrefixInternal(children, result, null, func, context);
    // 返回result
    return result;
}
  • 调用的mapIntoWithKeyPrefixInternal方法
// 方法接收五个参数
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context){
    // 定义escapedPrefix,方便后面传参
    var escapedPrefix = '';
    // 判断传入的参数prefix不为null
    if (prefix != null) {
        // 如果prefix不为空,则调用escapeUserProvidedKey方法,传入prefix,在获得的结果后加上'/'
        escapedPrefix = escapeUserProvidedKey(prefix) + '/';
    } 
    // 调用getPooledTraverseContext方法传入四个参数,将获得的结果赋值为traverseContext,方便为下面函数传参
    var traverseContext = getPooledTraverseContext(array, escapedPrefix, func, context);
    // 调用traverseAllChildren方法,传入三个参数,其中mapSingleChildIntoContext是一个函数,这个函数的作用就是将嵌套的数组展开
    traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
    // 调用releaseTraverseContext方法,传入参数
    releaseTraverseContext(traverseContext);
}
  • 调用的escapeUserProvidedKey方法
const userProvidedKeyEscapeRegex = /\/+/g;
// 匹配连续的'\'并替换为'$&/'
function escapeUserProvidedKey(text{
    return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/');
}
  • 接下来,分析调用的getPooledTraverseContext方法

该方法的主要作用是:创建一个对象池,复用Object,从而减少很多对象创建带来的内存占用和gc(垃圾回收)的损耗

// 这里定义了一个size为10的缓冲池
const POOL_SIZE = 10;
var traverseContextPool = [];
function getPooledTraverseContext(mapResult, keyPrefix, mapFunction, mapContext{
  if (traverseContextPool.length) { // 如果缓冲池中有值,则取出一个值使用
    var 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,
      count0
    };
  }
}

上面的两种情况的返回值就是调用函数时接收的变量值。

  • 在取到traverseContext值后,开始调用traverseAllChildren方法
// 函数传入了三个参数,第一个参数时children,第二个参数是一个mapSingleChildIntoContext函数,第三个参数时我们经过上面方法返回的值
function traverseAllChildren(children, callback, traverseContext{
    // 假设子节点为空,直接返回0
    if (children == null) {
        return 0;
    }
    // 否则调用traverseAllChildrenImpl函数
    return traverseAllChildrenImpl(children, '', callback, traverseContext);
}


// mapSingleChildIntoContext函数 
function mapSingleChildIntoContext(bookKeeping, child, childKey{
  const {result, keyPrefix, func, context} = bookKeeping;

  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    // 判断mappedChild是不是一个数组,如果是,再次调用mapIntoWithKeyPrefixInternal函数
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {// 如果不是数组,并且mappedChild部位null
    // 判断mappedChild在isValidElement函数中的返回值是不是true,是才可以调用cloneAndReplaceKey方法,传入了mappedChild节点的key值
    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中加入处理好的mappedChild节点,result是我们在控制台打印出来的值
    result.push(mappedChild);
  }
}
  • 调用的traverseAllChildrenImpl方法(核心方法)
// 函数接收了四个参数
function traverseAllChildrenImpl(children, nameSoFar, callback, traverseContext{
    // 首先会判断children的类型
    var type = typeof children;
    // 如果类型为undefined或者是布尔类型,让children为null
    if (type === 'undefined' || type === 'boolean') {
        // All of the above are perceived as null.
        children = null;
    }
    // 定义一个布尔类型变量,用来判断是否要调用传进来的callback
    var invokeCallback = false;

    if (children === null) { // 假如子节点为null,让标识变量变为true-
        invokeCallback = true;
    } else { // 假如与上相反,会判断type的具体类型
        switch (type) {
            case 'string':
            case 'number'// 是数字,将标识变为true
                invokeCallback = true;
                break;
            case 'object'// 是object,继续判断子节点中的$$typeof
                switch (children.$$typeof) {
                    // 是'REACT_ELEMENT_TYPE'类型,不做任何处理
                    case REACT_ELEMENT_TYPE:
                    case REACT_PORTAL_TYPE: // 是REACT_PORTAL_TYPE将标志改为true
                        invokeCallback = true;
                }
        }
    }
    // 假如invokeCallback为true,会调用传进来的callback,也就是mapSingleChildIntoContext函数,这里会返回新的参数
    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.
            // SEPARATOR是key最开始有的'.',这里是当传入的nameSoFar为空时,要调用getComponentKey方法,
            nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar);
        return 1;
    }

    var child = void 0;
    var nextName = void 0;
    var subtreeCount = 0// Count of children found in the current subtree.
    // 定义一个nextNamePrefix,这里是第二层子节点的key值处理,SUBSEPARATOR初值为':'
    var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
    // 判断children是不是一个数组
    if (Array.isArray(children)) { // 是数组,会循环子节点
        for (var i = 0; i < children.length; i++) {
            child = children[i];
            // 这里调用了getComponentKey方法,处理节点的key值
            nextName = nextNamePrefix + getComponentKey(child, i);
            // 统计子节点个数,会继续调用traverseAllChildrenImpl,这次给函数传递的是子节点,也就是json类型的数据,运用了递归的思想
            subtreeCount += traverseAllChildrenImpl(child, nextName, callback, traverseContext);
        }
    } else { // 不是数组
        var iteratorFn = getIteratorFn(children);
        if (typeof iteratorFn === 'function') {
            {
                // Warn about using Maps as children
                if (iteratorFn === children.entries) { // 报警告
                    !didWarnAboutMaps ? warning$1(false'Using Maps as children is unsupported and will likely yield ' + 'unexpected results. Convert it to a sequence/iterable of keyed ' + 'ReactElements instead.') : void 0;
                    didWarnAboutMaps = true;
                }
            }

            var iterator = iteratorFn.call(children);
            var step = void 0;
            var 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') {
            var addendum = '';
            {
                addendum = ' If you meant to render a collection of children, use an array ' + 'instead.' + ReactDebugCurrentFrame.getStackAddendum();
            }
            var childrenString = '' + children;
            (function ({
                {
                    {
                        throw ReactError(Error('Objects are not valid as a React child (found: ' + (childrenString === '[object Object]' ? 'object with keys {' + Object.keys(children).join(', ') + '}' : childrenString) + ').' + addendum));
                    }
                }
            })();
        }
    }
    // 返回了子节点数量
    return subtreeCount;
}
  • 在最后调用了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);
    }
}

然后返回的result就是我们在控制台输出的结果。

map方法(item => [item, [item, item])

import React from '../react.development.js';

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

export default () => (
    <ChildrenDemo>
        <div>1</div>
        <span>2</span>
    </ChildrenDemo>

)

这时候,传入的展开数组函数方法变得不一样了,我们还是按照上述的方法再走一遍流程,看看两者之间有什么不同和相同。

我们通过debugger发现,函数的执行流程和上述并没有什么不用,只是某几个函数执行的次数发生了变化。

  • traverseAllChildrenImpl函数
  • mapSingleChildIntoContext函数

forEach方法

我们知道在es6语法中map和forEach方法都是遍历一个数组,在这里边也是同样的,只是map方法有返回,而forEach方法没有,所以这篇文章不再对forEach进行讲解。

toArray

import React from '../react.development.js';

function ChildrenDemo(props{
    console.log(React.Children.toArray(props.children), 'toArray');
    return props.children
}

export default () => (
    <ChildrenDemo>
        <div>1</div>
        <span>2</span>
    </ChildrenDemo>

)

通过的debugger,我们发现它接下来要走的流程和map函数是一样的。

count

import React from '../react.development.js';

function ChildrenDemo(props{
    console.log(React.Children.count(props.children), 'count');
    return props.children
}

export default () => (
    <ChildrenDemo>
        <div>1</div>
        <span>2</span>
    </ChildrenDemo>

)

count的入口函数

function countChildren(children{
    return traverseAllChildren(children, function ({
        return null;
    }, null);
}

它调用了展平数组的函数,我们在上面写源码过程的时候,在traverseAllChildrenImpl函数中计算了节点的个数。

only

import React from '../react.development.js';

function ChildrenDemo(props{
    console.log(React.Children.only(props.children[0]), 'only');
    return props.children
}

export default () => (
    <ChildrenDemo>
        <div>1</div>
        <span>2</span>
    </ChildrenDemo>

)

在上面,我们在控制台打印的结果是一个json数据,包括了我们传进去节点的信息。下面,我们来看一下,它的源码部分

function onlyChild(children{
    // 一个闭包函数,假设传入的不是一个节点,那么会抛出异常
    (function ({
        if (!isValidElement(children)) {
            {
                throw ReactError(Error('React.Children.only expected to receive a single React element child.'));
            }
        }
    })();
    // 如果没问题,就会返回传过来的节点信息
    return children;
}

我们把React.children的五个方法都一一分析完毕,我们发现除了onlyChild的方法都执行了共同的方法。所以,为了能更加清晰和更好地的理解React.children方法的流程,我们画一张流程图感受一下(这里只画map方法的)

在es5、es6中数组展平的应用

  • es5
function mapChildren(array) {
    var result = [];
    for(var i = 0;i < array.length; i++) {
        if (Array.isArray(array[i])) {
            // 递归思想
            result = result.concat(mapChildren(array[i]))
        } else {
            result.push(array[i])
        }
    } 
    return result;
}

const result = mapChildren([1,[1,2,[3,4,5]]])
console.log(result);   // [1,1,2,3,4,5]
  • es6
function mapChildren(array) {
    while(array.some(item => Array.isArray(item)))
    array = [].concat(...array);
    return array
}

const result = mapChildren([1,[1,2,[3,4,5]]])
console.log(result);   // [1,1,2,3,4,5]

总结

React.children的源码至此全部分析完毕,我们要学习到框架的思想,拓展我们的思维,将这些思想运用到实战中,并且改善编码习惯,写出高质量的代码~

上述文章如有不对之处,还请大家指点出来,我们共同学习,共同进步~

最后,分享一下我的公众号【web前端日记】,关注后有资料可以领取(一般人我不告诉哦)~

往期推荐


评论
 上一篇
下一篇 
React源码解析(一):这些React的API你都知道吗 React源码解析(一):这些React的API你都知道吗
写在前面 Vue、React和Angular成为了前端工作者最常用的三大框架。分析其源码,对我们技术的提升有着至关重要的作用,我们先分析React源码,本篇文章将从其最常用的API说起 一、准备开始 1、React源码地址:从github上
2020-07-20