在教程開端先說些題外話,我喜歡在學習一門新技術或讀過一本書後,寫一篇教程或總結,既能幫助消化,也能加深印象和發現本身未注意的細節,寫的過程其實仍然是一個學習的過程。有個記錄的話,在將來須要用到相關知識時,也方便本身查閱。css
React既不是一個MVC框架,也不是一個模板引擎,而是Facebook在2013年推出的一個專一於視圖層,用來構建用戶界面的JavaScript庫。它推崇組件式應用開發,而組件(component)是一段獨立的、可重用的、用於完成某個功能的代碼,包含了HTML、CSS和JavaScript三部份內容。React爲了保持靈活性,只實現了核心功能,提供了少許的API,一些DOM方法都被剝離到了react-dom.js中。這麼作雖然輕巧,但有時候要完成特定的業務場景,仍是須要與其餘庫結合,例如Redux、Flux等。React不只讓函數式編程越發流行,還引入了JSX語法(能把HTML嵌入進JavaScript中)和Virtual DOM技術,大大提高了更新頁面時的性能。在React中,每一個組件的呈現和行爲都由特定的數據所決定,而數據的流動都是單向的,即單向數據流。在編寫React時,推薦使用ES6語法,官方的文檔也使用了ES6語法,所以,在學習React以前,建議對ES6有所瞭解,避免沒必要要的困惑。html
下面是一段較爲完整的React代碼,本文大部分的示例代碼來源於此,閱讀下面的代碼能夠對React有一個感性的認識。node
import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; class Search extends Component { //static defaultProps = { // url: "http://jane.com" //}; constructor(props) { super(props); this.state = { txt: "請輸入關鍵字" }; } componentWillMount() { console.log("component will mount"); } componentDidMount() { console.log("component did mount"); this.refs.txt.addEventListener("blur", (e) => { this.getValue(e); }); console.log(this.refs) } handle(keyword, e) { console.log(keyword); console.log(this); console.log(this.select.value); } getValue(e) { console.log(e.target.value); } refresh(e) { this.setState({ type: e.target.value }); } render() { console.log("render"); let {type} = this.state; console.log(type); return ( <div> {this.props.children} <select value={type} onChange={this.refresh.bind(this)}> <option value="1">標題</option> <option value="2">內容</option> </select> <select defaultValue={2} ref={(select) => this.select = select}> <option value="1">標題</option> <option value="2">內容</option> </select> <input placeholder={this.state.txt} ref="txt" defaultValue="教程" style={{marginLeft:10, textAlign:"center"}}/> <button className="btn" data-url={this.props.url} onClick={this.handle.bind(this, "REACT")}>{"<搜索>"}</button> </div> ); } } Search.defaultProps = { url: "http://jane.com" }; ReactDOM.render( <Search url="http://www.pwstrick.com"> <h1>React掃盲教程</h1> </Search>, document.getElementById("container") );
JSX是對JavaScript語法的一種擴展,它看起來像HTML,一樣擁有清晰的DOM樹狀結構和元素屬性,以下代碼所示。但與HTML不一樣的是,爲了不自動插入分號時出現問題,在最外層得用圓括號包裹,而且必須用一個元素包裹(例以下面的<div>元素)其它元素,全部的元素還必須得閉合。react
(<div>
<input placeholder={this.state.txt} />
<button className="btn">{"<搜索>"}</button>
</div>)
1)元素git
JSX中的元素分爲兩種:DOM元素和組件元素(也叫React元素),DOM元素就是HTML文檔映射的節點,首字母要小寫;而組件元素的首字母要大寫。不管是DOM元素仍是組件元素,最終都會經過React.createElement()方法轉換成JSON對象,以下所示,JSON對象是簡化過的。github
//React.createElement()方法 React.createElement("div", null, [ React.createElement("input", { placeholder: `${this.state.txt}` }, null), React.createElement("button", { className: "btn" }, "<搜索>") ]); //簡化過的JSON對象 { type: "div", props: { children: [ { type: "input", props: { placeholder: `${this.state.txt}` } }, { type: "button", props: { className: "btn", children: "<搜索>" } } ] } }
因爲JSX中的元素可以被編譯成對象,所以還能夠把它們應用到條件、循環等語句中,或者做爲一個值應用到變量和參數上。算法
2)屬性npm
JSX中的屬性要用駝峯的方式表示,例如maxlength要改爲maxLength、readonly要改爲readOnly。還有兩個比較特殊的屬性:class和for,因爲這兩個是JavaScript中的關鍵字,所以要更名,前者改爲className,後者改爲htmlFor。編程
JSX中的屬性值不只能夠是字符串,還能夠是表達式。若是是表達式,那麼就要用花括號(「{}」)包裹。而在JSX中,任何位置均可以使用表達式。瀏覽器
有一點要注意,爲了防止XSS的攻擊,React會把全部要顯示到DOM中的字符串進行轉義,例如「<搜索>」轉義成「<搜索>」。
3)Virtual DOM
衆所周知,DOM操做會引發重繪和重排,而這是很是消耗性能的。React能把元素轉換成對象,也就是說能夠用對象來表示DOM樹,而這個存在於內存中的對象正是Virtual DOM。Virtual DOM至關於一個緩存,當數據更新後,React會從新計算Virtual DOM,再與上一次的Virtual DOM經過diff算法作比對(以下圖所示),最後只在頁面中更新修改過的DOM。因爲大部分的操做都在內存中進行,所以性能將會有很大的提高。
組件的構建方式有3種:React.createClass()、ES6的類和函數。當用ES6的類來構建時,全部的組件都繼承自抽象基礎類React.Component,該抽象類聲明瞭state、props、defaultProps和displayName等屬性,定義了render()、setState()和forceUpdate()等方法。注意,在組件的構造函數constructor()中要調用super()函數,用於初始化this和執行抽象類的構造函數。
import React, { Component } from 'react'; class Search extends Component { constructor (props) { super(props); } render() { return (); } }
組件中的render()方法是必須的,它會返回一個元素、數字或字符串等各類值。render()是一個純函數,即輸出(返回值)只依賴輸入(參數),而且執行過程當中沒有反作用(不改變外部狀態)。
組件之間能夠相互嵌套,而它們的數據流是自頂向下流動的(以下圖所示),即父組件將數據傳給子組件。此處傳遞的數據就是組件的配置參數,由props屬性控制,而組件的內部狀態保存在state屬性中。
1)props
若是一個組件要作到可複用,那麼它應該是可配置的。爲此,React提供了props屬性,它的使用以下所示。
class Search extends Component { render() { return ( <div> <button className="btn" data-url={this.props.url}>{"<搜索>"}</button> </div> ); } } <Search url="http://www.pwstrick.com" />
先給Search組件定義一個名爲url的屬性,而後在組件內部,能夠經過引用props屬性來獲取url的值。有一點要注意,props是隻讀屬性,所以在組件內部沒法修改它。
React爲組件提供了默認的配置,能夠調用它的靜態屬性defaultProps。總共有兩種寫法實現默認配置,以下代碼所示,其中寫法一用到了ES6中的static關鍵字。
//寫法一 class Search extends Component { static defaultProps = { url: "http://jane.com" }; } //寫法二 Search.defaultProps = { url: "http://jane.com" }; <Search />
此時,即便組件不定義url屬性,在組件內部仍是會有值。
props還有一個特殊的屬性:children,它的值是組件內的子元素,以下代碼所示,children屬性的值爲「<h1>React掃盲教程</h1>」。
class Search extends Component { render() { return ( <div> {this.props.children} </div> ); } } <Search> <h1>React掃盲教程</h1> </Search>
2)state
組件的呈現會隨着內部狀態和外部配置而改變,一般會在組件的構造函數中初始化須要的內部狀態,以下代碼所示,爲文本框添加默認提示。
class Search extends Component { constructor (props) { super(props); this.state = { txt: "請輸入關鍵字" }; } }
React還提供了setState()方法,用於更新組件的狀態。注意,不要經過直接爲state賦值的方式來更新狀態,由於setState()方法在更新狀態後,還會調用render()方法,從新渲染組件。此外,React爲了提高性能,會把屢次setState()調用合併成一次,像下面這樣寫打印出的txt屬性的值仍然是前一次的值,所以狀態更新是異步的。
this.setState({ txt: "React" }); console.log(this.state.txt); //"請輸入關鍵字"
3)生命週期
組件的生命週期(life cycle)可簡單的分爲4個階段:初始化(Initialization)、掛載(Mounting)、更新(Updation)和卸載(Unmounting),具體以下圖所示。每一個階段都會對應幾個方法,其中包含will的方法會在某個方法以前被調用,而包含did的方法會在某個方法以後被調用。
一、在初始化階段,會設置props、state等屬性。
二、在掛載階段,兩個掛載方法將以組件的render()爲分界點。
三、更新階段發生在傳遞props或執行setState()的時候。
四、當一個組件被移除時,就會調用componentWillUnmount()方法。
當組件在頁面中輸出時,在控制檯將依次輸出「will mount」、「render」和「did mount」。
class Search extends Component { componentWillMount() { console.log("will mount"); } componentDidMount() { console.log("did mount"); } render() { console.log("render"); } }
1)ReactDOM
若是要把組件添加到真實的DOM中,那麼就須要使用ReactDOM中的render()方法,以下代碼所示,其實在前面已經調用過幾回這個方法了。
ReactDOM.render( <Search />, document.getElementById("container") );
此方法可接收三個參數,第一個是要渲染(即添加)的元素,第二個是容器元素(即添加的位置),第三個是可選的回調函數,會在渲染或更新以後執行。
ReactDOM還提供了另外兩個方法:unmountComponentAtNode()和findDOMNode(),具體可參考官方文檔。
2)事件
React實現了一種合成事件(SyntheticEvent),合成事件只有冒泡傳播,而且它的註冊方式、事件對象和事件處理程序中的this對象都與原生事件不一樣。
一、合成事件會經過設置元素的屬性來註冊事件,但與原生事件不一樣的是,屬性的命名要用駝峯的寫法而不是所有小寫,而且屬性值能夠是任意類型而再也不僅是字符串,以下代碼所示。React已經封裝好了一系列的事件類型(原生事件類型的一個子集),而且已經處理好它們的兼容性,提供的事件類型能夠參考官網。
class Search extends Component { handle(e) { console.log("click"); } render() { return ( <div> <button onClick={this.handle}>搜索</button> </div> ); } }
二、合成事件中的事件對象(event object)是一個基於W3C標準的SyntheticEvent對象的實例,它不但與原生的事件對象擁有相同的屬性和方法(例如cancelable、preventDefault()、stopPropagation()等),還完美解決了兼容性問題。
三、React的事件處理程序中的this對象默認是undefined,由於註冊的事件都是以普通函數的方式調用的。若是要讓this指向當前組件,那麼能夠用bind()方法或ES6的箭頭函數。
class Search extends Component { //bind()方法 handle1(e) { console.log(this); } //箭頭函數 handle2 = (e) => { console.log(this); }; render() { return ( <div> <button onClick={this.handle1.bind(this)}>搜索</button> <button onClick={this.handle2}>搜索</button> </div> ); } }
四、在向事件處理程序傳遞參數時,要把事件對象放在最後,以下代碼所示。
class Search extends Component { handle(keyword, e) { console.log(keyword); console.log(this); } render() { return ( <div> <button onClick={this.handle1.bind(this, "REACT")}>搜索</button> </div> ); } }
五、若是要爲組件中某個元素註冊原生事件,那麼能夠利用元素的ref屬性和組件的refs對象實現。例如實現一個文本框在失去焦點時,打印輸出它的值,以下代碼所示。注意,原生事件的註冊要在componentDidMount()方法內執行。
class Search extends Component { componentDidMount() { this.refs.txt.addEventListener("blur", (e) => { this.getValue(e); }); } getValue(e) { console.log(e.target.value); } render() { return ( <div> <input placeholder={this.state.txt} ref="txt" /> </div> ); } }
在上面的代碼中,ref屬性的值被設爲了「txt」,此時,在refs對象中就會出現一個名爲「txt」的屬性,關於這個它們的具體用法能夠參考官網。
3)表單
HTML中的表單元素(例如<input>、<select>和<radio>等)在React都有相應的組件實現,React還把表單中的組件分爲受控和非受控。
受控組件(controlled component)的狀態由React組件控制,它的每一個狀態的改變都會有一個與之對應的事件處理程序,而且在程序內部會調用setState()方法更新狀態。React推薦使用受控組件,下面是一個受控組件,注意,選擇框(<select>元素)中的value屬性表示選中項。
class Search extends Component { refresh(e) { this.setState({ type: e.target.value }); } render() { let {type} = this.state; return ( <div> <select value={type} onChange={this.refresh.bind(this)}> <option value="1">標題</option> <option value="2">內容</option> </select> </div> ); } }
非受控組件(uncontrolled component)的狀態不受React組件控制,也不用爲每一個狀態編寫對應的事件處理程序,但能夠經過元素的ref屬性獲取它的值,非受控組件的寫法更像是傳統的DOM操做。在使用非受控組件時,若是要爲其設置默認值,可使用屬性defaultValue或defaultChecked,具體以下所示。
class Search extends Component { handle(e) { console.log(this.select.value); } render() { return ( <div> <select defaultValue={2} ref={(select) => this.select = select}> <option value="1">標題</option> <option value="2">內容</option> </select> <button onClick={this.handle.bind(this)}>搜索</button> </div> ); } }
4)樣式
在React問世的初期,因爲它推崇組件模式,所以會要求HTML、CSS和JavaScript混合在一塊兒,這與過去的關注點分離正好相反。React已將HTML用JSX封裝,而對CSS的封裝,則拋出了CSS in JS的解決方案,即用JavaScript寫CSS。
在React中的元素都包含className和style屬性,前者可設置CSS類,後者可定義內聯樣式。style的屬性值是一個對象,其屬性就是CSS屬性,但屬性名要用駝峯的方式命名,例如margin-left改爲marginLeft,具體以下所示。
class Search extends Component { render() { return ( <div> <input style={{marginLeft:10, textAlign:"center"}}/> </div> ); } }
注意,屬性名不會自動補全瀏覽器前綴,而且React會自動給須要單位的數字加上px。在MDN上給出了CSS屬性用JavaScript命名的對應關係,可在此處參考。
因爲React處理CSS的功能並不強大,所以市面上出現了不少與CSS in JS相關第三方類庫,例如classnames、polished.js等,有外國網友還專門蒐集了40多種相關的類庫。
雖然這種方式能讓組件更方便的模塊化,但同時也完全拋棄了CSS,既不能使用CSS的特性(例如選擇器、媒體查詢等),也沒法再用CSS預處理器(例如SASS、LESS等)。爲了解決上述問題,又有人提出了CSS Modules。
若是要在React中製做動畫,官方推薦使用React Transition Group和React Motion。不過,你也可使用普通的動畫庫(例如animejs),只要在DOM渲染好之後調用便可。
1)跨級通訊
React數據流動是單向的,組件之間通訊最多見的方式是父組件經過props向子組件傳遞信息,但這種方式只能逐級傳遞,若是要跨級通訊(即父組件與孫子組件通訊),那麼能夠利用狀態提高實現,但這樣的話,代碼會顯得很不優雅而且很臃腫。好在React包含一個Context特性,能夠知足剛剛的需求,不過官方不建議大量使用該特性,由於它不但會增長組件之間的耦合性,還會讓應用變得混亂不堪,下圖演示了兩種數據傳遞的過程。在理解Context特性後,能更合理的使用狀態管理容器Redux。
當一個組件設置了Context後,它的子組件就能直接訪問Context中的內容,Context至關於一個全局變量,但做用域僅限於它的子組件中。總共有兩種Context的實現方式,都基於生產者消費者模式。首先來看第一種,具體代碼以下所示。
import PropTypes from 'prop-types'; class Grandpa extends Component { getChildContext() { return { name: "strick" }; } render() { return (<Son />); } } Grandpa.childContextTypes = { name: PropTypes.string }; class Son extends Component { render() { return (<Grandson />); } } class Grandson extends Component { render() { let { name } = this.context; return (<div>爺爺叫{name}</div>); } } Grandson.contextTypes = { name: PropTypes.string };
在上面的代碼中,建立了三個組件,Grandpa是最上層的父組件(生產者),Son是中間的子組件,Grandson是最下層的孫子組件(消費者)。首先在Grandpa中,聲明瞭一個靜態屬性childContextTypes和一個getChildContext()方法,這兩個是必須的,不然沒法實現數據傳遞。其中childContextTypes是一個對象,它的屬性名就是要傳遞的變量名,而屬性值則經過PropTypes指明瞭該變量的數據類型,getChildContext()方法返回的對象就是要傳遞的一組變量和它們的值。而後在Son中渲染Grandson組件。最後爲Grandson聲明一個靜態屬性contextTypes,一樣是個對象,而且屬性名和屬性值與childContextTypes中的相同。
第二種方式是在React 16.3的版本中引入的,比起第一種方式,寫法更加簡潔,而且Context的生產者和消費者都以組件的方式實現,以下所示。
let NameContext = React.createContext({ name }); class Grandpa extends Component { render() { return ( <NameContext.Provider value={{name: "strick"}}> <Son /> </NameContext.Provider> ); } } class Son extends Component { render() { return (<Grandson />); } } class Grandson extends Component { render() { return ( <NameContext.Consumer> {(context) => ( <div>爺爺叫{context.name}</div> )} </NameContext.Consumer> ); } }
上面的代碼依然建立了三個組件,名字也和第一種方式中的相同。除了中間組件Son以外,另外兩個組件的內容發生了變化。首先,經過React.createContext()方法建立一個Context對象,此對象包含兩個組件:Provider和Consumer,前者是生產者,後者是消費者。而後在Grandpa的render()方法中設置Provider組件的value屬性,此屬性至關於getChildContext()方法。最後在Grandson組件中調用Context對象,注意,Consumer組件的子節點只能是一個函數。
2)高階組件
高階組件(higher-order component,簡稱HOC)不是一個真的組件,而是一個函數,它的參數中包含組件,其返回值是一個功能加強的新組件。高階組件是一個沒有反作用的純函數,它遵循了裝飾者模式的設計思想,不會修改傳遞進來的原組件,而是對其進行包裝和拓展,不只加強了組件的複用性和靈活性,還保持了組件的易用性。下面演示了高階組件是如何控制props和state的。
class Button extends Component { render() { return ( <div> <button>{ this.props.txt }</button> </div> ); } } //高階組件 function HOC(Mine) { class Wrapped extends Component { constructor() { super(); this.state = { txt: "提交" }; } render() { return <Mine {...this.state} />; } } return Wrapped; } let Wrapped = HOC(Button);
高階組件HOC()的函數體中建立了一個名爲Wrapped的組件,在它的構造函數中初始化了state狀態。而後在其render()方法中使用了{...this.state},這是JSX的一種語法,在state對象前添加擴展運算符,可把它解構成組件的一組屬性。最後在Button組件中調用傳遞進來的屬性。
高階組件還有遷移重複代碼、劫持render()方法和引用refs等功能。
就先整理這些了,若有錯誤,歡迎指正,後面還會陸續加入漏掉的知識點。
最後,我想說下,其實本身也是一個React初學者,經過這樣的梳理後,對React有了更爲深入的理解,在後續的學習中能容易的吸取新的知識點。
源碼下載:
https://github.com/pwstrick/react
參考資料: