进击React源码之磨刀试炼部分为源码解读基础部分,会包含多篇文章,本篇为第二篇,第一篇《进击React源码之磨刀试炼1》入口(点击进入)。javascript
若是有没用过PureComponent
或不了解的同窗,能够看看这篇文章什么时候使用Component仍是PureComponent?html
Component(组件)做为React中最重要的概念,每当建立类组件都要继承Component
或PureComponent
,在未开始看源码的时候,你们能够先跟本身谈谈对于Component
和PureComponent
的印象,不妨根据经验猜一猜Component
内部将会为咱们实现怎样的功能?java
先来写个简单的组件react
class CompDemo extends PureComponent {
constructor(props) {
super(props);
this.state = {
msg: 'hello world'
}
}
componentDidMount() {
setTimeout(() => {
this.setState({
msg: 'Hello React'
});
}, 1000)
}
render() {
return (
<div className="CompDemo"> <div className="CompDemo__text"> {this.state.msg} </div> </div>
)
}
}
复制代码
Component
/
PureComponent
组件内部可能帮咱们处理了
props
,
state
,定义了生命周期函数,
setState
,
render
等不少功能。
打开packages/react/src/ReactBaseClasses.js
,打开后里面有不少英文注释,但愿你们无论经过什么手段先翻译看看,本身先大体了解一下。以后贴出的源码中我会过滤掉自带的注释和if(__DEV__)
语句,有兴趣了解的同窗能够翻阅源码研究。git
Componentgithub
function Component(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.isReactComponent = {};
Component.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
复制代码
以上就是Component相关的源码,它竟如此出奇的简洁!字面来看懂它也很简单,首先定义了Component
构造函数,以后在其原型链上设置了isReactComponent
(Component组件标志)、setState
方法和forceUpdate
方法。web
Component
构造函数能够接收三个参数,其中props
和context
咱们大多数人应该都接触过,在函数中还定义了this.refs
为一个空对象,但updater
就是一个比较陌生的东西了,在setState
和forceUpdate
方法中咱们能够看到它的使用:segmentfault
setState
并无具体实现更新state的方法,而是调用了updater
的enqueueSetState
,setState
接收两个参数:partialState
就是咱们要更新的state
内容,callback
可让咱们在state
更新后作一些自定义的操做,this.updater.enqueueSetState
在这里传入了四个参数,咱们能够猜到第一个为当前实例对象,第二个是咱们更新的内容,第三个是传入的callback
,最后一个是当前操做的名称。这段代码上面invariant
的做用是判断partialState
是不是对象、函数或者null
,若是不是则会给出提示。在这里咱们能够看出,setState
第一个参数不只能够为Object
,也能够是个函数,你们在实际操做中能够尝试使用。forceUpdate
相比于setState
,只有callback
,同时在使用enqueueForceUpdate
时候也少传递了一个参数,其余参数跟setState
中调用保持一致。这个updater.enqueueForceUpdate
来自ReactDom
,React
与ReactDom
是分开的两个不一样的内容,不少复杂的操做都被封装在了ReactDom
中,所以React
才保持如此简洁。React
在不一样平台(native和web)使用的都是相同的代码,可是不一样平台的DOM操做流程多是不一样的,所以将state
的更新操做经过对象方式传递过来,可让不一样的平台去自定义本身的操做逻辑,React
就能够专一于大致流程的实现。api
PureComponent数组
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
复制代码
看完Cpomponent
的内容,再看PureComponent
就很简单了,单看PureComponent
的定义是与Component
是彻底同样的,这里使用了寄生组合继承的方式,让PureComponent
继承了Component,以后设置了isPureReactComponent
标志为true。
若是有同窗对JavaScript继承不是很了解,这里找了一篇掘金上的文章深刻JavaScript继承原理 你们能够点击进入查看
经过ref
咱们能够得到组件内某个子节点的信息病对其进行操做,ref的使用方式有三种:
class RefDemo extends PureComponent {
constructor() {
super()
this.objRef = React.createRef()
}
componentDidMount() {
setTimeout(() => {
this.refs.stringRef.textContent = "String ref content changed";
this.methodRef.textContent = "Method ref content changed";
this.objRef.current.textContent = "Object ref content changed";
}, 3000)
}
render() {
return (
<div className="RefDemo"> <div className="RefDemo__stringRef" ref="stringRef">this is string ref</div> <div className="RefDemo__methodRef" ref={el => this.methodRef = el}>this is method ref</div> <div className="RefDemo__objRef" ref={this.objRef}>this is object ref</div> </div>
)
}
}
export default RefDemo;
复制代码
key
为所设字符串的属性,用来表示该节点的实例对象。若是该节点为dom,则对应dom示例,若是是class component
则对应该组件实例对象,若是是function component
,则会出现错误,function component
没有实例,但能够经过forward ref
来使用ref
。createRef()
建立对象,默认建立的对象为{current: null}
,将其传递个某个节点,在组件渲染结束后会将此节点的实例对象挂在到current
上源码位置packages/react/src/ReactCreactRef.js
export function createRef(): RefObject {
const refObject = {
current: null,
};
return refObject;
}
复制代码
它上方有段注释an immutable object with a single mutable value
,告诉咱们建立出来的对象具备单个可变值,可是这个对象是不可变的。在其内部跟咱们上面说的同样,建立了{current: null}
并将其返回。
forwardRef的使用
const FunctionComp = React.forwardRef((props, ref) => (
<div type="text" ref={ref}>Hello React</div>
))
class FnRefDemo extends PureComponent {
constructor() {
super();
this.ref = React.createRef();
}
componentDidMount() {
setTimeout(() => {
this.ref.current.textContent = "Changed"
}, 3000)
}
render() {
return (
<div className="RefDemo"> <FunctionComp ref={this.ref}/> </div> ) } } 复制代码
forwardRef
的使用,可让Function Component
使用ref,传递参数时须要注意传入第二个参数ref
forwardRef的实现
export default function forwardRef<Props, ElementType: React$ElementType>( render: (props: Props, ref: React$Ref<ElementType>) => React$Node, ) {
return {
$$typeof: REACT_FORWARD_REF_TYPE,
render,
};
}
复制代码
forwardRef
接收一个函数做为参数,这个函数就是咱们的函数组件,它包含props
和ref
属性,forwardRef
最终返回的是一个对象,这个对象包含两个属性:
$$typeof
:这个属性看过上一篇文章的小伙伴应该还记得,它是标志React Element类型的东西。这里说明一下,尽管forwardRef
返回的对象中$$typeof
为REACT_FORWARD_REF_TYPE
,可是最终建立的ReactElement的$$typeof仍然是REACT_ELEMENT_TYPE
这里文字描述有点绕,配合图片来看文字会好点。
在上述forwardRef使用
的代码中建立的FunctionComp
是{$$typeof:REACT_FORWARD_REF_TYPE,render}
这个对象,在使用<FunctionComp ref={this.ref}/>
时,它的本质是React.createElement(FunctionComp, {ref: xxxx}, null)
这样的,此时FunctionComp
是咱们传进createElement
中的type
参数,createElement
返回的element
的$$typeof
仍然是REACT_ELEMENT_TYPE
;
function ParentComp ({children}) {
return (
<div className="parent"> <div className="title">Parent Component</div> <div className="content"> {children} </div> </div>
)
}
复制代码
这样的代码你们平时用的应该多一点,在使用ParentComp
组件时候,能够在标签中间写一些内容,这些内容就是children。
来看看React.Children.map的使用
function ParentComp ({children}) {
return (
<div className="parent"> <div className="title">Parent Component</div> <div className="content"> {React.Children.map(children, c => [c,c, [c]])} </div> </div>
)
}
class ChildrenDemo extends PureComponent{
constructor() {
super()
this.state = {}
}
render() {
return (
<div className="childrenDemo"> <ParentComp> <div>child 1 content</div> <div>child 2 content</div> <div>child 3 content</div> </ParentComp> </div>
)
}
}
export default ChildrenDemo;
复制代码
咱们在使用这个API的时候,传递了两个参数,第一个是children
,你们应该比较熟悉,第二个是一个回调函数,回调函数传入一个参数(表明children的一个元素),返回一个数组(数组不是一位数组,里面三个元素最后一个仍是数组),在结果中咱们能够看到,这个API将咱们返回的数组平铺为一层[c1,c1,c1,c2,c2,c2,c3,c3,c3],浏览器中显示的也就如上图所示。
有兴趣的小伙伴能够尝试阅读官方文档对于这个api的介绍
在react.js
中定义React
时候咱们能够看到一段关于Children
的定义
Children: {
map,
forEach,
count,
toArray,
only,
},
复制代码
Children包含5个API,这里咱们先详细讨论map API。这一部分并非很好懂,请你们看的时候必定要用心。
笔者读这一部分也是费了很大的劲,而后用思惟导图软件画出了这个思惟导图+流程图的东西(暂时就给它起名为思惟流程图,其实更流程一点,而不思惟),画得仍是比较详细的,因此就很大,小伙伴最好把这个图下载下来放大看(能够配合源码,也能够配合下文),图片地址user-gold-cdn.xitu.io/2019/8/21/1…
因为图过小不清楚,下面也会分别截出每一个函数的流程图。
打开packages/react/src/ReactChildren.js
,找到mapChildren
function mapChildren(children, func, context) {
if (children == null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}
复制代码
这段代码短小精悍,给咱们提供了直接使用的API。它内部逻辑也很是简单,首先看看children
是否为null
,若是若是为null
就直接返回null
,若是不是,则定义result
(初始为空数组)来存放结果,通过mapIntoWithKeyPrefixInternal
的一系列处理,获得结果。结果不论是null
仍是result
,其实咱们再写代码的时候都遇到过,若是一个组件中间什么都没传,结果就是null什么都不会显示,若是传递了一个<div>
那就显示这个div
,若是传递了一组div
那就显示这一组(此时就是children不为null的状况),最后显示出来的东西也就是result
这个数组。
这一系列处理就是什么处理?
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);
}
复制代码
在进入这个函数的时候,必定要注意使用这个函数时候传递进来的参数到底是哪几个,否则后面传递次数稍微一多就会晕头转向。
从上一个函数跳过来的时候传递了5个参数,你们能够注意一下这五个参数表明的是什么:
children
:咱们再组件中间写的JSX代码result
: 最终处理完成存放结果的数组prefix
: 前缀,这里为nullfunc
: 咱们在演示使用的过程当中传入的第二个参数,是个回调函数c => [c,c,[c]]
context
: 上下文对象这个函数首先对prefix
前缀字符串作了个处理,处理完以后仍是个字符串。而后经过getPooledTraverseContext
函数从对象重用池
中拿出一个对象,说到这里,咱们就不得不打断一下这个函数的讲解,忽然出现一个对象重用池
的概念,不少人会很懵逼,而且若是强制把这个函数解析完再继续下一个,会让不少读者产生不少疑惑,不利于后面源码的理解。
暂时跳到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,
};
}
}
复制代码
首先看在使用getPooledTraverseContext
获取对象的时候,传递了4个参数:
array
: 上个函数中对应的result
,表示最终返回结果的数组escapedPrefix
: 前缀,一个字符串,没什么好说的func
: 咱们使用API传递的回调函数 c=>[c,c,[c]]
context
: 上下文对象而后咱们看看它作了什么,它去一个traverseContextPool
数组(这个数组默认为空数组,最多存放10个元素)中尝试pop
取出一个元素,若是能取出来的话,这个元素是一个对象,有5个属性,这里会把传进来的4个参数保存在这四个元素中,方便后面使用,另一个属性是个用来计数的计数器。若是没取出来,就返回一个新对象,包含的也是这五个属性。这里要跟你们说说对象重用池
了。这个对象有5个属性,若是每次使用这个对象都从新建立一个,那么会有较大的建立对象开销,为了节省这部分建立的开销,咱们能够在使用完这个对象以后,把它的5个属性都置为空(count就是0了),而后扔回这个数组(对象重用池
)中,后面要用的时候就直接从对象重用池
中拿出来,没必要从新建立对象,增长开销了。
再回到mapIntoWithKeyPrefixInternal
函数中继续向下读 经过上一步拿到一个带有5个属性的对象以后,继续通过traverseAllChildren
函数的一系列处理,获得了最终的结果result
,其中具体内容太多下面再说,而后经过releaseTraverseContext
函数释放了那个带5个参数的对象。咱们先来看看如何释放的:
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);
}
}
复制代码
这里也跟咱们上面说的对象重用池有所对应
,这里先把这个对象的5个属性清空,而后看看对象重用池是否是有空,有空的话就把这个清空的属性放进去,方便下次使用,节省建立开销。
traverseAllChildren和traverseAllChildrenImpl的实现
function traverseAllChildren(children, callback, traverseContext) {
if (children == null) {
return 0;
}
return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
复制代码
这个函数基本没作什么重要的事,仅仅判断了children
是否为null
,若是是的话就返回0,不是的话就进行具体的处理。仍是强调这里传递的参数,必定要注意,看图就能够了,就不用文字描述了。
重要的是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; // 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') {
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 = '';
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;
}
复制代码
上面的流程图说的很详细了,你们能够参照来看源码。这里就简单说一下这个函数的两部分分别做了什么事。 第一部分是对children
类型进行了检查(没有检查为Array或迭代器对象的状况),若是检查children是合法的ReactElement就会进行callback
的调用,这里必定要注意callback
传进来的是谁,这里是callback为mapSingleChildIntoContext
,一直让你们关注传参问题,就是怕你们看着看着就搞混了。 第二部分就是针对children
是数组和迭代器对象的状况进行了处理(迭代器对象检查的原理是obj[Symbol.iterator]
,比较简单你们能够本身定位源码找一下具体实现),而后对他们进行遍历,每一个元素都从新执行traverseAllChildrenImpl
函数造成递归。 它其实只让可渲染的单元素进行下一步callback
的调用,若是是数组或迭代器,就进行遍历。
最后一步callback => 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,
// 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);
}
}
复制代码
这里咱们就用到了从对象重用池拿出来的对象
,那个对象做用其实就是利用那5个属性帮咱们保存了一些须要使用的变量和函数,而后执行咱们传入的func
(c => [c,c,[c]]
),若是结果不是数组而是元素而且不为null
就会直接存储到result
结果中,若是是个数组就会对它进行遍历,从mapIntoWithKeyPrefixInternal
开始从新执行造成递归调用,直到最后将嵌套数组中全部元素都拿出来放到result
中,这样就造成了咱们最初看到的那种效果,无论咱们的回调函数是多少层的数组,最后都会变成一层。
这里文字性的小结就留给你们,给你们画了一张总结性的流程图(有参考yck大神的图),但实际上是根据本身看源码画出来的并非搬运的。
{
forEach,
count,
toArray,
only,
}
复制代码
对于这几个方法,你们能够自行查看了,建议先浏览一遍forEach
,跟map
很是类似,可是比map
少了点东西。其余几个都是四五行的代码,你们本身看看。里面用到的函数咱们上面都有讲到。
这篇文章跟你们一块儿读了Component
、refs
和Children
相关的源码,最复杂的仍是数Children
了,说实话,连看大神博客,看源码、画图带写文章,花了七八个小时,其实内容跟大神们的文章比起来仍是很不同的,若是基础不是很好的同窗,我感受这里会讲的更详细。 你们一块儿努力,明天的咱们必定会感谢今天努力的本身。
原创不易,若是本篇文章对你有帮助,但愿能够帮忙点个赞,有兴趣也能够帮忙github点个star,感谢各位。本篇文章github地址