1.1React 簡(jiǎn)介
React 把用戶界面抽象成一個(gè)個(gè)組件,開(kāi)發(fā)者通過(guò)組合這些組件,最終得到功能豐富、可交互的頁(yè)面。通過(guò)引入 JSX 語(yǔ)法,復(fù)用組件變得非常容易,同時(shí)也能保證組件結(jié)構(gòu)清晰。有了組件這層抽象,React 把代碼和真實(shí)渲染目標(biāo)隔離開(kāi)來(lái),除了可以在瀏覽器端渲染 DOM 來(lái)開(kāi)發(fā)網(wǎng)頁(yè),還能用于開(kāi)發(fā)原生移動(dòng)應(yīng)用。
1.1.1 專注視圖層
React 并不是完整的 MVC/MVVM 框架,它專注于提供清晰、簡(jiǎn)潔的 View(視圖)層解決方案,又是一個(gè)包括 View 和 Controller 的庫(kù)。對(duì)于復(fù)雜的應(yīng)用,可根據(jù)應(yīng)用場(chǎng)景自行選擇業(yè)務(wù)層框架,并根據(jù)需要搭配 Flux、Redux、Graph/Relay 來(lái)使用。
1.1.2 Virtual DOM
真實(shí)頁(yè)面對(duì)應(yīng)一個(gè) DOM 樹(shù)。傳統(tǒng)頁(yè)面開(kāi)發(fā)中,每次要更新頁(yè)面時(shí),都要手動(dòng)操作 DOM 來(lái)更新,如圖:

但在前端開(kāi)發(fā)中,性能消耗最大的就是 DOM 操作,且這部分代碼會(huì)讓整體項(xiàng)目的代碼變得難以維護(hù)。
React 把真實(shí) DOM 樹(shù)轉(zhuǎn)換成 JavaScript 對(duì)象樹(shù),即 Virtual DOM,如圖:

