最近闲来无事,研究一波React源码,一开始是以Vue源码起步的,结果发现我对Vue实在是不熟悉,看Vue源码还不够格,相比而言,我更喜欢React,可能是因为第一个学的框架学的就是React,所以对React更加的充满热情,也更加的熟练,个人观点,React还是要比Vue牛逼一点好看一点的。
React本身的源码是很少的,根据打包出来的Commonjs版本看来,React只有两千多行代码,但是ReactDom据说有两万多行,框架开发者实属伟大!致敬!!!
那么这一篇是React一些通用的API概况和React.Children方法的解析,如有不到位或错误的地方欢迎指教,我的邮箱 1103107216@qq.com 您也可以下方评论。
React源码获取
我发现有两种方式,一种呢就是从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模式。
通用API
首先先来一个简单的 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里面的所有的通用方法,这边只做一个概览,每一个具体的用处会在后面进行详细的介绍。
Children
这个里面封装的是对一个组件的子组件进行遍历等的一些操作,我们一般不会用到,讲真我除了看源码会用他来试一试其他的真没见到有人用它。
forEach,map 类似于数组的遍历对象遍历啥的
count 用来计算子组件的数量
only 官方解释:验证 children 是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误。 Tips:不可以使用
React.Children.map
方法的返回值作为参数,因为map的返回值是一个数组而不是一个React元素toArray 将Children按照数组的形式扁平展开并返回
搞不懂没关系,后面会介绍,有一个印象就好
createRef
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}>
}
}
Component, PureComponent
这两个大家应该都很熟悉,创建一个React组件,PureComponent在判断组件是否改更新的时候更加的方便。
createContext
创建一个上下文,返回一个Context
对象,里面包含了Provider,Consumer
属性,一般用来往组件树的更深处传递数据,避免一个组件一个组件的往下传,不方便解藕
forwardRef
创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中.React.forwardRef
接受渲染函数作为参数。React 将使用 props 和 ref 作为参数来调用此函数。此函数应返回 React 节点。
lazy
组件懒加载
const SomeComponent = React.lazy(() => import('./SomeComponent'));
memo
用来创建一个HOC的
useState...
接下来这几个就是React16大名鼎鼎的Hook函数,功能强大,函数式组件的福音,亲切感倍足
Fragment StrictMode Suspense unstable_ConcurrentMode unstable_Profiler
这四个都是React提供的组件,但他们呢其实都只是占位符,都是一个Symbol,在React实际检测到他们的时候会做一些特殊的处理,比如StrictMode和AsyncMode会让他们的子节点对应的Fiber的mode都变成和他们一样的mode
createElement
createElement 这是React中最重要的方法了,用来创建ReactElement
cloneElement
顾名思义,克隆一个ReactElement
createFactory
创建一个工厂,这个工厂专门用来创建某一类ReactElement
isValidElement
用来检测是否是一个ReactElement
version
记录React的当前版本号
React.Children 详解
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
的实现。