關于JSX
考慮這樣一段代碼:
const element = <h1>Hello, world!</h1>;
這段代碼既不是字符串也不是HTML,它是JSX,是javascript的拓展。在React里用來描述UI,因為JSX是用來生產React內的“元素”的。以后會介紹它是如何被渲染成DOM的。
我們可以在JSX里嵌入任何的javascript代碼,例如下面這個例子就混合了javascript代碼:
function formatName(user) { return user.firstName + ' ' + user.lastName; } const user = { firstName: 'Harper', lastName: 'Perez' }; const element = ( <h1> Hello, {formatName(user)}! </h1>); ReactDOM.render( element, document.getElementById('root') );
需要指出的是,雖然JSX是javascript的拓展,但是實際到最后,JSX還是要被編譯成純粹的javascript對象,所以在if語句、for循環語句里面都可以用,而且還可以把它當成函數參數或返回值。例如:
function getGreeting(user) { if (user) { return <h1>Hello, {formatName(user)}!</h1>; } return <h1>Hello, Stranger.</h1>; }
鑒于JSX相比HTML更接近javascript,所以它里面元素的屬性采用駝峰命名法,例如class要寫成className,tabindex要寫成tabIndex。
JSX自帶過濾以避免XSS攻擊,所以不必擔心注入的問題。
元素渲染
元素是React應用里最小的構造塊,一個元素所描述的內容決定了你在屏幕上看到的東西。比如這個:
const element = <h1>Hello, world</h1>;
另外,不同于瀏覽器DOM元素,React中的元素都是純粹的對象,創建起來代價很低,而且React會負責更新瀏覽器DOM以便于和React元素相匹配。
React中的元素是不可變的,一旦創建了一個元素,就無法改變它的屬性或者是后代元素,這有點類似電影中的一幀,僅是某個時間點的快照。就目前的知識來說,如果想要更新UI,唯一的方法就是再創建一個新的元素,比如我想制作一個顯示時間的程序,要做到內容每秒刷新一次,那么以目前的知識來說,只能這么寫代碼:
function tick() { const element = ( <div> <h1>Hello, world!</h1> <h2>It is {new Date().toLocaleTimeString()}.</h2> </div> ); ReactDOM.render( element, document.getElementById('root') ); } setInterval(tick, 1000);
值得注意的是,雖然每秒鐘我們創建一個新的元素,但React會十分智能的分辨其中的不同,每次僅僅會改變不同的部分,以上面的代碼為例,會發現每次只改變了dom中的文字。