每次數(shù)據(jù)更新后,重新計(jì)算 Virtual DOM,并和上一次生成的 Virtual DOM 做對(duì)比,對(duì)發(fā)生變化的部分做批量更新。
React 也提供了直觀的 shouldComponentUpdate 生命周期函數(shù),來(lái)減少數(shù)據(jù)變化后不必要的 Virtual DOM 對(duì)比過(guò)程,以保證性能。
Virtual DOM 最大的好處在于方便和其他平臺(tái)集成:如 react-native 就是基于 Virtual DOM 渲染出的原生控件,React 組件可以映射為對(duì)應(yīng)的原生控件,輸出時(shí),是輸出 Web DOM、Android 控件還是 iOS 控件,則由平臺(tái)本身決定。
1.1.3 函數(shù)式編程
函數(shù)式編程,對(duì)應(yīng)的是聲明式編程,它是人類模仿自己邏輯思考方式發(fā)明出來(lái)的。
React 把過(guò)去不斷重復(fù)構(gòu)建 UI 的過(guò)程抽象成了組件,且在給定參數(shù)的情況下約定渲染對(duì)應(yīng)的 UI 界面。
React 能充分利用很多函數(shù)式方法去減少冗余代碼。
且由于它本身就是簡(jiǎn)單函數(shù),所以易于測(cè)試。
可以說(shuō),函數(shù)式編程才是 React 的精髓。
1.2 JSX 語(yǔ)法
React 為方便 View 層組件化,承載了構(gòu)建 HTML 結(jié)構(gòu)化頁(yè)面的職責(zé),但 React 是通過(guò)創(chuàng)建與更新虛擬元素來(lái)管理整個(gè) Virtual DOM 的。
虛擬元素可以理解為真實(shí)元素的對(duì)應(yīng),它的構(gòu)建與更新都是在內(nèi)存中完成的,并不會(huì)真正渲染到 DOM 中去。在 React 中創(chuàng)建的虛擬元素可分為兩類: DOM 元素和組件元素,分別對(duì)應(yīng)原生 DOM 元素和自定義元素。
1.2.1 JSX 的由來(lái)
-
1. DOM 元素
用 HTML 語(yǔ)法表示一個(gè)按鈕:
<button class="btn btn-blue">
<em>Confirm</em>
</button>
Web 頁(yè)面是由一個(gè)個(gè) HTML 元素嵌套組合而成,當(dāng)時(shí)用 JavaScript 來(lái)描述這些元素時(shí),這些元素可以簡(jiǎn)單地被表示成純粹的對(duì)象,如上面的 button,用JavaScript 描述則如下,且依舊包括元素類型及屬性:
{
type: 'button',
props: {
className: 'btn btn-blue',
children: [{
type: 'em',
props: {children: 'Confirm'}
}]
}
}
這樣,我們就可以在 JavaScript 中創(chuàng)建 Virtual DOM 元素了。
但在 React 中,我們無(wú)法通過(guò)方法去調(diào)用這些元素,它們只是不可變的描述對(duì)象。
-
2. 組件元素
如下,我們可以封裝上述 button 元素,得到一種構(gòu)建按鈕的公共方法:
const Button = ({ color, text }) => {
return {
type: 'button',
props: {
className: `btn btn-${color}`,
children: {
type: 'em',
props: { children: text, },
},
},
};
}
我們要生成 DOM 元素中具體的按鈕時(shí),就可以方便的調(diào)用
Button({color: 'blue', text: 'confirm'})
來(lái)創(chuàng)建。
那么上面你的 Button 方法也可以作為元素而存在,方法名對(duì)應(yīng)元素類型,參數(shù)對(duì)應(yīng) DOM 元素屬性,它就具備了元素的兩大必要條件,這樣構(gòu)建的就是自定義類型的元素(組件元素)。
用 JSON 結(jié)構(gòu)來(lái)描述則如下:
{ type: Button, props: { color: 'blue', children: 'Confirm' } }
接下來(lái)我們可以再對(duì) Button 進(jìn)行封裝:
const DangerButton = ({ text }) => ({
type: Button,
props: {
color: 'red',
children: text
}
});
也可以繼續(xù)封裝更復(fù)雜些的功能模塊:
const DeleteAccount = () => ({type: 'div',
props: {
children: [{type: 'p', props: {children: 'Are you sure?',},}, {
type: DangerButton,
props: {children: 'Confirm',},
}, {type: Button, props: {color: 'blue', children: 'Cancel',},}],
}
});
但在表達(dá)這種結(jié)構(gòu)時(shí),就有些繁瑣,就有了 JSX ,用 JSX 語(yǔ)法來(lái)描述上述組件元素如下:
const DeleteAccount = () => (
<div>
<p>Are you sure?</p>
<DangerButton>Confirm</DangerButton>
<Button color="blue">Cancel</Button>
</div>
);
這樣就簡(jiǎn)潔了許多。
小結(jié):
JSX 是 JavaScript 語(yǔ)言的一種語(yǔ)法擴(kuò)展,JSX 結(jié)構(gòu)其實(shí)就是 JavaScript 對(duì)象。編譯時(shí),這種 JSX 結(jié)構(gòu)會(huì)被轉(zhuǎn)換成 JavaScript 的對(duì)象結(jié)構(gòu)。所以,使用 React 和 JSX 一定要經(jīng)過(guò)編譯。
1.2.2 JSX 基本語(yǔ)法
可以說(shuō),JSX 基本語(yǔ)法基本被 XML 囊括了,但也有少許不同之處。下面從基本語(yǔ)法、元素類型、元素屬性、JavaScript 屬性表達(dá)式等維度一一講述。
1.2.2.1 XML 基本語(yǔ)法
使用類 XML 語(yǔ)法的好處是標(biāo)簽可以任意嵌套,我們可以像 HTML 一樣清晰地看到 DOM 樹(shù)狀結(jié)構(gòu)及其屬性。如構(gòu)造一個(gè) LIst 組件:
const List = () => (
<div>
<Title>This is title</Title>
<ul>
<li>list item</li>
<li>list item</li>
<li>list item</li>
</ul>
</div>
);
注:
- 定義標(biāo)簽時(shí),只允許被一個(gè)標(biāo)簽包裹:即最外層只能是一個(gè)標(biāo)簽,不能使并列的標(biāo)簽,否則會(huì)報(bào)錯(cuò)。
- 標(biāo)簽一定要閉合: <div></div>、<p></p>之類標(biāo)簽一定要閉合, HTML 中自閉合的標(biāo)簽則可以自自合。自定義標(biāo)簽可根據(jù)是否有子組件或文本來(lái)決定是否閉合。
1.2.2.2. 元素類型
如 1.2 節(jié)中說(shuō)到兩種元素:DOM 元素和組件元素。在 JSX 里對(duì)應(yīng)規(guī)則是:DOM 元素 HTML 標(biāo)簽首字母小寫(xiě),組件元素 HTML 標(biāo)簽首字母大寫(xiě)。
HTML 標(biāo)準(zhǔn)中,還有些特殊的標(biāo)簽:
注釋:
HTML 中,注釋寫(xiě)成 這樣的形式,但 JSX 中并沒(méi)有定義注釋的轉(zhuǎn)換方法,事實(shí)上,JSX 還是 JavaScript,依然可以用簡(jiǎn)單方法注釋,但在一個(gè)組建的子元素位置使用注釋要用 {} 包起來(lái),否則會(huì)在頁(yè)面中直接顯示出來(lái)。如:
const App = (
<Nav>
{/* 節(jié)點(diǎn)注釋 */}
<Person
/* 多行
注釋 */
name={window.isLoggedIn ? window.name : ''}/>
</Nav>
);
DOCTYPE:
DOCTYPE 頭是一個(gè)非常特殊的標(biāo)志,一般會(huì)在使用 React 作為服務(wù)端渲染時(shí)用到。在 HTML 中,DOCTYPE 是沒(méi)有閉合的,也就是說(shuō)我們無(wú)法渲染它。 常見(jiàn)的做法是構(gòu)造一個(gè)保存 HTML 的變量,將 DOCTYPE 與整個(gè) HTML 標(biāo)簽渲染后的結(jié)果串連起來(lái)。第 7 章會(huì)詳細(xì)講到。
1.2.2.3. 元素屬性
DOM 元素:
DOM 元素的屬性是標(biāo)準(zhǔn)規(guī)范屬性,但有兩個(gè)例外——class 和 for,在 JavaScript 中這兩個(gè)單詞都是關(guān)鍵詞。因此,React 將其這樣轉(zhuǎn)換:
class 屬性改為 className;
for 屬性改為 htmlFor。
組件元素:
組件元素的屬性是完全自定義的屬性,也可以理解為實(shí)現(xiàn)組件所需要的參數(shù)。如:
const Header = ({title, children}) => ( <h3 title={title}>{children}</h3> );
我們給 Header 組件加了一個(gè) title 屬性,則可以這樣調(diào)用:
<Header title="hello world">Hello world</Header>
可以看出,Header 中 title 屬性代表的是自定義標(biāo)簽的屬性可以傳遞;
h3 中的 title 屬性是標(biāo)簽自帶的屬性無(wú)法傳遞。
注:自定義屬性都使用小駝峰寫(xiě)法。
JSX 特有的屬性表達(dá):
Boolean 屬性:Boolean 屬性省略時(shí)默認(rèn)為 true,要傳 false,則必須使用屬性表達(dá)式。如下:
<Checkbox checked={true} /> 可以簡(jiǎn)寫(xiě)為 <Checkbox checked />,反之,設(shè)置 checked 屬性為 false 時(shí)則必須寫(xiě): <Checkbox checked={false} />。
展開(kāi)屬性:
如果事先知道組件需要的全部屬性,JSX 可以這樣來(lái)寫(xiě):
var component = <Component foo={x} bar={y} />;
如果你不知道要設(shè)置哪些 props,那么現(xiàn)在好不要設(shè)置它:
var component = <Component />;
component.props.foo = x; // 不好
component.props.bar = y; // 同樣不好
上面這是反模式,因?yàn)?React 不能幫你檢查屬性類型(propTypes)。這樣即使你的 屬性類型有錯(cuò)誤也不能得到清晰的錯(cuò)誤提示。
Props 應(yīng)該被認(rèn)為是不可變的。在別處修改 props 對(duì)象可能會(huì)導(dǎo)致預(yù)料之外的結(jié)果,所以原則上這將是一個(gè)凍結(jié)的對(duì)象。
那么你可以使用 JSX 的新特性——展開(kāi)屬性:(使用了 ES6 的 rest/spread 特性(...))
var props = {};
props.foo = x;
props.bar = y;
var component = <Component {...props} />;
傳入對(duì)象的屬性會(huì)被復(fù)制到組件內(nèi)。它能被多次使用,也可以和其它屬性一起用。注意順序很重要,后面的會(huì)覆蓋掉前面的。
var props = { foo: 'default' };
var component = <Component {...props} foo={'override'} />;
console.log(component.props.foo); // 'override'
自定義 HTML 屬性:
DOM 標(biāo)簽自定義屬性要使用 data- 前綴,這與 HTML 標(biāo)準(zhǔn)一致:
<div data-attr="xxx">content</div>
組件標(biāo)簽中任意屬性都是被支持的:
<x-my-component custom-attr="foo" />
1.2.2.4. JavaScript 屬性表達(dá)式
屬性值要使用表達(dá)式,即用 {} 代替 ""即可。
const person = <Person name={window.isLoggedIn ? window.name : ''} />;
子組件也可以使用表達(dá)式:
const content = <Container>{window.isLoggedIn ? <Nav /> : <Login />}</Container>;
1.2.2.5. HTML 轉(zhuǎn)義
React 會(huì)將所有要顯示到 DOM 的字符串轉(zhuǎn)義,防止 XSS。所以,如果 JSX 中含有轉(zhuǎn)義后的 實(shí)體字符,比如 ?(?),則后 DOM 中不會(huì)正確顯示,因?yàn)?React 自動(dòng)把 ? 中的特 殊字符轉(zhuǎn)義了。有幾種解決辦法:
直接使用 UTF-8 字符 ?;
使用對(duì)應(yīng)字符的 Unicode 編碼查詢編碼;
使用數(shù)組組裝 <div>{['cc ', <span>?</span>, ' 2015']}</div>;
直接插入原始的 HTML。
此外,React 提供了 dangerouslySetInnerHTML 屬性。正如其名,它的作用就是避免 React 轉(zhuǎn) 義字符,在確定必要的情況下可以使用它:
<div dangerouslySetInnerHTML={{__html: 'cc © 2015'}} />
1.2.2.6. 事件監(jiān)聽(tīng)
React.js 不需要手動(dòng)調(diào)用 addEventListener 進(jìn)行監(jiān)聽(tīng),它幫我們封裝好了一系列的 on* 屬性,當(dāng)你要為某元素監(jiān)聽(tīng)某個(gè)事件時(shí),只需要簡(jiǎn)單地給它加上相應(yīng)的 on* 即可。你也不需要考慮不同瀏覽器兼容性問(wèn)題,React.js 已經(jīng)將其封裝好了。
注:on*只能用在普通的 HTML 標(biāo)簽上,而不能用在組件上。
event 對(duì)象
和普通瀏覽器一樣,事件監(jiān)聽(tīng)函數(shù)會(huì)自動(dòng)傳入一個(gè) event 對(duì)象,這個(gè)對(duì)象和普通而瀏覽器 event 對(duì)象所包含的方法和屬性基本一致。不同的是,這個(gè)對(duì)象并不是瀏覽器所提供的,而是 React.js 自己內(nèi)部構(gòu)建的。
render(){
const { isLiked } = this.state;
const innerText = isLiked ? '取消' : '點(diǎn)贊';
return(
<button className='like-btn' onClick={this.isLikedChange} style={{background: this.props.color}}>
<span>{innerText}</span>
</button>
)
}
關(guān)于事件中的 this
React.js 在調(diào)用你所傳給它的方法時(shí),并不是通過(guò)對(duì)象方法的方式調(diào)用,而是直接通過(guò)函數(shù)調(diào)用,所以事件監(jiān)聽(tīng)函數(shù)內(nèi)并不能通過(guò) this 獲取到當(dāng)前實(shí)例。如果你想在事件函數(shù)中使用當(dāng)前的實(shí)例,你需要手動(dòng)地將實(shí)例方法 bind 到當(dāng)前實(shí)例上再傳入給 React.js。
render(){
const { isLiked } = this.state;
const innerText = isLiked ? '取消' : '點(diǎn)贊';
return(
<button className='like-btn' onClick={this.isLikedChange.bind(this)} style={{background: this.props.color}}>
<span>{innerText}</span>
</button>
)
}
bind 會(huì)把實(shí)例方法綁定到當(dāng)前實(shí)例上,然后我們?cè)侔呀壎ê蟮暮瘮?shù)傳給 React.js 的 onClick 事件監(jiān)聽(tīng)。
1.3 React 組件
1.3.1 組件的演變
傳統(tǒng)組件:
組件封裝的基本思路就是面向?qū)ο笏枷?。交互基本上以操?DOM 為主,結(jié)構(gòu)上哪里需要變,就操作哪里。
基本的封裝性:通過(guò)實(shí)例化方法來(lái)構(gòu)造對(duì)象。
簡(jiǎn)單的生命周期呈現(xiàn):如 constructor 和 destroy 代表組建的掛載和卸載過(guò)程。
明確的數(shù)據(jù)流動(dòng):根據(jù)傳入?yún)?shù)的不同做出不同相應(yīng),從而得到渲染結(jié)果。
Web Components:
Web Components
1.3.2 React 組件的構(gòu)建
React 組件即為組件元素。組件元素被描述成純粹的 JSON 對(duì)象,意味著可以使用方法或類來(lái)構(gòu)建。
React 組件基本上由 3 個(gè)部分組成——屬性(props)、狀態(tài)(state)以及生命周期方法。如下圖:

