最开始React.Children这个 API 是不想讲的,一方面平时不怎么用,另一方面跟数组处理功能差不多,不深究实现是比较容易理解的。但是后来实际去看了一下源码之后发现,他的实现方式还是非常有趣的,尤其是mapforEach,我们就按照map的流程来看一下,forEach其实差不多,只是没有返回新的节点。

先来看一下流程图:

map流程

当然这么看肯定云里雾里,接下去会对各个函数进行讲解,然后再回过头来配合图片观看更好理解。

开始

function mapChildren(children, func, context) {
    if (children == null) {
        return children;
    }
    const result = [];
    mapIntoWithKeyPrefixInternal(children, result, null, func, context);
    return result;
}

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);
}

mapforEach的最大区别就是有没有return result

getPooledTraverseContext就是从pool里面找一个对象,releaseTraverseContext会把当前的context对象清空然后放回到pool中。

const POOL_SIZE = 10;
const traverseContextPool = [];
function getPooledTraverseContext() {
    // args
    if (traverseContextPool.length) {
        const traverseContext = traverseContextPool.pop();
        // set attrs
        return traverseContext;
    } else {
        return {
            /* attrs */
        };
    }
}

function releaseTraverseContext(traverseContext) {
    // clear attrs
    if (traverseContextPool.length < POOL_SIZE) {
        traverseContextPool.push(traverseContext);
    }
}

那么按照这个流程来看,是不是pool永远都只有一个值呢,毕竟推出之后操作完了就推入了,这么循环着。答案肯定是否的,这就要讲到React.Children.map的一个特性了,那就是对每个节点的map返回的如果是数组,那么还会继续展开,这是一个递归的过程。接下去我们就来看看。

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

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

function traverseAllChildrenImpl(
    children,
    nameSoFar,
    callback,
    traverseContext
) {
    const type = typeof children;

    if (type === "undefined" || type === "boolean") {
        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,
            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") {
            // iterator,和array差不多
        } else if (type === "object") {
            // 提醒不正确的children类型
        }
    }

    return subtreeCount;
}

这里就是一层递归了,对于可循环的children,都会重复调用traverseAllChildrenImpl,直到是一个节点的情况,然后调用callback,也就是mapSingleChildIntoContext

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,
                keyPrefix +
                    (mappedChild.key &&
                    (!child || child.key !== mappedChild.key)
                        ? escapeUserProvidedKey(mappedChild.key) + "/"
                        : "") +
                    childKey
            );
        }
        result.push(mappedChild);
    }
}

mapSingleChildIntoContext这个方法其实就是调用React.Children.map(children, callback)这里的callback,就是我们传入的第二个参数,并得到map之后的结果。注意重点来了,如果map之后的节点还是一个数组,那么再次进入mapIntoWithKeyPrefixInternal,那么这个时候我们就会再次从pool里面去context了,而pool的意义大概也就是在这里了,如果循环嵌套多了,可以减少很多对象创建和gc的损耗。

而如果不是数组并且是一个合规的ReactElement,就触达顶点了,替换一下key就推入result了。

React 这么实现主要是两个目的:

  1. 拆分map出来的数组
  2. 因为对Children的处理一般在render里面,所以会比较频繁,所以设置一个pool减少声明和gc的开销

这就是Children.map的实现,虽然不算什么特别神奇的代码,但是阅读一下还是挺有意思的。

results matching ""

    No results matching ""

    Jokcy的二维码

    扫码添加Jokcy,更多更新更优质的前端学习内容不断更新中,期待与你一起成长!

    Jokcy的二维码