写在前面
Vue、React和Angular成为了前端工作者最常用的三大框架。分析其源码,对我们技术的提升有着至关重要的作用,我们先分析React源码,本篇文章将从其最常用的API说起
一、准备开始
1、React源码地址:从github上下载源码
2、React官方文档:准备好文档,便于参考
二、绘制思维导图
根据官方文档以及源码,画出一张思维导图
这些API将是这篇文章着重介绍的React API
三、API
3.1 JSX到JS的转换
3.1.1 介绍
在React中,我们经常编写JSX,JSX和JS最大的不同就是:在JSX中,可以写HTML的代码。那么React是如何解析JSX的呢?这里面就涉及到了JSX向JS的转换,在React中,JSX通过Babel转换成JS去编译。接下来,我们来研究Babel是如何将JSX转换成JS的。
首先推荐一个测试工具:Babel转换工具
在JSX中先写一对div标签
<div></div>
它通过Babel转换成了这样
React.createElement("div", null)
逐步增加标签内的内容
转换前 | 转换后 |
---|---|
<div>我被转换了</div> |
React.createElement("div", null, '我被转换了') |
<div key="1">我被转换了</div> |
React.createElement("div", {key: "1"}, 我被转换了) |
<div key="1" id="2">我被转换了</div> |
React.createElement("div",{key: "1",id: "2"},"我被转换了") |
<div><span>001</span><span>002</span></div> |
React.createElement("div", null, React.createElement("span", null),React.createElement("span",null)) |
通过上面这些情况,可以总结出:
- 在React中,节点由
React.createElement
函数创建 ,它接收三个参数(标签名称,标签属性,标签内容) - 其中标签属性是一个对象,当没有属性时,接收参数为
null
,第三个参数会根据标签内容判断,所以可能会出现第四个参数、第五个参数…
在React中,我们经常要封装自定义组件,那么对自定义组件和原生DOM节点的解析有何不同?
// Test组件时一个自定义组件
function Test() {
return <a>我是一个自定义组件</a>
}
// 使用Test自定义组件
<Test>测试</Test>
被Babel编译后
function Test() {
return React.createElement("a", null, "\u6211\u662F\u4E00\u4E2A\u81EA\u5B9A\u4E49\u7EC4\u4EF6");
}
React.createElement(Test, null, "\u6D4B\u8BD5");
可以看到:
- 中文字符被转义
- 转换原则和上面好像没有什么不同
- 仔细观察,我们发现,在
React.createElement
函数中的第一个参数是一个变量,而不是像上面一样是一个字符串,这是因为Babel区分了自定义组件和原生DOM节点 - 自定义组件第一个字母大写,假如我们自定义的组件第一个字母为小写,Babel还会正常转换,但当React发现这个组件不是原生DOM节点的时候React会报错
3.1.2 小结
在React中,经常使用JSX,我们也首先要了解JSX向JS转换的原理,其次才能做其他部分的事情。
3.2 ReactElement
3.2.1 绘制思维导图
3.2.2 介绍
我们要知道的第一个API就是ReactElment,它的源码的文件名是ReactElement.js。上面的思维导图就是这个.js文件大概所要阐述的,中心主题是我们所说的ReactElement,第二层节点是所有的方法,第三层节点是方法所接收的参数,第四层及以后是参数上的属性,每个方法最后会返回一个方法,虚线是最后返回ReactElemnt方法的部分。有了这样一个思路以后,再看源码就不会感觉到那么难了。
我摘取了以下几个部分
1.这里面使用了很多Object构造函数中的方法,比如getPropertyDescriptor()
、defineProperty()
等等,这需要我们去了解这些方法的用途。
2.在createElemnt
方法中,有这样一段代码
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}
createElement
方法中会接收三个参数,这部分代码是处理第三个参数children
的,我们在3.1中介绍JSX
向JS
中转换说到的createElement
方法其实就是它,createElement
方法接收的参数个数由父节点的内容所控制(参见3.1),所以我们看到的childrenLength
的计算-2
,就是减掉了这个方法接收的前两个参数,然后呢,根据它的长度判断chilren
的值,最后传给了props
的children
属性
3.对defaultProps的处理
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
这部分也是在createElement
方法中处理的,它是对第一个参数type
中的defaultProps
属性的处理,在我们自己封装组件时,可能会让一些属性有默认值,在父组件中使用时,会判断父组件是否给子组件传值,如果没有,就会使用defaultProps
中定义的默认值
4.ReactElement方法接收了七个参数,并最终返回了element。剩下的部分大多和这些思想一致,就不一一累述。
3.2.3 小结
学习源码是一个枯燥的过程,而这仅仅是一个API介绍,是一个开始,我们要做的就是学习源码的思考问题的方式等,让自己思考问题更加全面,学会写更优秀的代码。我同时也推荐先画思维导图这种方式,理解,再深入研究每个方法的用途,这么写代码的目的。
3.3 ReactComponent
3.3.1 绘制思维导图
3.3.2 介绍
Component源码是在ReactBaseClasses文件中,同样在这个文件中存在的还有一个PureComponent源码,最终在文件中export {PureComponent, Component}
。这部分代码不是很长,我们依然挑出重点的部分进行分享:
- this.setState
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');
};
这部分代码就是我们常用的this.setState
方法的源码部分,它是存在于Component的原型链上的方法,接受了两个参数,第一个参数是要更新state的部分,第二个参数是一个回调函数,走进函数,首先是要判断partialState
的类型,如果类型不正确,将会报错;正式更新state的部分是this.updater.enqueueSetState(this, partialState, callback, 'setState')
,这里面调用了updater
中的enqueueSetState
方法,它接收了四个参数,那么它是如何更新state的呢?我们先留一个悬念…
3.4 ref、forwardref
3.4.1 绘制思维导图
3.4.2 React中使用ref、forwardRef的实例
1、ref
ref的使用有三种方式:
① stringRef
② methodRef(function)
③ createRef
import React, {Component} from 'react';
export default class Ref extends Component {
constructor (props) {
super();
this.objRef = React.createRef()
this.state = {
ref: '初始化'
}
}
componentDidMount() {
setTimeout(() => {
this.refs.stringRef.textContent = '我加载成功了'
this.methodRef.textContent = '我也加载成功了'
this.objRef.current.textContent = '我同样加载成功了'
}, 2000)
}
render() {
const { ref } = this.state;
return (
<div>
<h3 ref="stringRef">{ref}</h3>
<h3 ref={ele => (this.methodRef = ele)}>{ref}</h3>
<h3 ref={this.objRef}>{ref}</h3>
</div>
)
}
}
2、forward-ref
forward-ref的出现是为了解决HOC组件传递ref的问题
import React, {Component} from 'react';
const Target = React.forwardRef((props, ref) => {
return (
<input ref={ref} />
)
})
export default class Forward extends Component {
constructor(props) {
super();
this.ref = React.createRef();
this.state = {
}
}
componentDidMount(){
this.ref.current.value = 'forward ref';
}
render() {
return <Target ref={this.ref} />
}
}
3.4.3 介绍
对于ref的三种使用方式,在源码中有所体现的是第三种。在源码中,有一个createRef函数,它声明了refObject变量,里面存在current属性,默认值为空,最后return refObject,在我们使用的时候,current的值就是当前使用的节点名称。
forward-ref返回了一个对象,里面存在typeof和render函数。对于forward-ref的使用,它解决了HOC组件传递ref的问题,使用的方法如上面第二段代码所示。如果使用stringRef无法将ref作为参数传递,所以有了createRef方法
3.5 context
3.5.1 绘制思维导图
3.5.2 React中使用context的实例
- childContextType方式
// 子组件
import React, {Component} from 'react';
import PropTypes from 'prop-types';
export default class ContextChild extends Component {
render() {
return (
<div>
<h3>childContext: {this.context.value}</h3>
</div>
)
}
}
ContextChild.contextTypes = {
value: PropTypes.string
}
// 父组件
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import ContextChild from './context-child';
export default class Context extends Component {
constructor (props) {
super();
this.state = {
childContext: 'childContext',
}
}
getChildContext() {
return {value: this.state.childContext}
}
render() {
const { childContext } = this.state;
return (
<div>
<div>
<input
value={childContext}
onChange={e => {this.setState({childContext: e.target.value})}}
/>
</div>
<ContextChild />
</div>
)
}
}
Context.childContextTypes = {
value: PropTypes.string
}
这样,当
Input
输入框值改变时,子组件中获取的this.context.value
也会随之改变,这里面,需要注意的是要规定context的类型,如果父子组件context类型不统一,是不会获得上面的结果的
- createContext方式
import React, {Component} from 'react';
import PropTypes from 'prop-types';
const { Provider, Consumer } = React.createContext()
export default class Context extends Component {
constructor (props) {
super();
this.state = {
create: 'create'
}
}
render() {
const { childContext, create } = this.state;
return (
<div>
<div>
<input
type="text"
value={create}
onChange={e => {this.setState({create: e.target.value})}}
/>
</div>
<Provider value={this.state.create}>
<Consumer>{value => <p>create: {value}</p>}</Consumer>
</Provider>
</div>
)
}
}
Context.childContextTypes = {
value: PropTypes.string
}
Provider和Consumer组件配合使用,Consumer组件可以获得Provider组件上的value属性的值,当input值改变时,Consumer组件里面的值随之改变
3.5.3 介绍
context部分只有createContext的源码,它接收了defaultValue和calculateChangedBits(计算新老context的API变化的方法)两个值,最后返回了context对象,下面分析context对象这一部分的源码
const context: ReactContext<T> = {
?typeof: REACT_CONTEXT_TYPE,
_calculateChangedBits: calculateChangedBits,
// As a workaround to support multiple concurrent renderers, we categorize
// some renderers as primary and others as secondary. We only expect
// there to be two concurrent renderers at most: React Native (primary) and
// Fabric (secondary); React DOM (primary) and React ART (secondary).
// Secondary renderers store their context values on separate fields.
_currentValue: defaultValue,
_currentValue2: defaultValue,
// Used to track how many concurrent renderers this context currently
// supports within in a single renderer. Such as parallel server rendering.
_threadCount: 0,
// These are circular
Provider: (null: any),
Consumer: (null: any),
};
context.Provider = {
?typeof: REACT_PROVIDER_TYPE,
_context: context,
};
context.Consumer = context;
上面代码中
I. _currentValue: 记录Provider组件上value属性的值
II. context对象里面拥有Provider、Consumer
III. _context: context中的context指的是上面的context对象
IV. context.Consumer等于本身的context对象,所以在使用Consumer组件时,可直接获取context对象中_currentValue的值
3.6 suspense-and-lazy
3.6.1 绘制思维导图
3.6.2 React中使用suspense和lazy的实例
// suspenseDemo.jsx
import React, { Suspense, lazy } from 'react'
const LazyComp = lazy(() => import('./suspenseChild.jsx'))
let data = ''
let result = ''
function requestData() {
if (data) return data
if (result) throw result
result = new Promise(resolve => {
setTimeout(() => {
data = 'success'
resolve()
}, 4000)
})
throw result
}
function SuspenseComp() {
return <p>{requestData()}</p>
}
export default SuspenseDemo => (
<Suspense fallback="loading data">
<SuspenseComp />
<LazyComp />
</Suspense>
)
// suspenseChild.jsx
import React, { Component } from 'react';
export default () => {
return <p>child</p>
}
效果图:
- 加载成功前
- 加载成功后
3.6.3 介绍
Suspense组件在其下方组件返回promise结果之前,都展示fallback函数中的值,包裹在suspense中用lazy加载的组件也会在promise结果返回后与异步组件一同加载。Suspense是一个常量,lazy在react中接收一个方法,返回Thenable,Thenable是promise对象,最终返回LazyComponent
export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
let lazyType = {
?typeof: REACT_LAZY_TYPE,
_ctor: ctor,
// React uses these fields to store the result.
_status: -1,
_result: null,
};
return lazyType;
}
返回的LazyComponent中,存在lazyType对象,其中的_ctor属性为它接收的方法,_status属性是记录Thenable对象的状态,_result为返回的结果。
3.7 hooks
3.7.1 绘制思维导图
3.7.2 React中使用hooks实例
在react的官方网站上一个useState的实例
import React, { useState } from 'react';
const State = () => {
// 声明一个新的叫做 “count” 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default State
效果图
每次点击按钮的时候,上面的数字就会+1
3.7.3 介绍
hooks是react16.8版本新增的特性,它可以使我们在不编写class的情况下使用state或者其他的React特性。
在使用的时候,我们要先引入使用的特性,像这样:
import React, { useState } from 'react';
然后根据我们想要实现的功能,去选择对应的hook功能API,hook的源码先定义了一个resolveDispatcher函数方法,该方法返回dispatcher,接下来定义每一个hook的API并导出,设置其API接收的参数,并有对应的返回
四、总结
这篇文章主要讲解了部分React的基础API以及它的使用方法和部分源码解析,并没有深入。接下来,我们会一点一点深入react,接下来关于react的文章:
- React.children源码解析
- hooks的详细使用
上述文章如有不对之处,还请大家指点出来,我们共同进步~
最后,分享一下我的微信公众号~