摘要:在這篇文章中,我們就要實現的組件功能。這篇文章的代碼從零開始實現系列是前端最受歡迎的框架之一,解讀其源碼的文章非常多,但是我想從另一個角度去解讀從零開始實現一個,從層面實現的大部分功能,在這個過程中去探索為什么有虛擬為什么這樣設計等問題。
前言
在上一篇文章JSX和虛擬DOM中,我們實現了基礎的JSX渲染功能,但是React的意義在于組件化。在這篇文章中,我們就要實現React的組件功能。
React定義組件的方式可以分為兩種:函數和類,我們姑且將兩種不同方式定義的組件稱之為函數定義組件和類定義組件
函數定義組件函數定義組件相對簡單,只需要用組件名稱聲明一個函數,并返回一段JSX即可。
例如我們定義一個Welcome組件:
function Welcome( props ) { returnHello, {props.name}
; }
注意組件名稱要以大寫字母開頭
函數組件接受一個props參數,它是給組件傳入的數據。
我們可以這樣來使用它:
const element =讓createElemen支持函數定義組件; 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幫我們做的
例如在處理
function Welcome( props ) { returnHello, {props.name}
; }
所以我們需要修改一下createElement,讓它能夠渲染組件。
function createElement( tag, attrs, ...children ) { // 如果tag是一個方法,那么它是一個組件 if ( typeof tag === "function" ) { return tag( attrs || {} ); } return { tag, attrs, children } }渲染函數定義組件
在簡單的修改了createElement方法后,我們就可以用來渲染函數定義組件了。
渲染上文定義的Welcome組件:
const element =; ReactDOM.render( element, document.getElementById( "root" ) );
在瀏覽器中可以看到結果:
試試更復雜的例子,將多個組件組合起來:
function App() { return (); } ReactDOM.render(, document.getElementById( "root" ) );
在瀏覽器中可以看到結果:
類定義組件相對麻煩一點,我們通過繼承React.Component來定義一個組件:
class Welcome extends React.Component { render() { returnComponetHello, {this.props.name}
; } }
為了實現類定義組件,我們需要定義一個Component類:
class Component {}state & props
通過繼承React.Component定義的組件有自己的私有狀態state,可以通過this.state獲取到。同時也能通過this.props來獲取傳入的數據。
所以在構造函數中,我們需要初始化state和props
// 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的變化,很多生命周期的狀態沒辦法區分,所以我們暫時只添加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() { returnHello, {this.props.name}
; } } class App extends React.Component { render() { return (); } } ReactDOM.render(, 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 (this.onClick() }>); } } ReactDOM.render(number: {this.state.num}
, 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
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93731.html
摘要:一個比較好的做法是利用的事件隊列機制。整個系列大概會有四篇左右,我每周會更新一到兩篇,我會第一時間在上更新,有問題需要探討也請在上回復我博客地址關注點,訂閱點上一篇文章從零開始實現一個三算法 前言 在上一篇文章中,我們實現了diff算法,性能有非常大的改進。但是文章末尾也指出了一個問題:按照目前的實現,每次調用setState都會觸發更新,如果組件內執行這樣一段代碼: for ( le...
摘要:而對比變化,找出需要更新部分的算法我們稱之為算法。整個系列大概會有四篇,我每周會更新一到兩篇,我會第一時間在上更新,有問題需要探討也請在上回復我博客地址關注點,訂閱點上一篇文章從零開始實現一個二組件和生命周期 前言 在上一篇文章,我們已經實現了React的組件功能,從功能的角度來說已經實現了React的核心功能了。 但是我們的實現方式有很大的問題:每次更新都重新渲染整個應用或者整個組件...
摘要:通過文件可以對圖標名稱等信息進行配置。注意,注冊的只在生產環境中生效,并且該功能只有在下才能有效果該文件是過濾文件配置該文件是描述文件定義了項目所需要的各種模塊,以及項目的配置信息比如名稱版本許可證等元數據。 一、 快速開始: 全局安裝腳手架: $ npm install -g create-react-app 通過腳手架搭建項目: $ create-react-app 開始項目: ...
閱讀 1297·2021-11-04 16:09
閱讀 3485·2021-10-19 11:45
閱讀 2396·2021-10-11 10:59
閱讀 1010·2021-09-23 11:21
閱讀 2762·2021-09-22 10:54
閱讀 1129·2019-08-30 15:53
閱讀 2600·2019-08-30 15:53
閱讀 3477·2019-08-30 12:57