在上一篇文章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' ) );
回顧一下上一篇文章中咱們對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>; } }
爲了實現類定義組件,咱們須要定義一個Component
類:
class Component {}
經過繼承React.Component
定義的組件有本身的私有狀態state
,能夠經過this.state
獲取到。同時也能經過this.props
來獲取傳入的數據。
因此在構造函數中,咱們須要初始化state
和props
// React.Component class Component { constructor( props = {} ) { this.isReactComponent = true; this.state = {}; this.props = props; } }
這裏多了一個isReactComponent
屬性,咱們後面會用到。
組件內部的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
方法。
在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 } }
函數定義組件返回的是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的變化,不少生命週期的狀態沒辦法區分,因此咱們暫時只添加componentWillMount
和componentWillUpdate
兩個方法,它們會在組件掛載以前和更新以前執行。
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,從API層面實現React的大部分功能,在這個過程當中去探索爲何有虛擬DOM、diff、爲何setState這樣設計等問題。
整個系列大概會有六篇左右,我每週會更新一到兩篇,我會第一時間在github上更新,有問題須要探討也請在github上回復我~
博客地址: https://github.com/hujiulong/blog
關注點star,訂閱點watch