React源码解析————ReactChildren.js
2021SC@SUDSC
2021SC@SUDSC
ReactChildren.js
只要是看过源码的同学都知道,这个js文件一共导出了五个方法:
export {
forEachChildren as forEach,
mapChildren as map,
countChildren as count,
onlyChild as only,
toArray,
};
然后再React.js文件中就有了:
const Children = {
map,
forEach,
count,
toArray,
only,
};
下面就由我带领大家去解析这个文件。
import
思路清晰,先说import部分,看看ReactChildren这个文件引入了什么。
import type {ReactNodeList} from 'shared/ReactTypes';
import invariant from 'shared/invariant';
import isArray from 'shared/isArray';
import {
getIteratorFn,
REACT_ELEMENT_TYPE,
REACT_PORTAL_TYPE,
} from 'shared/ReactSymbols';
import {isValidElement, cloneAndReplaceKey} from './ReactElement';
- 首先引入的是ReactTypes中的ReactNodeList
export type ReactNodeList = ReactEmpty | React$Node;
这些是flow内置的私有类型,是在flow里面定义好的,所以你在React里面找不到定义。这里我把我找到的放上来,其中React$Node在官网中的定义,也可以从this查看
declare type React$Node =
| null
| boolean
| number
| string
| React$Element<any>
| React$Portal
| Iterable<?React$Node>;
- 接下来引入的是我们的老朋友invariant,关于它的用处可以看我的上一篇博客
- isArray.js
declare function isArray(a: mixed): boolean %checks(Array.isArray(a));
const isArrayImpl = Array.isArray;
// eslint-disable-next-line no-redeclare
function isArray(a: mixed): boolean {
return isArrayImpl(a);
}
export default isArray;
isArray这个方法很简单,就是用来检查是否为数组的,但是这个形式很奇怪,而之所以会使用这个形式,是因为在Flow中直接定义判断参数的函数,会报错:
function truthy(a, b): boolean { // 报错
return a && b;
}
所以需要用 %checks 来标记这个函数是一个检查函数:
function truthy(a, b): boolean %checks { // 不报错
return a && b;
}
- ReactSymbols.js
const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator;
const FAUX_ITERATOR_SYMBOL = '@@iterator';
export function getIteratorFn(maybeIterable: ?any): ?() => ?Iterator<*> {
if (maybeIterable === null || typeof maybeIterable !== 'object') {
return null;
}
const maybeIterator =
(MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL]) ||
maybeIterable[FAUX_ITERATOR_SYMBOL];
if (typeof maybeIterator === 'function') {
return maybeIterator;
}
return null;
}
getIteratorFn方法在整个ReactChildren文件中只调用了一次,针对的是那些不是Array类型,但是有迭代器,能遍历的情况,正常代码不会这样,基本上可以忽略。 5. ReactElement.js
export function cloneAndReplaceKey(oldElement, newKey) {
const newElement = ReactElement(
oldElement.type,
newKey,
oldElement.ref,
oldElement._self,
oldElement._source,
oldElement._owner,
oldElement.props,
);
return newElement;
}
克隆一个旧的react元素,得到的新的react元素被设置了新的key,并调用ReactElement方法(该方法的分析见下一篇博客)
export function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
判断一个对象是否是合法的react元素,判断其$$typeof属性是否为REACT_ELEMENT_TYPE
map
function mapChildren(
children: ?ReactNodeList,
func: MapFunc,
context: mixed,
): ?Array<React$Node> {
if (children == null) {
return children;
}
const result = [];
let count = 0;
mapIntoArray(children, result, '', '', function(child) {
return func.call(context, child, count++);
});
return result;
}
类似 array.map,但有一下几个不同点: 1.返回的结果一定是一个一维数组,多维数组会被自动摊平 2.对返回的每个节点,如果 isValidElement(el) === true ,则会给它加上一个 key,如果元素本来就有 key,则会重新生成一个新的 key。
map 的用法:第一个参数是要遍历的 children,第二个参数是遍历的函数,第三个是 context,执行遍历函数时的 this。 如果 children == null,则直接返回了,遍历出来的元素会丢到 result 中最后返回出去
function mapIntoArray(
children: ?ReactNodeList,
array: Array<React$Node>,
escapedPrefix: string,
nameSoFar: string,
callback: (?React$Node) => ?ReactNodeList,
): number {
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: any).$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
invokeCallback = true;
}
}
}
if (invokeCallback) {
const child = children;
let mappedChild = callback(child);
// 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:
const childKey =
nameSoFar === '' ? SEPARATOR + getElementKey(child, 0) : nameSoFar;
if (isArray(mappedChild)) {
let escapedChildKey = '';
if (childKey != null) {
escapedChildKey = escapeUserProvidedKey(childKey) + '/';
}
mapIntoArray(mappedChild, array, escapedChildKey, '', 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
escapedPrefix +
// $FlowFixMe Flow incorrectly thinks React.Portal doesn't have a key
(mappedChild.key && (!child || child.key !== mappedChild.key)
? // $FlowFixMe Flow incorrectly thinks existing element's key can be a number
escapeUserProvidedKey('' + mappedChild.key) + '/'
: '') +
childKey,
);
}
array.push(mappedChild);
}
return 1;
}
}
这个函数比较复杂,函数签名是这样的 children 要处理的 children result 存储处理后children的数组 escapedPrefix是当前的key nameSoFar 是父级 key,会一层一层拼接传递,用‘ : ’ 分隔 callback是:
function(child) {
return func.call(context, child, count++);
}
如果当前children的类型是: 1.undefined、boolean 会变成 null 2.string、number、$$typeof 是 REACT_PORTAL_TYPE或REACT_ELEMENT_TYPE,会进行处理,首先判断是否为数组,若是,则再次调用自身,直到可以取出所有的子元素,详情见下面的代码,同时这样也避免了面对多重不规则嵌套时建立大量对象的问题,ex:[1,[2,[3,4]]],同时注意callback变了,变成了c=>c,意为function(c){return c};若不是数组且返回值不为空,判断返回值是否为有效的 Element,是的话就把这个元素 clone 一遍并且替换掉 key,并将其放入数组中。 3.若是数组进行下面的处理:
let child;
let nextName;
let subtreeCount = 0; // Count of children found in the current subtree.
const nextNamePrefix =
nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
if (isArray(children)) {
for (let i = 0; i < children.length; i++) {
child = children[i];
nextName = nextNamePrefix + getElementKey(child, i);
subtreeCount += mapIntoArray(
child,
array,
escapedPrefix,
nextName,
callback,
);
}
}
从上文的分析中我们知道这段代码:当节点是数组的话,就开始遍历数组,并且把数组中的每个元素再递归执行,也就是为什么上文要验证是否为数组。
else {
const iteratorFn = getIteratorFn(children);
if (typeof iteratorFn === 'function') {
const iterableChildren: Iterable<React$Node> & {
entries: any,
} = (children: any);
if (__DEV__) {
// Warn about using Maps as children
if (iteratorFn === iterableChildren.entries) {
if (!didWarnAboutMaps) {
console.warn(
'Using Maps as children is not supported. ' +
'Use an array of keyed ReactElements instead.',
);
}
didWarnAboutMaps = true;
}
}
const iterator = iteratorFn.call(iterableChildren);
let step;
let ii = 0;
while (!(step = iterator.next()).done) {
child = step.value;
nextName = nextNamePrefix + getElementKey(child, ii++);
subtreeCount += mapIntoArray(
child,
array,
escapedPrefix,
nextName,
callback,
);
}
} else if (type === 'object') {
const childrenString = '' + (children: any);
invariant(
false,
'Objects are not valid as a React child (found: %s). ' +
'If you meant to render a collection of children, use an array ' +
'instead.',
childrenString === '[object Object]'
? 'object with keys {' + Object.keys((children: any)).join(', ') + '}'
: childrenString,
);
}
}
不是数组的话,就看看 children 是否可以支持迭代,通过 obj[Symbol.iterator] 的方式去取,只有取出来对象是个函数类型才是正确的,然后获取到children的迭代器 iterator,执行迭代器,将迭代的结果传入自身,再次调用自身处理 总的来说,该函数核心作用就是通过把传入的 children 数组通过遍历摊平成单个节点,然后将其推入数组。
forEach
function forEachChildren(
children: ?ReactNodeList,
forEachFunc: ForEachFunc,
forEachContext: mixed,
): void {
mapChildren(
children,
function() {
forEachFunc.apply(this, arguments);
// Don't return anything.
},
forEachContext,
);
}
类似 array.forEach。 forEach 只需要遍历,不需要返回一个数组
count
function countChildren(children: ?ReactNodeList): number {
let n = 0;
mapChildren(children, () => {
n++;
// Don't return anything
});
return n;
}
计算 children 的个数,简单没什么好说的。
only
function onlyChild<T>(children: T): T {
invariant(
isValidElement(children),
'React.Children.only expected to receive a single React element child.',
);
return children;
}
如果参数是一个 ReactElement,则直接返回它,否则报错,用在测试中,正式代码没什么用。
toArray
function toArray(children: ?ReactNodeList): Array<React$Node> {
return mapChildren(children, child => child) || [];
}
将children转换成Array,对children排序时需要使用
|