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