在React程序中大部分代碼都是一次性的,不牽扯到動態刷新的問題,不過在后面會介紹如何利用狀態組件解決這個問題。
組件
組件給了你將UI切割的能力,以便更好地復用UI代碼。從概念上看,組件和javascript中的函數很像,可以接受任意的輸入然后返回一個最終繪制在屏幕上的React元素。
創建組件的最簡單方式就是寫一個javascript函數:
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
之前的React元素都采用的是原生的dom標簽,比如div,其實React提供了一種機制來滿足我們采用自定義標簽,例如:
function Welcome(props) { return <h1>Hello, {props.name}</h1>; } const element = <Welcome name="Sara" />; ReactDOM.render( element, document.getElementById('root') );
解釋React對一下上面這段代碼干了什么:
- 對<Welcome name="Sara" />元素調用ReactDOM.render()方法。
- React以{name: 'Sara'}為props調用Welcome組件。
- Welcome組件返回了一個<h1>Hello, Sara</h1>元素。
- React高效的更新DOM。
需要注意的是組件均是以大寫字母開頭,比如<div/>就是一個dom標簽,而<Welcome/>就是一個組件,而且需要在作用于中有一個對應的Welcome函數。
需要指出的是,組件可以嵌套,例如:
function Welcome(props) { return <h1>Hello, {props.name}</h1>; } function App() { return ( <div> <Welcome name="Sara" /> <Welcome name="Cahal" /> <Welcome name="Edite" /> </div> ); } ReactDOM.render( <App />, document.getElementById('root') );
注意:組件返回的必須是一個元素,例如上面的代碼中,雖然希望的是三個Welcome元素,但是必須用一個div包裹起來,不能直接返回三個元素。
鑒于組件的可嵌套能力, React推薦將一個大組件分割成小組件,例如下面這個組件:
function Comment(props) { return ( <div className="Comment"> <div className="UserInfo"> <img className="Avatar" src={props.author.avatarUrl} alt={props.author.name} /> <div className="UserInfo-name"> {props.author.name} </div> </div> <div className="Comment-text"> {props.text} </div> <div className="Comment-date"> {formatDate(props.date)} </div> </div> ); }
現在演示一下如何拆分這個組件,首先,可以提取出Avatar
function Avatar(props) { return ( <img className="Avatar" src={props.user.avatarUrl} alt={props.user.name} /> ); }
然后提取Comment:
function Comment(props) { return ( <div className="Comment"> <UserInfo user={props.author} /> <div className="Comment-text"> {props.text} </div> <div className="Comment-date"> {formatDate(props.date)} </div> </div> ); }
提取工作一開始看起來可能是乏味的,但是在大型應用中,這可以極大的提高代碼復用率。一個指導原則是,如果一個UI要素多次出現或者太大,都要做拆分以便復用。
有一點需要說明的是,組件的props都是只讀的,不要想在組件中修改props,那樣不會起任何作用。
生命周期
思考一下上面演示的時鐘程序,到目前為止只介紹了一種更新UI的方法,也就是調用ReactDOM.render()去重新渲染,如下所示:
function tick() { const element = ( <div> <h1>Hello, world!</h1> <h2>It is {new Date().toLocaleTimeString()}.</h2> </div> ); ReactDOM.render( element, document.getElementById('root') ); } setInterval(tick, 1000);
再介紹新方法前,我們先將這段代碼利用React的組件化能力進行封裝:
function Clock(props) { return ( <div> <h1>Hello, world!</h1> <h2>It is {props.date.toLocaleTimeString()}.</h2> </div> ); } function tick() { ReactDOM.render( <Clock date={new Date()} />, document.getElementById('root') ); } setInterval(tick, 1000);
僅僅做到這樣是不夠的,我們希望將更改時間的邏輯和元素綁定到一起,去除掉setInterval,最終的理想效果應該這樣的:
ReactDOM.render( <Clock />, document.getElementById('root') );
為了到達理想的效果,我們需要為組件添加state。要想使用state就需要講組件用class的寫法,因為class寫法在定義class時可以提供一些額外的特性,state就是其中之一。
將函數化的組件轉化成class的寫法,可以按照下面的步驟進行:
- 創建一個同名的ES6 class,該class繼承自React.Component
- 添加一個空的render方法。
- 將函數的主體部分挪到render方法內。
- 用this.props替換props。
- 將原來的函數形組件刪掉。
上述操作完成后函數就轉換為了class:
class Clock extends React.Component { render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.props.date.toLocaleTimeString()}.</h2> </div> ); } }
接下來就能用state和lifecycle hooks這兩項技術,首先用state代替props
class Clock extends React.Component { render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } }
然后添加一個構造函數去初始化this.state:
class Clock extends React.Component { constructor(props) { super(props); this.state = {date: new Date()}; } render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } }
需要注意的是,構造函數的參數是props并且將這個參數傳給了基類。
接下來可以將date(prop)從Clock元素上移除了。
ReactDOM.render( <Clock />, document.getElementById('root') );
此時我們的組件是這樣的:
class Clock extends React.Component { constructor(props) { super(props); this.state = {date: new Date()}; } render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } } ReactDOM.render( <Clock />, document.getElementById('root') );
接下來我們將為Clock設置一個定時器,這需要我們了解React組件生命周期的知識,React中的組件會經歷從創建到移除的一個周期,暴露給我們的是兩個鉤子函數:componentDidMount和componentWillUnmount,分別代表創建和移除,在這兩個時間點上我們可以做一些工作。例如定義計時器:
class Clock extends React.Component { constructor(props) { super(props); this.state = {date: new Date()}; } componentDidMount() { this.timerID = setInterval( () => this.tick(), 1000 ); } componentWillUnmount() { clearInterval(this.timerID); } tick() { this.setState({ date: new Date() }); } render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); } } ReactDOM.render( <Clock />, document.getElementById('root') );
需要注意的是,在更新date數值的時候,不能直接賦值,而是采用this.setState方法進行賦值。不過在構造函數里給date初始化的時候可以直接賦值。
事件處理
React里的事件名稱采用的是駝峰拼寫法,比如HTML中是onclick,在React里就是onClick。另外不能通過返回false來阻止默認事件,而應該用preventDefault方法。
感覺不管是React還是Angular,在工具性上都比較差,可能需要其它附件做為拓展吧,總之工具性的都比較差,常常有不夠用的感覺。
有條件的渲染
在React里面,可以根據實際情況有選擇性的渲染,例如考慮下面兩個組件:
function UserGreeting(props) { return <h1>Welcome back!</h1>; } function GuestGreeting(props) { return <h1>Please sign up.</h1>; }
然后創建一個新的組件,來有對上面兩個組件做選擇:
function Greeting(props) { const isLoggedIn = props.isLoggedIn; if (isLoggedIn) { return <UserGreeting />; } return <GuestGreeting />; } ReactDOM.render( // Try changing to isLoggedIn={true}: <Greeting isLoggedIn={false} />, document.getElementById('root') );
React支持將元素變量化以便根據不同的情況渲染不同的元素,如下:
function LoginButton(props) { return ( <button onClick={props.onClick}> Login </button> ); } function LogoutButton(props) { return ( <button onClick={props.onClick}> Logout </button> ); } class LoginControl extends React.Component { constructor(props) { super(props); this.handleLoginClick = this.handleLoginClick.bind(this); this.handleLogoutClick = this.handleLogoutClick.bind(this); this.state = {isLoggedIn: false}; } handleLoginClick() { this.setState({isLoggedIn: true}); } handleLogoutClick() { this.setState({isLoggedIn: false}); } render() { const isLoggedIn = this.state.isLoggedIn; let button = null; if (isLoggedIn) { button = <LogoutButton onClick={this.handleLogoutClick} />; } else { button = <LoginButton onClick={this.handleLoginClick} />; } return ( <div> <Greeting isLoggedIn={isLoggedIn} /> {button} </div> ); } } ReactDOM.render( <LoginControl />, document.getElementById('root') );
注意其中button變量的用法。
同時,在組件中還可以用javascript邏輯控制語句,如:
function Mailbox(props) { const unreadMessages = props.unreadMessages; return ( <div> <h1>Hello!</h1> {unreadMessages.length > 0 && <h2> You have {unreadMessages.length} unread messages. </h2> } </div> ); } const messages = ['React', 'Re: React', 'Re:Re: React']; ReactDOM.render( <Mailbox unreadMessages={messages} />, document.getElementById('root') );
React的組件也可以用condition ? true : false語句,如:
render() { const isLoggedIn = this.state.isLoggedIn; return ( <div> The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in. </div> ); }
或者更復雜一些的:
render() { const isLoggedIn = this.state.isLoggedIn; return ( <div> {isLoggedIn ? ( <LogoutButton onClick={this.handleLogoutClick} /> ) : ( <LoginButton onClick={this.handleLoginClick} /> )} </div> ); }
如果不希望組件渲染出內容,可以返回null;