從零開始實現一個React(二):實現組件功能

前言

在上一篇文章JSX和虛擬DOM中,咱們實現了基礎的JSX渲染功能,可是React的意義在於組件化。在這篇文章中,咱們就要實現React的組件功能。前端

React定義組件的方式能夠分爲兩種:函數和類,咱們姑且將兩種不一樣方式定義的組件稱之爲函數定義組件類定義組件node

函數定義組件

函數定義組件相對簡單,只須要用組件名稱聲明一個函數,並返回一段JSX便可。
例如咱們定義一個Welcome組件:react

function Welcome( props ) {
    return <h1>Hello, {props.name}</h1>;
}
注意組件名稱要以大寫字母開頭

函數組件接受一個props參數,它是給組件傳入的數據。git

咱們能夠這樣來使用它:github

const element = <Welcome name="Sara" />;
ReactDOM.render(
    element,
    document.getElementById( 'root' )
);

讓createElemen支持函數定義組件

回顧一下上一篇文章中咱們對React.createElement的實現:數組

function createElement( tag, attrs, ...children ) {
    return {
        tag,
        attrs,
        children
    }
}

這種實現只能渲染原生DOM元素,而對於組件,createElement獲得的參數略有不一樣:
若是JSX片斷中的某個元素是組件,那麼createElement的第一個參數tag將會是一個方法,而不是字符串。瀏覽器

區分組件和原生DOM的工做,是 babel-plugin-transform-react-jsx幫咱們作的

例如在處理<Welcome name="Sara" />時,createElement方法的第一個參數tag,實際上就是咱們定義Welcome的方法:babel

function Welcome( props ) {
    return <h1>Hello, {props.name}</h1>;
}

因此咱們須要修改一下createElement,讓它可以渲染組件。app

function createElement( tag, attrs, ...children ) {
    
    // 若是tag是一個方法,那麼它是一個組件
    if ( typeof tag === 'function' ) {
        return tag( attrs || {} );
    }

    return {
        tag,
        attrs,
        children
    }
}

渲染函數定義組件

在簡單的修改了createElement方法後,咱們就能夠用來渲染函數定義組件了。
渲染上文定義的Welcome組件:框架

const element = <Welcome name="Sara" />;
ReactDOM.render(
    element,
    document.getElementById( 'root' )
);

在瀏覽器中能夠看到結果:

圖片描述

試試更復雜的例子,將多個組件組合起來:

function App() {
    return (
        <div>
            <Welcome name="Sara" />
            <Welcome name="Cahal" />
            <Welcome name="Edite" />
        </div>
    );
}
ReactDOM.render(
    <App />,
    document.getElementById( 'root' )
);

在瀏覽器中能夠看到結果:
圖片描述

類定義組件

類定義組件相對麻煩一點,咱們經過繼承React.Component來定義一個組件:

class Welcome extends React.Component {
    render() {
        return <h1>Hello, {this.props.name}</h1>;
    }
}

Componet

爲了實現類定義組件,咱們須要定義一個Component類:

class Component {}

state & props

經過繼承React.Component定義的組件有本身的私有狀態state,能夠經過this.state獲取到。同時也能經過this.props來獲取傳入的數據。
因此在構造函數中,咱們須要初始化stateprops

// React.Component
class Component {
    constructor( props = {} ) {
        this.isReactComponent = true;
        this.state = {};
        this.props = props;
    }
}

這裏多了一個isReactComponent屬性,咱們後面會用到。

setState

組件內部的state和渲染結果相關,當state改變時一般會觸發渲染,爲了讓React知道咱們改變了state,咱們只能經過setState方法去修改它。咱們能夠經過Object.assign來作一個簡單的實現。
在每次更新state後,咱們須要使用ReactDOM.render從新渲染。

import ReactDOM from '../react-dom'
class Component {
    constructor( props = {} ) {
        // ...
    }

    setState( stateChange ) {
        // 將修改合併到state
        Object.assign( this.state, stateChange );
        if ( this._container ) {
            ReactDOM.render( this, this._container );
        }
    }
}

你可能據說過React的setState是異步的,同時它有不少優化手段,這裏咱們暫時不去管它,在之後會有一篇文章專門來說setState方法。

讓createElemen支持類定義組件

在js中,class只是語法糖,它的本質仍然是一個函數。
因此第一步,咱們須要在createElemen方法中區分當前的節點是函數定義仍是類定義。
類定義組件必須有render方法,而經過class定義的類,它的方法都附加在prototype上。
因此只須要判斷tag的prototype中是否有render方法,就能知道這個組件是函數定義仍是類定義。
如今咱們能夠進一步修改React.createElement

function createElement( tag, attrs, ...children ) {

    // 類定義組件
    if ( tag.prototype &&  tag.prototype.render ) {
        return new tag( attrs );
    // 函數定義組件
    } else if ( typeof tag === 'function' ) {
        return tag( attrs || {} );
    }

    return {
        tag,
        attrs,
        children
    }
}

render

