react.children官方API ,閱讀全文,可以瞭解react.children基本原理,掌握react.children各個API的用法,還能瞭解到官方API以外的補充用法。
預備知識: react元素Obj的$$typeof有兩種,可以用ReactElement.js中的isValidElement來區分:
$$typeof: REACT_PORTAL_TYPE ------------------------- isValidElement返回false
$$typeof: REACT_ELEMENT_TYPE ------------------------- isValidElement返回true
複製程式碼
而Obj.type.$$typeof有五種,可以用packages\shared\isValidElementType.js來檢測是否是合法的ElementType,都返回true。
$$typeof: REACT_FORWARD_REF_TYPE
$$typeof: REACT_MEMO_TYPE
$$typeof: REACT_CONTEXT_TYPE
$$typeof: REACT_PROVIDER_TYPE
$$typeof: REACT_LAZY_TYPE
複製程式碼
內部工具函式
traverseContextPool資料結構
//資料結構:context池,大小為10。當做一個棧使用
const POOL_SIZE = 10;
const traverseContextPool = [];
//獲取一個context
//給棧頂的context設定相應屬性值,並彈出返回。
//如果棧中沒有元素,直接返回一個物件,相應的設定了屬性值
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,
};
}
}
//如果棧未滿,push一個空context物件
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);
}
}
複製程式碼
escape
將傳入的key中所有的'='替換成'=0',':'替換成 '=2',並在key之前加上'$'
function escape(key) {
const escapeRegex = /[=:]/g;
const escaperLookup = {
'=': '=0',
':': '=2',
};
const escapedString = ('' + key).replace(escapeRegex, function(match) {
return escaperLookup[match];
});
return '$' + escapedString;
}
複製程式碼
getComponentKey
如果component存在不為null的key,則返回escape(component.key),否則返回index.toString(36)
function getComponentKey(component, index) {
// Do some typechecking here since we call this blindly. We want to ensure
// that we don't blog 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
// 轉換成36進位制
return index.toString(36);
}
複製程式碼
traverseAllChildrenImpl
Children不能是一個物件 程式碼有點長,簡述其作用:輸入children樹,返回樹中節點型別是string,number,或者節點的即$$typeof為REACT_ELEMENT_TYPE,REACT_PORTAL_TYPE的節點數量。因此React.Fragment的$$typeof也為REACT_ELEMENT_TYPE,所以React.Fragment為一個節點。如果children是Array或者其他型別的子節點,則遞迴呼叫traverseAllChildrenImpl,直到children的typeof是string,number,或者$$typeof為REACT_ELEMENT_TYPE,REACT_PORTAL_TYPE時,對該children執行callback函式,並返回1。注意:不是對所有的節點遍歷。
callback傳入的引數為traverseContext,children,nameSoFar 其中nameSoFar === '' ? '.' + getComponentKey(children, 0) : nameSoFar。
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,
);
複製程式碼
可以參看blog/D3檔案下的reactchildren.vsdx檔案中的流程圖以及react資料夾下對應的原始碼註釋。
function traverseAllChildrenImpl(
children,
nameSoFar,
callback,
traverseContext,
){...}
複製程式碼
traverseAllChildren
traverseAllChildrenImpl呼叫封裝,與其功能一樣。
forEachSingleChild
執行bookKeeping.func,並將bookKeeping.count的值加1。func傳入的引數為bookKeeping.context,child以及bookKeeping.count。
function forEachSingleChild(bookKeeping, child, name) {
const {func, context} = bookKeeping;
//執行bookKeeping.func,bookKeeping.count計數增加一
func.call(context, child, bookKeeping.count++);
}
複製程式碼
forEachChildren
通過呼叫getPooledTraverseContext將傳入的引數forEachFunc以及forEachContext賦值給traverseContext的func與context屬性。 呼叫traverseAllChildren
function forEachChildren(children, forEachFunc, forEachContext) {
if (children == null) {
return children;
}
const traverseContext = getPooledTraverseContext(
null,
null,
forEachFunc,
forEachContext,
);
traverseAllChildren(children, forEachSingleChild, traverseContext);
releaseTraverseContext(traverseContext);
}
複製程式碼
escapeUserProvidedKey
匹配一個或者多個 "/",並用'$&/'替換
const userProvidedKeyEscapeRegex = /\/+/g;
function escapeUserProvidedKey(text) {
return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/');
}
複製程式碼
mapIntoWithKeyPrefixInternal
呼叫escapeUserProvidedKey對傳入的prefix進行處理得到escapedPrefix,載 通過呼叫getPooledTraverseContext將傳入的引數array、escapedPrefix、func以及context賦值給traverseContext的result、keyPrefix、func與context屬性。 呼叫traverseAllChildren。最後清除traverseContext上的屬性,併入棧。
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);
}
複製程式碼
mapSingleChildIntoContext
對children執行func(func為傳入的React.Children.map中的func), 如果返回了一個陣列,則對這個陣列呼叫mapIntoWithKeyPrefixInternal目的是新增特定的key 克隆以child節點為根節點的樹中的所有child,替換掉每個新child元素的key,push到bookKeeping中的result
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
const {result, keyPrefix, func, context} = bookKeeping;
let mappedChild = func.call(context, child, bookKeeping.count++);
if (Array.isArray(mappedChild)) {
//如果child中包含多個child,則返回的mappedChild是一個陣列,則遞迴呼叫mapIntoWithKeyPrefixInternal
mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
} else if (mappedChild != null) {
//如果child中只有一個child,並且是合法的react元素,
// 則將mappedChild的key屬性值替換掉
// 最後將新的react元素push到bookKeeping.result
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);
}
}
複製程式碼
對外介面
mapChildren
React.Children.map(children, func, context)
複製程式碼
引數描述:
children:
可以是一個物件,但是必須具備屬性$$typeof為Symbol.for('react.portal')或者Symbol.for('react.element'),可以稱其為類reactChild物件,否則報錯。
children為null或者undefined就返回null或者undefined,children中的Fragment為一個子元件。
func:
對符合規定的children執行的函式,func會被傳入兩個引數,符合規定的children以及到當前children的數量。所有執行func返回的children都會新增到一個陣列中,沒有巢狀。
context:
一般都為null,如果傳入context則func執行中的this為context,看例2
複製程式碼
返回值: 返回一個平面陣列,看例1
例子相關程式碼,見runLogic資料夾的index.js:
//
<App>
{/*測試*/}
<Header/>
<Content/>
string 1
<React.Fragment>
Some text.
<h2>A heading</h2>
</React.Fragment>
<Footer>覆蓋</Footer>
string 2
</App>
//
class App extends React.Component{
render(){
例1程式碼
例2程式碼
return (
<div>
...
</div>
)
}
}
複製程式碼
例1:測試children是一個巢狀結構,返回的陣列是否是一個平面陣列:
let reactChildLike = {$$typeof:Symbol.for('react.element')}
let complexChildren = [reactChildLike,[reactChildLike,this.props.children]]
console.log(React.Children.map(complexChildren,(children)=>[children,children,children]))
複製程式碼
結果:
this.props.children為:
(6) [{…}, {…}, "string 1", {…}, {…}, "string 2"]
complexChildren中符合規定的child為1+1+6=8,所以輸出的result為3*8=24個元素的平面陣列
(24) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, "string 1", "string 1", "string 1", {…}, {…}, {…}, {…}, {…}, {…}, "string 2", "string 2", "string 2"]
複製程式碼
例2:測試context的作用
let reactChildLike = {$$typeof:Symbol.for('react.element')}
let func = function (child) {
console.log(this)
this.a=1000;
return child
}
let contextTest = {a:1}
console.log("React.Children.map test",React.Children.map(reactChildLike,func,contextTest))
console.log("contextTest.a",contextTest.a)
複製程式碼
結果:
React.Children.map test
[{…}]
0: {$$typeof: Symbol(react.element), type: undefined, key: ".0", ref: undefined, props: undefined, …}length: 1__proto__: Array(0)
contextTest.a 1000
複製程式碼
func給this.a賦值為1000,在傳入context的時候,外部的context.a變成了1000。
原始碼:
function mapChildren(children, func, context) {
//children為null或者undefined就返回null或者undefined
if (children == null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}
複製程式碼
執行邏輯:
1. 初始呼叫
mapChildren函式中:mapChildren傳入(children,func,context),呼叫mapIntoWithKeyPrefixInternal(children, [], null, func, context)
2. 構建traverseContext
mapIntoWithKeyPrefixInternal函式中:prefix為傳入的第三個引數null,此時traverseContextPool=[],直接返回
traverseContext={
result: [],
keyPrefix: null,
func: func,
context: context,
count: 0,
}
複製程式碼
呼叫traverseAllChildren(children, mapSingleChildIntoContext, traverseContext)
3. children構建key
traverseAllChildren函式中:呼叫traverseAllChildrenImpl(children, '', mapSingleChildIntoContext, traverseContext)
traverseAllChildrenImpl函式中: 如果children是符合規定的則呼叫mapSingleChildIntoContext(traverseContext,children,'.’+children的key)。
4. 對children執行func,並新增到結果陣列
mapSingleChildIntoContext函式中:traverseContext.func為mapChildren函式接收到的func,呼叫traverseContext.func.call(context, children, traverseContext.count++)。如果傳入mapChildren的func對該children進行了改造。並返回了一陣列的新children,則呼叫mapIntoWithKeyPrefixInternal為符合規定的成員新增特定的Key之後,新增到result陣列中。
toArray
利用mapChildren也能實現toArray的功能,只需要func為child => child即可
function toArray(children) {
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, child => child);
return result;
}
複製程式碼
onlyChild
判斷children是否是單個React element child
function onlyChild(children) {
invariant(
isValidElement(children),
'React.Children.only expected to receive a single React element child.',
);
return children;
}
複製程式碼
countChildren
計算children個數
function countChildren(children) {
return traverseAllChildren(children, () => null, null);
}
複製程式碼