感謝 yck: 剖剖析 React 原始碼解析,本篇文章是在讀完他的文章的基礎上,將他的文章進行拆解和加工,加入我自己的一下理解和例子,便於大家理解。覺得yck寫的真的很棒 。React 版本為 16.8.6,關於原始碼的閱讀,可以移步到yck react原始碼解析
本文永久有效連結: react解析 React.Children(二)
在React實際開發中, React.Children
這個API我們雖然使用的比較少, 但是我們通過這個API可以操作children
, 可以檢視文件
我們來看下這個API的神奇用法
React.Children.map(this.props.children, c => [[c, c]])複製程式碼
下面可以看一下它在專案中的實際用法:
控制檯列印渲染的節點和props,如下圖 從上圖可以得知,通過 c => [[c, c]]
轉換以後節點變為了:
// 通過 c => [[c, c]] 轉換以後
<div>
<p>1</p>
<p>1</p>
<p>2</p>
<p>2</p>
</div>複製程式碼
我們需要定位到 ReactChildren.js
檔案,檢視程式碼, React.Children.map 方法實際就是mapChildren函式,讓我們來看看 mapChildren 內部到底是如何實現的吧!
function mapChildren(children, func, context) {
if (children == null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
// 這裡是處理 key,不關心也沒事
let escapedPrefix = '';
if (prefix != null) {
escapedPrefix = escapeUserProvidedKey(prefix) + '/';
}
const traverseContext = getPooledTraverseContext(
array,
escapedPrefix,
func,
context,
);
traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
releaseTraverseContext(traverseContext);
}複製程式碼
getPooledTraverseContext 和 releaseTraverseContext 中的程式碼, 引入了物件重用池的概念。這個概念的用處就是維護一個大小固定的物件重用池,每次從這個池子裡取一個物件去賦值,用完之後就將物件上的屬性清空然後丟回池子。維護這個池子的用意就是提高效能,避免頻繁建立銷燬多屬性物件。
雖然在呼叫了traverseAllChildren函式,實際呼叫的是traverseAllChildrenImpl方法。
function traverseAllChildrenImpl( children, nameSoFar, callback,traverseContext ) {
const type = typeof children;
if (type === 'undefined' || type === 'boolean') {
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,
nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
);
return 1;
}
let child;
let nextName;
let subtreeCount = 0;
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') {
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,
);
}
}
}
return subtreeCount;
}複製程式碼
這個函式首先 判斷 children 的型別, 若children為陣列,繼續遞迴呼叫traverseAllChildrenImpl
,直到處理成單個可渲染的節點,然後呼叫才能呼叫callback,也就是mapSingleChildIntoContext
。
最後讓我們來讀一下 mapSingleChildIntoContext 函式的實現。
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,
keyPrefix +
(mappedChild.key && (!child || child.key !== mappedChild.key)
? escapeUserProvidedKey(mappedChild.key) + '/'
: '') +
childKey,
);
}
result.push(mappedChild);
}
}複製程式碼
mapSingleChildIntoContext
函式其實就是呼叫React.Children.map(children, callback)
中的callback. 如果map之後還是陣列, 那麼再次進入mapIntoWithKeyPrefixInternal, 那麼這個時候我們就會再次從物件重用池裡面去獲取context, 而物件重用池的意義也就是在這裡, 如果迴圈巢狀多了, 可以減少很多物件建立和gc的損耗. 如果不是陣列, 判斷返回值是否是有效的 Element, 驗證通過的話就 clone 一份並且替換掉 key, 最後把返回值放入 result 中, result 其實也就是 mapChildren 的返回值.
下面是程式碼的呼叫順序:
mapChildren 函式
|
\|/
mapIntoWithKeyPrefixInternal 函式
|
\|/
traverseAllChildrenImpl函式(迴圈成單個可渲染的節點,如果不是遞迴)
|
|單個節點
\|/mapSingleChildIntoContext函式(判斷是否是有效Element, 驗證通過就 clone 並且替換掉 key,
並將值放入result,result就是map的返回值)複製程式碼
更多內容:
react解析: React.createElement(一)
參考:
Jokcy 的 《React 原始碼解析》: react.jokcy.me/