React 組件可以接收參數(shù)(props),也可能由自身狀態(tài)(state),接收到的參數(shù)或自身狀態(tài)有所改變,都會(huì)導(dǎo)致 React 組件執(zhí)行相應(yīng)的生命周期方法,最后渲染。
React 組件引用方式遵循 ES6 module 標(biāo)準(zhǔn)。
React 組件的構(gòu)建方法
官方在 React 組件構(gòu)建上提供了 3 種不同的方法:React.createClass、ES6 classes 和無(wú)狀態(tài)函數(shù)。
1. React.createClass
用 React.createClass 構(gòu)建組件是 React 最傳統(tǒng)、兼容性最好的方法。
const Button = React.createClass({
getDefaultProps() {
return {
color: 'blue',
text: 'Confirm',
};
},
render() {
const { color, text } = this.props;
return (
<button className={`btn btn-${color}`}>
<em>{text}</em>
</button>
);
}
});
從表象看,React.createClass 方法就是構(gòu)建一個(gè)組件對(duì)象。另一個(gè)組件要調(diào)用 Button 組件時(shí),只需寫(xiě)成 <Button/>,就可以被解析成 React.createClass(Button) 方法來(lái)創(chuàng)建 Button 實(shí)例。
React.createClass會(huì)自綁定函數(shù)方法(不像React.Component只綁定需要關(guān)心的函數(shù))導(dǎo)致不必要的性能開(kāi)銷,增加代碼過(guò)時(shí)的可能性。
2. ES6 classes
ES6 classes 的寫(xiě)法是通過(guò) ES6 標(biāo)準(zhǔn)的類語(yǔ)法方式來(lái)構(gòu)建方法:
import React, { Component } from 'react';
class Button extends Component {
constructor(props) {
super(props);
}
static defaultProps = {
color: 'blue',
text: 'Confirm',
};
render() {
const { color, text } = this.props;
return (
<button className={`btn btn-${color}`}>
<em>{text}</em>
</button>
);
}
}
這里的直觀感受是從調(diào)用內(nèi)部方法變成了用類來(lái)實(shí)現(xiàn)。與 createClass 的結(jié)果相同的是,調(diào)用類實(shí)現(xiàn)的組件會(huì)創(chuàng)建實(shí)例對(duì)象。
說(shuō)明:
React 的所有組件都繼承自頂層類 React.Component。它的定義非常簡(jiǎn)潔,只是初始化了React.Component方法,聲明了 props、context、refs 等,并在原型上定義了 setState 和forceUpdate 方法。內(nèi)部初始化的生命周期方法與 createClass 方式使用的是同一個(gè)方法創(chuàng)建的。
3. 無(wú)狀態(tài)函數(shù)
使用無(wú)狀態(tài)函數(shù)創(chuàng)建的組件成為無(wú)狀態(tài)組件,是官方頗為推崇的組件。
function Button({ color = 'blue', text = 'Confirm' }) {
return (
<button className={`btn btn-${color}`}>
<em>{text}</em>
</button>
);
}
無(wú)狀態(tài)組件只傳入 props 和 context 兩個(gè)參數(shù),不存在 state,也沒(méi)有生命周期方法組件本身即是一個(gè) render 方法。不過(guò),像 propTypes 和 defaultProps 還是可以通過(guò)向方法設(shè)置靜態(tài)屬性來(lái)實(shí)現(xiàn)的。
注:只要有可能,盡量使用無(wú)狀態(tài)組件。
無(wú)狀態(tài)組件在調(diào)用時(shí)不會(huì)創(chuàng)建新實(shí)例,它創(chuàng)建時(shí)始終保持了一個(gè)實(shí)例,避免了不必要的檢查和內(nèi)存分配,做到了內(nèi)部?jī)?yōu)化。
