最近闲来无事,研究一波React源码,一开始是以Vue源码起步的,结果发现我对Vue实在是不熟悉,看Vue源码还不够格,相比而言,我更喜欢React,可能是因为第一个学的框架学的就是React,所以对React更加的充满热情,也更加的熟练,个人观点,React还是要比Vue牛逼一点好看一点的。
React本身的源码是很少的,根据打包出来的Commonjs版本看来,React只有两千多行代码,但是ReactDom据说有两万多行,框架开发者实属伟大!致敬!!!
那么这一篇是React一些通用的API概况和React.Children方法的解析,如有不到位或错误的地方欢迎指教,我的邮箱 1103107216@qq.com 您也可以下方评论。
我发现有两种方式,一种呢就是从github
上拉取react
项目的源码,github地址大家可以自己找,git clone
下来之后,在/packages/react
下面就是react
的源码了,可以看到下面是分成了很多个小文件的,这个我一般用来看的不是用来调试的。
另一个呢就是建一个项目,安装一下cnpm i react react-dom -S
之后在node_modules
里面找到react
的源码,建一个项目,用webpack
打包,装个babel
一套,毕竟es6比es5好使多了,开个热更新,之后就直接修改这个node_modules
里面的源码进行打印调试了,我个人喜欢console.log
不解释,只有在调试一些算法问题时我才会开Debug模式。
首先先来一个简单的 React 应用,这边使用es6的class写法,个人建议多练练函数式编程,写函数组件比写class舒服多了,毕竟React16提供了这么多强大的Hook
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的原因,慢慢的亲切感。
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里面的所有的通用方法,这边只做一个概览,每一个具体的用处会在后面进行详细的介绍。
这个里面封装的是对一个组件的子组件进行遍历等的一些操作,我们一般不会用到,讲真我除了看源码会用他来试一试其他的真没见到有人用它。
forEach,map 类似于数组的遍历对象遍历啥的
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
的实现。