React16源码: React.Children源码实现
React.Children
1 ) 概述
- 这个API用的也比较的少,因为大部分情况下,我们不会单独去操作children
- 我们在一个组件内部拿到 props 的时候,我们有props.children这么一个属性
- 大部分情况下,直接把 props.children 把它渲染到我们的jsx 里面就可以了
- 很少有情况需要去操作一下这个children,但是一旦需要去操作这个children呢
- 直接使用react点children的API,而不是你直接去操作dom
- 大部分时候拿到的 children,可能是一个合理的react element,或者是一个数组
- React提供Children这个API去操作它,一定是有一个合理的原因的
2 )示例演示
import React from 'react'
function ChildrenDemo(props) {
console.log(props.children)
console.log(React.Children.map(props.children, c => [c, c]))
return props.children
}
export default () => (
<ChildrenDemo>
<span>1</span>
<span>2</span>
</ChildrenDemo>
)
- 上面这个代码非常简单,创建了一个组件叫 ChildrenDemo
- 里面有两个 span 作为children,在 props.children 里面,就可以拿到
- 第一个打印出来的就是 props.children
- 它就是两个 react element 节点
- 第二个打印的是 map 的返回值
- 我们通过react.children.map, 传入这个props.children 和一个callback
- 这个callback,返回的是一个数组, 这个数组里面,包含两个相同的节点
- 那这时候打印出来的是4个节点
- 也就是每个span都被克隆成2份,2个span是4份
- 前两个children 都是1,后两个都是2
- 再来改一下
console.log(React.Children.map(props.children, c => [c, [c, c]]))
- 它会输出六个节点
- 0, 1, 2,的children是 1
- 3, 4, 5 的 children 是2
- 也就是说 react.children的map function返回的是一个数组,它会继续把它展开
- 里面不管传了多少层嵌套的数组,最终都会展开成一层数组,即: 被拍平
- 拍平后有几个元素,map中的当前child就会被克隆成几份
- 这就是
React.Children.map
,跟普通原生的数组.map 的一个本质区别
3 )源码分析
定位到 React.js 中
const React = {
Children: {
map,
forEach,
count,
toArray,
only,
},
// ... 省略其他
};
- 这个对象里面有五个函数,跟原生数组操作非常的像
- 前两个是最重要的,就是map 和 forEach, 它和数组的意义是一样的
- 但它实际的操作可能跟数组的map和forEach会有一定的区别
- map是这些方法所有逻辑里面最复杂的一个,而 map 和 forEach 是差不多的
- 它们唯一的区别是一个有返回一个没有返回
- map是通过我们传入的一个方法之后,返回的一个新的数组
- 而forEach 中只在 null 的判断中返回,其实并非真实的返回值
再定位到 ReactChildren.js 中
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import invariant from 'shared/invariant';
import warning from 'shared/warning';
import {
getIteratorFn,
REACT_ELEMENT_TYPE,
REACT_PORTAL_TYPE,
} from 'shared/ReactSymbols';
import {isValidElement, cloneAndReplaceKey} from './ReactElement';
import ReactDebugCurrentFrame from './ReactDebugCurrentFrame';
const SEPARATOR = '.';
const SUBSEPARATOR = ':';
/**
* Escape and wrap key so it is safe to use as a reactid
*
* @param {string} key to be escaped.
* @return {string} the escaped key.
*/
function escape(key) {
const escapeRegex = /[=:]/g;
const escaperLookup = {
'=': '=0',
':': '=2',
};
const escapedString = ('' + key).replace(escapeRegex, function(match) {
return escaperLookup[match];
});
return '$' + escapedString;
}
/**
* TODO: Test that a single child and an array with one item have the same key
* pattern.
*/
let didWarnAboutMaps = false;
const userProvidedKeyEscapeRegex = /\/+/g;
function escapeUserProvidedKey(text) {
return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/');
}
const POOL_SIZE = 10;
const traverseContextPool = [];
function getPooledTraverseContext(
mapResult,
keyPrefix,
mapFunction,
mapContext,
) {
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 {
return {
result: mapResult,
keyPrefix: keyPrefix,
func: mapFunction,
context: mapContext,
count: 0,
};
}
}
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);
}
}
/**
* @param {?*} children Children tree container.
* @param {!string} nameSoFar Name of the key path so far.
* @param {!function} callback Callback to invoke with each child found.
* @param {?*} traverseContext Used to pass information throughout the traversal
* process.
* @return {!number} The number of children in this subtree.
*/
function traverseAllChildrenImpl(
children,
nameSoFar,
callback,
traverseContext,
) {
const type = typeof children;
if (type === 'undefined' || type === 'boolean') {
// All of the above are perceived as null.
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,
// 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.
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') {
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,
);
}
} 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;
}
/**
* Traverses children that are typically specified as `props.children`, but
* might also be specified through attributes:
*
* - `traverseAllChildren(this.props.children, ...)`
* - `traverseAllChildren(this.props.leftPanelChildren, ...)`
*
* The `traverseContext` is an optional argument that is passed through the
* entire traversal. It can be used to store accumulations or anything else that
* the callback might find relevant.
*
* @param {?*} children Children tree object.
* @param {!function} callback To invoke upon traversing each child.
* @param {?*} traverseContext Context for traversal.
* @return {!number} The number of children in this subtree.
*/
function traverseAllChildren(children, callback, traverseContext) {
if (children == null) {
return 0;
}
return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
/**
* Generate a key string that identifies a component within a set.
*
* @param {*} component A component that could contain a manual key.
* @param {number} index Index that is used if a manual key is not provided.
* @return {string}
*/
function getComponentKey(component, index) {
// Do some typechecking here since we call this blindly. We want to ensure
// that we don't block potential future ES APIs.
if (
typeof component === 'object' &&
component !== null &&
component.key != null
) {
// Explicit key
return escape(component.key);
}
// Implicit key determined by the index in the set
return index.toString(36);
}
function forEachSingleChild(bookKeeping, child, name) {
const {func, context} = bookKeeping;
func.call(context, child, bookKeeping.count++);
}
/**
* Iterates through children that are typically specified as `props.children`.
*
* See https://reactjs.org/docs/react-api.html#reactchildrenforeach
*
* The provided forEachFunc(child, index) will be called for each
* leaf child.
*
* @param {?*} children Children tree container.
* @param {function(*, int)} forEachFunc
* @param {*} forEachContext Context for forEachContext.
*/
function forEachChildren(children, forEachFunc, forEachContext) {
if (children == null) {
return children;
}
const traverseContext = getPooledTraverseContext(
null,
null,
forEachFunc,
forEachContext,
);
traverseAllChildren(children, forEachSingleChild, traverseContext);
releaseTraverseContext(traverseContext);
}
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,
// 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.push(mappedChild);
}
}
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);
}
/**
* Maps children that are typically specified as `props.children`.
*
* See https://reactjs.org/docs/react-api.html#reactchildrenmap
*
* The provided mapFunction(child, key, index) will be called for each
* leaf child.
*
* @param {?*} children Children tree container.
* @param {function(*, int)} func The map function.
* @param {*} context Context for mapFunction.
* @return {object} Object containing the ordered map of results.
*/
function mapChildren(children, func, context) {
if (children == null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}
/**
* Count the number of children that are typically specified as
* `props.children`.
*
* See https://reactjs.org/docs/react-api.html#reactchildrencount
*
* @param {?*} children Children tree container.
* @return {number} The number of children.
*/
function countChildren(children) {
return traverseAllChildren(children, () => null, null);
}
/**
* Flatten a children object (typically specified as `props.children`) and
* return an array with appropriately re-keyed children.
*
* See https://reactjs.org/docs/react-api.html#reactchildrentoarray
*/
function toArray(children) {
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, child => child);
return result;
}
/**
* Returns the first child in a collection of children and verifies that there
* is only one child in the collection.
*
* See https://reactjs.org/docs/react-api.html#reactchildrenonly
*
* The current implementation of this function assumes that a single child gets
* passed without a wrapper, but the purpose of this helper function is to
* abstract away the particular structure of children.
*
* @param {?object} children Child collection structure.
* @return {ReactElement} The first and only `ReactElement` contained in the
* structure.
*/
function onlyChild(children) {
invariant(
isValidElement(children),
'React.Children.only expected to receive a single React element child.',
);
return children;
}
export {
forEachChildren as forEach,
mapChildren as map,
countChildren as count,
onlyChild as only,
toArray,
};
-
翻到最下面,看到
mapChildren as map
,这边 export 出去的是map
-
对应的,我们来看
mapchildren
这个方法/** * Maps children that are typically specified as `props.children`. * * See https://reactjs.org/docs/react-api.html#reactchildrenmap * * The provided mapFunction(child, key, index) will be called for each * leaf child. * * @param {?*} children Children tree container. * @param {function(*, int)} func The map function. * @param {*} context Context for mapFunction. * @return {object} Object containing the ordered map of results. */ function mapChildren(children, func, context) { if (children == null) { return children; } const result = []; mapIntoWithKeyPrefixInternal(children, result, null, func, context); return result; }
-
开始会调用一个方法叫
mapIntoWithKeyPrefixInternal
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); }
-
代码上是先处理了一下 escapedPrefix, 这个倒没什么,后面调用的方法和
forEachChildren
是差不多的 -
进入这里的
getPooledTraverseContext
方法 -
它先判断是否已有存在池中是否节点,如果有,则pop一个
-
并将传入的内容都挂载到这个pop出来的对象上面,用于记录
-
再经过一系列作用之后,执行到上面的
releaseTraverseContext
就是对对象进行清空 -
上面的过程涉及到了一个对象池的概念,也就是缓存池,用于节省操作的性能
- js是单线程语言,对大量对象进行操作,比如挂载和删除,可能会造成内存抖动的问题
- 可能导致浏览器内的页面性能很差
- 它设置Pool Size的大小是 10,是一个渐进的过程
- 一开始是空的,随着对象的创建会进行缓存,接着复用
-
总体来说,它会做一个非常重要的事情,就是到一个叫做
contextPool
的地方去获取一个 context -
接下去所有流程都是在这个函数里面调用了
traverseAllChildren
这个方法 -
调用完所有的方法之后,它会把这个context再返回到这个
contextPool
里面 -
调用的
traverseAllChildren
方法没有什么操作,本质上调了traverseAllChildrenImpl
的方法/** * Traverses children that are typically specified as `props.children`, but * might also be specified through attributes: * * - `traverseAllChildren(this.props.children, ...)` * - `traverseAllChildren(this.props.leftPanelChildren, ...)` * * The `traverseContext` is an optional argument that is passed through the * entire traversal. It can be used to store accumulations or anything else that * the callback might find relevant. * * @param {?*} children Children tree object. * @param {!function} callback To invoke upon traversing each child. * @param {?*} traverseContext Context for traversal. * @return {!number} The number of children in this subtree. */ function traverseAllChildren(children, callback, traverseContext) { if (children == null) { return 0; } return traverseAllChildrenImpl(children, '', callback, traverseContext); }
-
进入
traverseAllChildrenImpl
这个方法会判断我们的children是否是一个数组或者是一个iterator对象 -
这代表它们是多个节点是可以遍历的, 如果它是多个节点,它会去循环每一个节点
-
然后对每一个节点再重新调用这个
traverseAllChildrenImpl
这个方法, 也就是递归实现 -
最终要传入这个
traverseAllChildrenImpl
方法的children,是以单个节点的时候 -
才会去执行真正的
mapSingleChildIntoContext
方法, 在这个方法里面会调用 -
React.Children.map
传入的第二个参数,也就是那个map function -
它会传入上面遍历出来的最终的单个节点,返回想要的map结果的一个数据
-
拿到一个map数据之后,它会进行一个判断,是否是数组,如果不是数组
-
它会在result中插入克隆节点,并替换key,防止有相同的key出现
-
如果是数组的话,又会回过头来去调用这个
mapIntoWithKeyPrefixInternal
,到这里完成了一个大的递归 -
在这么一个递归的过程下去,最终是把里面返回的所有层级的数组都进行了一个展开
-
展开之后就变成了一个一维数组, 这就是它的一个整体的流程
-
-
然后再来对比一下
forEachChildren
/** * Iterates through children that are typically specified as `props.children`. * * See https://reactjs.org/docs/react-api.html#reactchildrenforeach * * The provided forEachFunc(child, index) will be called for each * leaf child. * * @param {?*} children Children tree container. * @param {function(*, int)} forEachFunc * @param {*} forEachContext Context for forEachContext. */ function forEachChildren(children, forEachFunc, forEachContext) { if (children == null) { return children; } const traverseContext = getPooledTraverseContext( null, null, forEachFunc, forEachContext, ); traverseAllChildren(children, forEachSingleChild, traverseContext); releaseTraverseContext(traverseContext); }
- 可以看到它最后没有 return,这就是
forEachChildren
跟mapChildren
的本质区别
- 可以看到它最后没有 return,这就是
-
关于export 出去的
toArray
function toArray(children) { const result = []; mapIntoWithKeyPrefixInternal(children, result, null, child => child); return result; }
toArray
和mapChildren
唯一的区别就是 map function- 换句话说,它的map function,其实就是
child => child
- 它其实也会把数组展开,只是说没有map的过程
-
还有 export 出去的
onlyChild
function onlyChild(children) { invariant( isValidElement(children), 'React.Children.only expected to receive a single React element child.', ); return children; }
- 其实就是判断一下这个children是否是单个的合理的 react element 节点
- 如果是的话,就返回,不是的话,给出提醒
-
最后 export 出去的
countChildren
function countChildren(children) { return traverseAllChildren(children, () => null, null); }
- 内部调用
traverseAllChildren
, 本质上还是调用traverseAllChildrenImpl
- 最终返回的是统计后的值
subtreeCount
- 内部调用
-
最后还有一个 节点 key 相关的处理,主要核心实现是在
getComponentKey
也是个递归的处理- 打印出的每个节点上,都会有一个key,这个key的处理也是比较核心的
- 参考 ChildrenDemo中的 React.Children.map 中的回调
c => [c, [c,c]]
这里会总计打印出6个节点- 可以看到第一个节点是 .0/.0
- 然后第二个节点是 .0/.1:0
- 然后第三个节点是 .0/.1:1
- …
- 可以按照上述函数和顶层声明的两个变量
SEPARATOR
和SUBSEPARATOR
- 理解下这个打印出的结果
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!