React源碼解析(一):這些React的API你都知道嗎

寫在前面

Vue、React和Angular成爲了前端工做者最經常使用的三大框架。分析其源碼,對咱們技術的提高有着相當重要的做用,咱們先分析React源碼,本篇文章將從其最經常使用的API提及html

1、準備開始

一、React源碼地址:從github上下載源碼前端

二、React官方文檔:準備好文檔,便於參考react

2、繪製思惟導圖

根據官方文檔以及源碼,畫出一張思惟導圖git

這些API將是這篇文章着重介紹的React APIgithub

3、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的。promise

首先推薦一個測試工具:Babel轉換工具bash

在JSX中先寫一對div標籤微信

<div></div>
複製代碼

它經過Babel轉換成了這樣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的實例

一、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>
        )
    }
}
複製代碼

二、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接收的參數,並有對應的返回

4、總結

這篇文章主要講解了部分React的基礎API以及它的使用方法和部分源碼解析,並無深刻。接下來,咱們會一點一點深刻react,接下來關於react的文章:

  • React.children源碼解析
  • hooks的詳細使用

上述文章若有不對之處,還請你們指點出來,咱們共同進步~

最後,分享一下個人微信公衆號~

相關文章
相關標籤/搜索