1、例子javascript
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 ()=>(
<ChildrenDemo>
<span key={'.0/'}>1</span>
<span>2</span>
</ChildrenDemo>
)
複製代碼
props.children :php
React.Children.map(props.children, item => [item, [item, [item]]] :html
看到一個有趣的現象,就是多層嵌套的數組[item, [item, [item]]]
通過map()
後,平鋪成[item,item,item]
了,接下來以該例解析React.Child.map()
java
2、React.Children.map()
做用:
zh-hans.reactjs.org/docs/react-…react
源碼:web
// 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
結果面試
3、mapIntoWithKeyPrefixInternal()
做用:getPooledTraverseContext()
/traverseAllChildren()
/releaseTraverseContext()
的包裹器算法
源碼:api
//第一次: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()
mapSingleChildIntoContext
是traverseAllChildren(children, mapSingleChildIntoContext, traverseContext)
的第二個參數,爲避免講traverseAllChildren
要調頭看這個 API,就先分析下
做用:
遞歸還是數組的child
;
將單個ReactElement
的child
加入result
中
源碼:
//bookKeeping:traverseContext=
// {
// result:[],
// keyPrefix:'',
// func:(item)=>{return [item,[item,] ]},
// context:undefined,
// count:0,
// }
//child:<span>1<span/>
//childKey:.0
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
//解構賦值
const {result, keyPrefix, func, context} = bookKeeping;
//func:(item)=>{return [item,[item,] ]},
//item即<span>1<span/>
//第二個參數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時,<span>1<span/>和<span>2<span/>做爲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
//<span>1<span/> 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++) {
//<span>1</span>
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)children
是Object
,而且$$typeof
是REACT_ELEMENT_TYPE
/REACT_PORTAL_TYPE
調用callback
即mapSingleChildIntoContext
,複製除key
外的屬性,替換key
屬性,將其放入到result
中
(2)children
是Array
循環children
,再用traverseAllChildrenImpl
執行child
3、流程圖
4、根據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<arr.length;i++){
if(Array.isArray(arr[i])){
res = res.concat(flatten(arr[i]));
}else{
res.push(arr[i]);
}
}
return res;
}
複製代碼
解法二: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
}
複製代碼
(完)