React源码解析(一):这些React的API你都知道吗


写在前面

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中介绍JSXJS中转换说到的createElement方法其实就是它,createElement方法接收的参数个数由父节点的内容所控制(参见3.1),所以我们看到的childrenLength的计算-2,就是减掉了这个方法接收的前两个参数,然后呢,根据它的长度判断chilren的值,最后传给了propschildren属性

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 {valuethis.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<TR>(ctor: () => Thenable<TR>): LazyComponent<T{
  let lazyType = {
    $$typeof: REACT_LAZY_TYPE,
    _ctor: ctor,
    // React uses these fields to store the result.
    _status: -1,
    _resultnull,
  };
   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的详细使用

上述文章如有不对之处,还请大家指点出来,我们共同进步~

最后,分享一下我的微信公众号~


评论
 上一篇
React源码解析(二):关于React.Children,你了解多少? React源码解析(二):关于React.Children,你了解多少?
前言 上一篇文章说了React的一部分基础API,今天这一篇文章说一下React.children React.children 在React.js中,摘取出Children,其中罗列了React.children的几个方法 const&n
2020-07-20
下一篇 
【React】深入理解虚拟dom和diff算法 【React】深入理解虚拟dom和diff算法
本篇文章同步发表在我的个人博客中:https://zhengyq.club 写在前面 在React中,Virtual Dom和diff的结合大大提高了渲染效率。diff算法由最初的O(n^3)复杂度变为了现在的O(n),那么在这其中
2020-05-21