react 遍历对象_React 源码系列 | React Children 详解
本文基于 React V16.8.6,本文代碼地址
- 測試代碼
- 源碼講解
React 中一個元素可能有 0 個、1 個或者多個直接子元素,React 導出的 Children 中包含 5 個處理子元素的方法。
- map 類似 array.map
- forEach 類似 array.forEach
- count 類似 array.length
- toArray
- only
React 內部處理 Children 的幾個重要函數包括
- mapChildren
- traverseAllChildrenImpl
- mapIntoWithKeyPrefixInternal
- mapSingleChildIntoContext
- getPooledTraverseContext
- releaseTraverseContext
源碼都在 packages/react/src/ReactChildren.js 中。
導出的語句
export {forEachChildren as forEach,mapChildren as map,countChildren as count,onlyChild as only,toArray, };Children API
map
類似 array.map,但有一下幾個不同點:
- 返回的結果一定是一個一維數組,多維數組會被自動攤平
- 對返回的每個節點,如果 isValidElement(el) === true ,則會給它加上一個 key,如果元素本來就有 key,則會重新生成一個新的 key
map 的用法:第一個參數是要遍歷的 children,第二個參數是遍歷的函數,第三個是 context,執行遍歷函數時的 this。
如果 children == null,則直接返回了。
mapChildren
/*** Maps children that are typically specified as `props.children`.* 用來遍歷 `props.children`** @param {?*} children Children tree container.* @param {function(*, int)} func The map function.* @param {*} context Context for mapFunction.* @return {object} Object containing the ordered map of results.*/ function mapChildren(children, func, context) {if (children == null) {return children;}// 遍歷出來的元素會丟到 result 中最后返回出去const result = [];mapIntoWithKeyPrefixInternal(children, result, null, func, context);return result; }mapIntoWithKeyPrefixInternal
將 children 完全遍歷,遍歷的節點最終全部存到 array 中,是 ReactElement 的節點會更改 key 之后再放到 array 中。
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {// 這里是處理 key,不關心也沒事let escapedPrefix = '';if (prefix != null) {escapedPrefix = escapeUserProvidedKey(prefix) + '/';}// getPooledTraverseContext 和 releaseTraverseContext 是配套的函數// 用處其實很簡單,就是維護一個大小為 10 的對象重用池// 每次從這個池子里取一個對象去賦值,用完了就將對象上的屬性置空然后丟回池子// 維護這個池子的用意就是提高性能,畢竟頻繁創建銷毀一個有很多屬性的對象消耗性能const traverseContext = getPooledTraverseContext(array, // result escapedPrefix, // ''func, // mapFunccontext, // context);// 最核心的一句traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);releaseTraverseContext(traverseContext); }getPooledTraverseContext
getPooledTraverseContext 和 releaseTraverseContext,這兩個函數是用來維護一個對象池,池子最大為10。Children 需要頻繁的創建對象會導致性能問題,所以維護一個固定數量的對象池,每次從對象池拿一個對象進行復制,使用完將各個屬性 reset。
const POOL_SIZE = 10; const traverseContextPool = []; // 返回一個傳入參數構成的對象 // traverseContextPool 長度為 0 則自己構造一個對象出來,否則從 traverseContextPool pop 一個對象 // 再對這個對象的各個屬性進行賦值 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,};} }releaseTraverseContext
將 getPooledTraverseContext 產生的對象加入數組中,對象池 >= 10 則不用管
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);} }traverseAllChildren
沒太多好說的
function traverseAllChildren(children, callback, traverseContext) {if (children == null) {return 0;}return traverseAllChildrenImpl(children, '', callback, traverseContext); }traverseAllChildrenImpl
它的作用可以理解為
- children 是可渲染節點,則調用 mapSingleChildIntoContext 把 children 推入 result 數組中
- children 是數組,則再次對數組中的每個元素調用 traverseAllChildrenImpl,傳入的 key 是最新拼接好的
- children 是對象,則通過 children[Symbol.iterator] 獲取到對象的迭代器 iterator, 將迭代的結果放到 traverseAllChildrenImpl 處理
函數核心作用就是通過把傳入的 children 數組通過遍歷攤平成單個節點,然后去執行 mapSingleChildIntoContext。
這個函數比較復雜,函數簽名是這樣的
- children 要處理的 children
- nameSoFar 父級 key,會一層一層拼接傳遞,用 : 分隔
- callback 如果當前層級是可渲染節點,undefined、boolean 會變成 null,string、number、$$typeof 是 REACT_ELEMENT_TYPE 或者 REACT_PORTAL_TYPE,會調用 mapSingleChildIntoContext 處理
- traverseContext 對象池中拿出來的一個對象
mapSingleChildIntoContext
將 child 推入 traverseContext 的 result 數組中,child 如果是 ReactElement,則更改 key 了再推入。
只有當傳入的 child 是可渲染節點才會調用。如果執行了 mapFunc 返回的是一個數組,則會將數組放到 mapIntoWithKeyPrefixInternal 繼續處理。
/*** @param bookKeeping 就是我們從對象池子里取出來的東西,`traverseContext`* @param child 傳入的節點,`children`* @param childKey 節點的 key,`nameSoFar`*/ function mapSingleChildIntoContext(bookKeeping, child, childKey) {const {result, keyPrefix, func, context} = bookKeeping; // traverseContext// func 就是我們在 React.Children.map(this.props.children, c => c)// 中傳入的第二個函數參數let mappedChild = func.call(context, child, bookKeeping.count++);// 判斷函數返回值是否為數組// 因為可能會出現這種情況// React.Children.map(this.props.children, c => [c, c])// 對于 c => [c, c] 這種情況來說,每個子元素都會被返回出去兩次// 也就是說假如有 2 個子元素 c1 c2,那么通過調用 React.Children.map(this.props.children, c => [c, c]) 后// 返回的應該是 4 個子元素,c1 c1 c2 c2if (Array.isArray(mappedChild)) {// 是數組的話就回到最先調用的函數中// 然后回到之前 traverseAllChildrenImpl 攤平數組的問題// 假如 c => [[c, c]],當執行這個函數時,返回值應該是 [c, c]// 然后 [c, c] 會被當成 children 傳入// traverseAllChildrenImpl 內部邏輯判斷是數組又會重新遞歸執行// 所以說即使你的函數是 c => [[[[c, c]]]]// 最后也會被遞歸攤平到 [c, c, c, c]mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);} else if (mappedChild != null) {// 不是數組且返回值不為空,判斷返回值是否為有效的 Element// 是的話就把這個元素 clone 一遍并且替換掉 keyif (isValidElement(mappedChild)) {mappedChild = cloneAndReplaceKey(mappedChild,// Keep both the (mapped) and old keys if they differ, just as // traverseAllChildren used to do for objects as childrenkeyPrefix +(mappedChild.key && (!child || child.key !== mappedChild.key)? escapeUserProvidedKey(mappedChild.key) + '/': '') +childKey,);}result.push(mappedChild);} }map 測試
map 代碼就是上面這些,寫一個 demo 看看執行過程。
class Child extends Component {render() {console.log('this.props.children', this.props.children)const c = React.Children.map(this.props.children, c => {debuggerreturn c})console.log('mappedChildren', c)return <div>{c}</div>} }export default class Children extends Component {render() {// return 的代碼包含 2 種情況:children 是和不是數組return (<Child><div>childrendasddadas<div>childrendasddadas</div><div>childrendasddadas</div></div><div key="key2">childrendasddadas</div><div key="key3">childrendasddadas</div>{[<div key="key4">childrendasddadas</div>,<div key="key5=">childrendasddadas</div>,<div key="key6:">childrendasddadas</div>,]}</Child>)} }打印的結果如下
React.Children.map 就是把傳進去的 this.props.children 全部攤平,最后返回的一定是一維數組,數組中的對象都會添加上 key 屬性。對 mappedChildren key 的生成做分析如下。
this.props.children 自身是一個數組,在第一次調用 traverseAllChildrenImpl 時,nextName 為 .0,第一個 child 執行 traverseAllChildrenImpl 時,invokeCallback 為 true,nameSoFar 為 .0,再執行 mapSingleChildIntoContext 走到 cloneAndReplaceKey ,新 key 生成為 .0(因為 (mappedChild.key && (!child || child.key !== mappedChild.key) 為 false,keyPrefix 為空字符串)。
第二個和第三個 child 的 key 加上了 .$,在 traverseAllChildrenImpl 中,遍歷到第二個和第三個下標時 nextName = nextNamePrefix + getComponentKey(child, i);,nextNamePrefix 是 .,i 是 2、3,getComponentKey 執行,由于它有自己的 key,所以 escape 后變成 . + $key2 => .$key2,.$key3 同理。
function escape(key) {const escapeRegex = /[=:]/g;const escaperLookup = {'=': '=0', // 替換 =':': '=2', // 替換 :};const escapedString = ('' + key).replace(escapeRegex, function(match) {return escaperLookup[match];});return '$' + escapedString; // 返回的字符串前面加上 $ }第四、五、六個是嵌套在數組里面的,同上面,this.props.children 遍歷到這個數組的時候索引為 3。傳給下一輪 traverseAllChildrenImpl 的 nameSoFar 為 .3、child 為數組,下一 輪traverseAllChildrenImpl ,children 是一個數組,對其進行遍歷,nextNamePrefix 是 .3:,由下面這句計算出來。
const nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;而 getComponentKey(child, i),由于數組中的每個元素有自己的 key,所以返回的是 $key4、 $key5=0 和 $key6=2,拼接出來就是 .3:$key4 、.3:$key5=0、.3:$key6=2,這里第五、六個的 = 和 : 被 escape 處理成了 =0 和 =2。
上面例子代碼 debugger 時的調用棧:
下面貼一張 map 的流程圖。
forEach
類似 array.forEach。
和 map 的不同之處是傳給 getPooledTraverseContext 的參數 result 為 null,因為 forEach 只需要遍歷,不需要返回一個數組。另外 traverseAllChildren 它的第二個參數變成了 forEachSingleChild。
它沒有 map 那么復雜。
forEachChildren
調用 traverseAllChildren 讓每個 child 都被放到 forEachSingleChild 中執行
/*** Iterates through children that are typically specified as `props.children`.* The provided forEachFunc(child, index) will be called for each* leaf child.** @param {?*} children Children tree container. `this.props.children`* @param {function(*, int)} forEachFunc 遍歷函數* @param {*} forEachContext Context for forEachContext. 遍歷函數的上下文*/ function forEachChildren(children, forEachFunc, forEachContext) {if (children == null) {return children;}const traverseContext = getPooledTraverseContext(null,null,forEachFunc,forEachContext,);traverseAllChildren(children, forEachSingleChild, traverseContext);releaseTraverseContext(traverseContext); }forEachSingleChild
把 children 中的每個元素放到 func 中執行
/*** 把 `children` 中的每個元素放到 `func` 中執行** @param bookKeeping traverseContext* @param child 單個可 render child* @param name 這里沒有用到*/ function forEachSingleChild(bookKeeping, child, name) {const {func, context} = bookKeeping;func.call(context, child, bookKeeping.count++); }count
計算 children 的個數,計算的是攤平后數組元素的個數
countChildren
traverseAllChildren 有一個返回值 subtreeCount,表示子節點的個數,traverseAllChildren 遍歷所有 child 之后,subtreeCount 會統計出結果。
/*** 計算 children 的個數,計算的是攤平后數組元素的個數* Count the number of children that are typically specified as* `props.children`.** @param {?*} children Children tree container.* @return {number} The number of children.*/ function countChildren(children) {return traverseAllChildren(children, () => null, null); }toArray
同 mapChildren(children, child => child, context)
/*** 是 `mapChildren(children, child => child, context)` 版本* Flatten a children object (typically specified as `props.children`) and* return an array with appropriately re-keyed children.*/ function toArray(children) {const result = [];mapIntoWithKeyPrefixInternal(children, result, null, child => child);return result; }only
如果參數是一個 ReactElement,則直接返回它,否則報錯,用在測試中,正式代碼沒什么用。
/*** Returns the first child in a collection of children and verifies that there* is only one child in the collection.* The current implementation of this function assumes that a single child gets* passed without a wrapper, but the purpose of this helper function is to* abstract away the particular structure of children.** @param {?object} children Child collection structure.* @return {ReactElement} The first and only `ReactElement` contained in the* structure.*/ function onlyChild(children) {invariant(isValidElement(children),'React.Children.only expected to receive a single React element child.',);return children; }function isValidElement(object) {return (typeof object === 'object' &&object !== null &&object.$$typeof === REACT_ELEMENT_TYPE); }導出的函數中,map 是最復雜的,把每個函數的意義和簽名都讀懂之后我對整體有了比較深的認識。看一看流程圖,整個過程就清楚了。
總結
以上是生活随笔為你收集整理的react 遍历对象_React 源码系列 | React Children 详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 线性回归 - 多元线性回归案例 - 分析
- 下一篇: granule size oracle,