最近閒來無事,研究一波React源碼,一開始是以Vue源碼起步的,結果發現我對Vue實在是不熟悉,看Vue源碼還未入流,相比而言,我更喜歡React,多是由於第一個學的框架學的就是React,因此對React更加的充滿熱情,也更加的熟練,我的觀點,React仍是要比Vue牛逼一點好看一點的。html
React自己的源碼是不多的,根據打包出來的Commonjs版本看來,React只有兩千多行代碼,可是ReactDom聽說有兩萬多行,框架開發者實屬偉大!致敬!!!node
那麼這一篇是React一些通用的API概況和React.Children方法的解析,若有不到位或錯誤的地方歡迎指教,個人郵箱 1103107216@qq.com 您也能夠下方評論。react
我發現有兩種方式,一種呢就是從github
上拉取react
項目的源碼,github地址你們能夠本身找,git clone
下來以後,在/packages/react
下面就是react
的源碼了,能夠看到下面是分紅了不少個小文件的,這個我通常用來看的不是用來調試的。webpack
另外一個呢就是建一個項目,安裝一下cnpm i react react-dom -S
以後在node_modules
裏面找到react
的源碼,建一個項目,用webpack
打包,裝個babel
一套,畢竟es6比es5好使多了,開個熱更新,以後就直接修改這個node_modules
裏面的源碼進行打印調試了,我我的喜歡console.log
不解釋,只有在調試一些算法問題時我纔會開Debug模式。git
首先先來一個簡單的 React 應用,這邊使用es6的class寫法,我的建議多練練函數式編程,寫函數組件比寫class舒服多了,畢竟React16提供了這麼多強大的Hookes6
import React from 'react'; class App extends React.Component { constructor(props) { super(props) } render() { return ( <div> Hello World </div> ) } } 複製代碼
OK, Hello World 致敬,咱們能夠開始幹活了。首先看一下React的源碼,在/packages/react/src/React.js
這個文件裏面,能夠看到React的定義,你會發現和Vue的源碼很不同,這也是我更喜歡React的緣由,慢慢的親切感。github
const React = { Children: { map, forEach, count, toArray, only, }, createRef, Component, PureComponent, createContext, forwardRef, lazy, memo, useCallback, useContext, useEffect, useImperativeHandle, useDebugValue, useLayoutEffect, useMemo, useReducer, useRef, useState, Fragment: REACT_FRAGMENT_TYPE, StrictMode: REACT_STRICT_MODE_TYPE, Suspense: REACT_SUSPENSE_TYPE, createElement: __DEV__ ? createElementWithValidation : createElement, cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement, createFactory: __DEV__ ? createFactoryWithValidation : createFactory, isValidElement: isValidElement, version: ReactVersion, unstable_ConcurrentMode: REACT_CONCURRENT_MODE_TYPE, unstable_Profiler: REACT_PROFILER_TYPE, // 這一行跳過 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals, }; 複製代碼
這邊定義了React裏面的全部的通用方法,這邊只作一個概覽,每個具體的用處會在後面進行詳細的介紹。web
這個裏面封裝的是對一個組件的子組件進行遍歷等的一些操做,咱們通常不會用到,講真我除了看源碼會用他來試一試其餘的真沒見到有人用它。算法
forEach,map 相似於數組的遍歷對象遍歷啥的npm
count 用來計算子組件的數量
only 官方解釋:驗證 children 是否只有一個子節點(一個 React 元素),若是有則返回它,不然此方法會拋出錯誤。 Tips:不可使用React.Children.map
方法的返回值做爲參數,由於map的返回值是一個數組而不是一個React元素
toArray 將Children按照數組的形式扁平展開並返回
搞不懂不要緊,後面會介紹,有一個印象就好
ref 屬性是在開發中常用的,說白了就是用來獲取真實Dom的,新版的React中使用ref的操做也變了
class MyComponent extends React.Component { constructor(props) { super(props); this.inputRef = React.createRef(); } // 這是一種 render() { return <input type="text" ref={this.inputRef} />; } // 這是另一種 render() { return <input type="text" ref={node => this.inputRef = node}> } } 複製代碼
這兩個你們應該都很熟悉,建立一個React組件,PureComponent在判斷組件是否改更新的時候更加的方便。
建立一個上下文,返回一個Context
對象,裏面包含了Provider,Consumer
屬性,通常用來往組件樹的更深處傳遞數據,避免一個組件一個組件的往下傳,不方便解藕
建立一個React組件,這個組件可以將其接受的 ref 屬性轉發到其組件樹下的另外一個組件中.React.forwardRef
接受渲染函數做爲參數。React 將使用 props 和 ref 做爲參數來調用此函數。此函數應返回 React 節點。
組件懶加載
const SomeComponent = React.lazy(() => import('./SomeComponent')); 複製代碼
用來建立一個HOC的
接下來這幾個就是React16大名鼎鼎的Hook函數,功能強大,函數式組件的福音,親切感倍足
這四個都是React提供的組件,但他們呢其實都只是佔位符,都是一個Symbol,在React實際檢測到他們的時候會作一些特殊的處理,好比StrictMode和AsyncMode會讓他們的子節點對應的Fiber的mode都變成和他們同樣的mode
createElement 這是React中最重要的方法了,用來建立ReactElement
顧名思義,克隆一個ReactElement
建立一個工廠,這個工廠專門用來建立某一類ReactElement
用來檢測是不是一個ReactElement
記錄React的當前版本號
React.Children 提供了用於處理 this.props.children 不透明數據結構的實用方法。
這一部分的代碼在 packages/react/react/src/ReactChildren.js
裏面,主要分裝了forEach map count only toArray
,前二者用於遍歷Reach Children。
count
用於返回該組件的children數量
only
用於判斷該組件是否是隻有一個子節點
toArray
將React.Children以扁平的形式返回出來,並附加key
在React
中,一段文本能夠被稱爲一個子節點,一段標籤也能夠被成爲一個節點。
class App extends React.Component { constructor(props) { super(props); } render() { // Hello World console.log(this.props.children); return ( <div></div> ) } } ReactDom.render( <App> // 一段文本也是一個子節點 Hello World </App> , document.getElementById('root') ); 複製代碼
class App extends React.Component { constructor(props) { super(props); } render() { // 被標記爲一個React.Element console.log(this.props.children); return ( <div></div> ) } } ReactDom.render( <App> // 一段標籤也能夠是一個子節點 <div>Hello World</div> </App> , document.getElementById('root') ); 複製代碼
在上面的示例代碼中,若是傳遞的子節點是一段html標籤,那麼打印出來的結果是這樣的:
咱們也能夠在App
組件中顯示咱們傳遞的這個Children
class App extends React.Component { constructor(props) { super(props); } render() { console.log(this.props.children); return ( <div>{ this.props.children }</div> ) } } 複製代碼
若是傳遞的是多個節點,那麼就會被解析成一個數組
<App>
<div>Hello World</div>
<div>Hello China</div>
</App>
複製代碼
那麼Reach.Children
的方法應該就是在這裏進行使用,由於我實際上也沒有使用過,作個簡單的示例,咱們能夠打印一下App
這個組件的子節點�數,使用count
方法
class App extends React.Component { constructor(props) { super(props); } render() { // 2 console.log(React.Children.count(this.props.children)); return ( <div>{ this.props.children }</div> ) } } ReactDom.render( <App> <div>Hello World</div> <div>Hello China</div> </App> , document.getElementById('root') ); 複製代碼
這邊會打印出來一個 2 由於咱們傳遞的是兩個節點
示例看完了咱們能夠來分析一下源碼了,介紹一下map
的源碼
找到ReactChildren.js
(這是在React源碼裏,不是在node_modules裏),找到最下面模塊導出語句
export { forEachChildren as forEach, mapChildren as map, countChildren as count, onlyChild as only, toArray, }; 複製代碼
能夠看到map
是mapChildren
的一個別名,下面找到這個函數
/** * Maps children that are typically specified as `props.children`. * * See https://reactjs.org/docs/react-api.html#reactchildrenmap * * The provided mapFunction(child, key, index) will be called for each * leaf child. * * @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; } const result = []; mapIntoWithKeyPrefixInternal(children, result, null, func, context); return result; } 複製代碼
方法接受三個參數,第一個參數是咱們傳遞的this.props.children
,也是必選參數,第二個參數是一個function,在遍歷的過程當中,會對每個節點都使用這個function,這個function接受一個參數,參數就是當前遍歷的節點,第三個參數是一個上下文,通常不用傳。 能夠看出重點是mapIntoWithKeyPrefixInternal
這個方法。
使用示例
class App extends React.Component { constructor(props) { super(props); } render() { React.Children.map(this.props.children, (item) => { console.log(item); }) return ( <div>{ this.props.children }</div> ) } } 複製代碼
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); } 複製代碼
首先是獲取一下遍歷的上下文,這個在後面的方法應該會用到,下面就是開始遍歷全部的Children了,重點是traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
,第一個參數好理解就是咱們傳遞的this.props.children
,第二個參數是一個方法,第三個參數就是前面獲取到的遍歷上下文。
首先看一下這個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
,我腦海中蹦出來的詞是鏈接池,因此暫且就這麼理解他,這個鏈接池的容量爲10,若是這個鏈接池裏有東西的話,也就是說這個traverseContextPool.length !== 0
的話,那麼會彈出最後一個進行賦值而後返回,若是池裏沒有東西的話就直接返回一個新的對象。
下面看重點方法traverseAllChildren
/** * Traverses children that are typically specified as `props.children`, but * might also be specified through attributes: * * - `traverseAllChildren(this.props.children, ...)` * - `traverseAllChildren(this.props.leftPanelChildren, ...)` * * The `traverseContext` is an optional argument that is passed through the * entire traversal. It can be used to store accumulations or anything else that * the callback might find relevant. * * @param {?*} children Children tree object. * @param {!function} callback To invoke upon traversing each child. * @param {?*} traverseContext Context for traversal. * @return {!number} The number of children in this subtree. */ function traverseAllChildren(children, callback, traverseContext) { if (children == null) { return 0; } return traverseAllChildrenImpl(children, '', callback, traverseContext); } 複製代碼
主要看這個方法的實現traverseAllChildrenImpl
/** * @param {?*} children Children tree container. * @param {!string} nameSoFar Name of the key path so far. * @param {!function} callback Callback to invoke with each child found. * @param {?*} traverseContext Used to pass information throughout the traversal * process. * @return {!number} The number of children in this subtree. */ 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; } 複製代碼
分步解析
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; } 複製代碼
這一塊是用來判斷 children 類型的,若是是string
好比說傳遞一個文本,number
,object
好比說一個dom節點,那麼代表 children 只是一個節點,那麼就直接執行 callback
返回一個 1
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, ); } } 複製代碼
若是咱們傳遞的是多個節點,那麼會遍歷children數組,進行遞歸遍歷,直到返回的是上面顯示的幾個類型。
上邊提到的callback
就是傳遞的mapSingleChildIntoContext
,這邊就是利用到以前的traverseContextPool
被我稱之爲鏈接池的東西.
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); } } 複製代碼
這邊的mappedChild
就是咱們傳遞的funcion的返回值,function呢就是調用React.Children.map(children,callback)
這裏的callback了,若是這個返回值返回的是一個數組的話,那麼就進行遞歸調用,這個時候就須要用到以前的鏈接池了。
採用這個鏈接池的目的我也是在其餘的地方看到了
由於對Children的處理通常在render裏面,因此會比較頻繁,因此設置一個pool減小聲明和gc的開銷
這就是React.Children.map
的實現。