函數定義組件返回的是jsx,咱們不須要作額外處理。可是類定義組件不一樣,它並不直接返回jsx。而是經過render方法來獲得渲染結果。

因此咱們須要修改ReactDOM.render方法。
修改以前咱們先來回顧一下上一篇文章中咱們對ReactDOM.render的實現:

function render( vnode, container ) {

    if ( vnode === undefined ) return;
    
    // 當vnode爲字符串時,渲染結果是一段文本
    if ( typeof vnode === 'string' ) {
        const textNode = document.createTextNode( vnode );
        return container.appendChild( textNode );
    }

    const dom = document.createElement( vnode.tag );

    if ( vnode.attrs ) {
        Object.keys( vnode.attrs ).forEach( key => {
            if ( key === 'className' ) key = 'class';            // 當屬性名爲className時,改回class
            dom.setAttribute( key, vnode.attrs[ key ] )
        } );
    }

    vnode.children.forEach( child => render( child, dom ) );    // 遞歸渲染子節點

    return container.appendChild( dom );    // 將渲染結果掛載到真正的DOM上
}

在上文定義Component時,咱們添加了一個isReactComponent屬性,在這裏咱們須要用它來判斷當前渲染的是不是一個組件:

function render( vnode, container ) {

    if ( vnode.isReactComponent ) {
        const component = vnode;
        component._container = container;   // 保存父容器信息,用於更新
        vnode = component.render();            //  render()返回的結果纔是須要渲染的vnode
    }
    
    // 後面的代碼不變...
}

如今咱們的render方法就能夠用來渲染組件了。

生命週期

上面的實現還差一個關鍵的部分:生命週期。

在React的組件中,咱們能夠經過定義生命週期方法在某個時間作一些事情,例如定義componentDidMount方法,在組件掛載時會執行它。

可是如今咱們的實現很是簡單,尚未對比虛擬DOM的變化,不少生命週期的狀態沒辦法區分,因此咱們暫時只添加componentWillMountcomponentWillUpdate兩個方法,它們會在組件掛載以前和更新以前執行。

function render( vnode, container ) {

    if ( vnode.isReactComponent ) {
        const component = vnode;

        if ( component._container ) {
            if ( component.componentWillUpdate ) {
                component.componentWillUpdate();    // 更新
            }
        } else if ( component.componentWillMount ) {
            component.componentWillMount();          // 掛載
        }

        component._container = container;   // 保存父容器信息,用於更新

        vnode = component.render();
    }
    
    // 後面的代碼不變...
}

渲染類定義組件

如今大部分工做已經完成,咱們能夠用它來渲染類定義組件了。
咱們來試一試將剛纔函數定義組件改爲類定義:

class Welcome extends React.Component {
    render() {
        return <h1>Hello, {this.props.name}</h1>;
    }
}

class App extends React.Component {
    render() {
        return (
            <div>
                <Welcome name="Sara" />
                <Welcome name="Cahal" />
                <Welcome name="Edite" />
            </div>
        );
    }
}
ReactDOM.render(
    <App />,
    document.getElementById( 'root' )
);

運行起來結果和函數定義組件徹底一致:
圖片描述

再來嘗試一個能體現出類定義組件區別的例子,實現一個計數器Counter,每點擊一次就會加1。
而且組件中還增長了兩個生命週期函數:

class Counter extends React.Component {
    constructor( props ) {
        super( props );
        this.state = {
            num: 0
        }
    }

    componentWillUpdate() {
        console.log( 'update' );
    }

    componentWillMount() {
        console.log( 'mount' );
    }

    onClick() {
        this.setState( { num: this.state.num + 1 } );
    }

    render() {
        return (
            <div onClick={ () => this.onClick() }>
                <h1>number: {this.state.num}</h1>
                <button>add</button>
            </div>
        );
    }
}

ReactDOM.render(
    <Counter />,
    document.getElementById( 'root' )
);

能夠看到結果:
圖片描述

mount只在掛載時輸出了一次,後面每次更新時會輸出update

後話

至此咱們已經從API層面實現了React的核心功能。可是咱們目前的作法是每次更新都從新渲染整個組件甚至是整個應用,這樣的作法在頁面複雜時將會暴露出性能上的問題,DOM操做很是昂貴,而爲了減小DOM操做,React又作了哪些事?這就是咱們下一篇文章的內容了。

這篇文章的代碼:https://github.com/hujiulong/...

從零開始實現React系列

React是前端最受歡迎的框架之一,解讀其源碼的文章很是多,可是我想從另外一個角度去解讀React:從零開始實現一個React,從API層面實現React的大部分功能,在這個過程當中去探索爲何有虛擬DOM、diff、爲何setState這樣設計等問題。

整個系列大概會有六篇左右,我每週會更新一到兩篇,我會第一時間在github上更新,有問題須要探討也請在github上回復我~

博客地址: https://github.com/hujiulong/blog
關注點star,訂閱點watch

上一篇文章

從零開始實現React(一):JSX和虛擬DOM

相關文章
相關標籤/搜索