文章目錄
React.Children.map
先來看這樣一段代碼:
import React from 'react';
function Father(props){
console.log(props.children);
console.log(React.Children.map(props.children, item=>[item, item]));
return props.children;
}
export default () => (
<Father>
<div>hello</div>
<div>hello</div>
<div>hello</div>
</Father>
);
代碼很簡單,在頁面上只是顯示了三行"hello"而已。重點不在這裏,而是在於通過console.log
在控制檯輸出的內容。
我們知道可以通過組件的props.children
獲得該組件的子組件。那麼可以想到console.log(props.children);
預期的結果應該是打印出一個長度爲3的數組。
事實也確實如此,控制檯中輸出了一個長度爲3的ReactElement
數組。
那麼console.log(React.Children.map(props.children, item=>[item, item]));
會在控制檯輸出什麼呢?
我們可能會猜想它應該與數組的map
方法一樣,返回一個長度爲3的二維數組,每個元素又是一個長度爲2的ReactElement
數組。
可事實卻並不是這樣,控制檯輸出的是一個長度爲6的ReactElement
數組。可以想見,React.map
將我們設想中的二維數組給降維了。
具體過程是怎樣,需要看看源碼。
源碼閱讀
React.js
首先在源碼目錄下的React.js
可以發現map
是在此處成爲React.Children
的一個方法的。
接着可以發現它真正的定義是在同目錄下的ReactChildren.js
文件中。
ReactChildren.js
在ReactChildren.js
文件的最尾部,可以看到map
方法在此處被導出,其真正的名字應該是mapChildren
。
mapChildren()
function mapChildren(children, func, context) {
if (children == null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}
這個方法接受3個參數children
、func
和context
。
children
就是將要被遍歷的子組件數組,func
是對單個子組件需要執行的函數,context
則是func
執行時this
指針所指向的對象。
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);
}
這個方法本身並沒有什麼可說的,值得說的是其中被調用的getPooledTraverseContext
、traverseAllChildren
和releaseTraverseContext
。
getPooledTraverseContext()
getPooledTraverseContext()
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,
};
}
}
traverseContextPool
是文件中定義的對象池,POOL_SIZE
則定義了對象池的大小。
getPooledTraverseContext
方法就是從對象池中獲得一個context
對象。
邏輯是若對象池已存在對象則pop
出來,並將我們調用React.map
時傳入的一些參數賦值給context
對象中相應的屬性。
若對象池爲空,則直接返回一個新的context
對象。
對象池
對象池的作用是避免頻繁的創建和銷燬,以避免不必要的性能消耗和內存抖動的問題。
traverseAllChildren()
function traverseAllChildren(children, callback, traverseContext) {
if (children == null) {
return 0;
}
return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
這個方法用以判斷傳入的子組件是否爲空,不是空的再繼續調用traverseAllChildrenImpl
方法。
值得注意的是,參數中的callback
並不是我們傳入的回調函數,而是之前在mapIntoWithKeyPrefixInternal
中傳入的mapSingleChildIntoContext
。
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);
}
}
在前面通過getPooledTraverseContext
獲得到的context
對象在使用過後,會通過這個方法將對象內屬性清空並重新放入對象池中(當對象池還有空間時)。
看到這裏可能大家會有一個疑問。最開始的時候對象池爲空,於是直接返回了一個新的context
對象,使用完之後,通過
releaseTraverseContext
方法放回對象池中。而在又一次從對象池中獲取對象的過程中,獲得到的正是我們剛剛放進去的那一個,那麼豈不是對象池中始終只有一個對象?我們接着往下看。
traverseAllChildrenImpl()
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;
}
這部分代碼算得上是React.map
實現的核心之一,我們逐段逐段的看。
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;
}
}
}
這段代碼是用來判斷傳入的子組件的,主要是判斷是不是單個對象以及類型。如果滿足條件,則將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;
}
若上面的代碼將invokeCallback
置爲true
,則調用callback
,注意此處的callback
不是我們傳入的回調函數,而是mapSingleChildIntoContext
。
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,
);
}
}
這段代碼則是用以處理傳入的是數組的情況的。若爲數組,則會遍歷數組,並將數組中的每一個元素作爲traverseAllChildrenImpl
的第一個參數,遞歸的調用自身。
mapSingleChildIntoContext
這個方法是React.map
實現的另一個核心。
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);
}
}
在這個方法裏,我們從context
對象中獲取到調用React.map
時傳入的回調函數,並執行。
下面這段代碼解釋了一開始的問題,爲什麼我們調用React.map
,儘管預期是一個二維數組,而返回的卻是一個一維數組。
if (Array.isArray(mappedChild)) {
mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
}
如果執行我們傳入的回調函數後返回的是一個數組,那麼則會將這個數組作爲參數,重新走一遍調用React.map
之後的流程,且此時傳入的回調函數就只返回本身。
同時這段代碼還解釋了,對象池是不是一直只有一個對象的問題。
在當我們傳入的回調函數不返回一個數組時確實是這樣的,但當返回一個數組,甚至是多維數組時,在此處由於會多次重走流程,於是也會多次向對象池獲取對象,然而第一次獲取到的對象此時還未被放回對象池中。於是便會直接返回一個新的對象,當整個方法調用完成後,對象池中便會存在多個對象了。