React 是 Facebook 裡一群牛 X 的碼農折騰出的牛X的框架。 實現了一個虛擬 DOM,用 DOM 的方式將需要的組件秒加,用不著的秒刪。React 扮演著 MVC 結構中 V 的角色, 不過你要是 Flux 搭配使用, 你就有一個很牛X的能讓輕松讓 M 和 V 同步的框架了,Flux 的事以後再說~
組件們
在 React 中,你可以創建一個有特殊功能的組件,這在 HTML 元素裡你是打著燈籠也找不到的,比如這個教程裡的下拉導航。每個組件都有自己的地盤(scope),所以我們定義一個組件後可以反復的用反復的用,根本就不需要和別的組件交互!
每個組件都有一個功能叫 render , 它可以高效的返回要射到浏覽器上 HTML。我們也可以調用 React 的其他組件,這意味這一入 React 深似海,哦,不對,是只有想不到,沒有做不到。
JSX
如果你經常注意React你也許會發現有個東西叫JSX。JSX允許我們在Javascript中寫HTML,而不是用HTML包含Javascript。它能幫助我們快速開發,因為我們不用擔心字符串和換行等。你可以在浏覽器中運行JSX,但是不推薦,因為這樣會減慢你的頁面速度。gulp和grunt為你的預處理任務提供了一個JSX解釋器,所以如果你想使用JSX,我建議開啟這個功能。
使用 JSX
如前面所說,JSX每一個組件都有一個 render 方法,這個方法產生一個"ViewModel(視圖模型)" - 在返回HTML到組件之前,你可以將這個模型(ViewModel)的信息放入視圖中,意味著你的HTML會根據這個模型動態改變(例如一個動態列表)。
一旦你完成了操作,就可以返回你想渲染(render)的東西,我們使用JSX來做是如此簡單
var ExampleComponent = React.createClass({ render: function () { return ( <div className="navigation"> Hello World! </div> ); } });
如果你要在你的浏覽器中運行這段代碼,只會得到語法錯誤的提示,因為在Javascript中<和>字符要用引號包含起來。當你在代碼上運行JSX解釋器,它將被轉換成這樣
var ExampleComponent = React.createClass({ render: function () { return ( React.createElement('div', {className: 'navigation'}, 'Hello World!') ); } });
可以點此測試示例 - 我使用的是浏覽器JSX解釋器(這並不推薦,但是為了JSFiddle)。
運行了!JSX解釋了你使用 React.createElement 生成的所有DOM節點,生成了節點類型、參數和內容。你可以不用JSX,但這意味著你必須要手動寫 React.createElement 以外的所有DOM節點。無數例子告訴我要使用JSX。
你一定很疑惑我為什麼在DOM上使用 className 來代替 class。這是因為 class 是Javascript的保留詞。當JSX解釋你的代碼時就會改變這節點上的所有屬性到一個對象上,但你不能把對象當作屬性!
將變量用在屬性上
如果你想動態改變組件的樣式類(或者任何其他的屬性值),你可以使用變量. 不過你不能只是傳進去一個變量名稱,你還要將其用一對大括弧包起來, 這樣JSX就能知道這是一個外部的變量了.
var ExampleComponent = React.createClass({ render: function () { var navigationClass = 'navigation'; return ( <div className={ navigationClass }> Hello World! </div> ); } });
你可以在這兒看到這個功能.
初始的渲染器
當你最開始要渲染一個React組件時,你需要告訴React是要渲染什麼組件,還要制定一個現有的DOM節點以表示在哪兒渲染這個組件. 為此你會要使用React.render函數.
var ExampleComponent = React.createClass({ render: function () { return ( <div className="navigation"> Hello World! </div> ); } }); React.render(<ExampleContent />, document.body);
他將會在body節點上渲染組件——簡單吧! 從此你就可以像通常那樣調用其他的組件了, 或者如果你希望的話,可以使用render函數許多次, 如果你不想使用React來渲染整個頁面,但仍然想要使用多個組件.
一個組件的基礎
組件可以擁有其自己的“狀態”。這使我們能夠重復多次使用相同的組件但卻讓它們看起來完全不同,因為每個組件實例的狀態是唯一的。
當你通過傳遞屬性到一個組件時這些被稱為屬性。不要只局限於 HTML 屬性,你可以傳遞任何你想傳遞的東西,並在組件內通過 this.props 訪問它。這樣使得我們能夠重復使用相同的組件但傳遞一組不同的屬性,比如組件的“配置”。
屬性
根據我們前面“Hello World!”的例子,我們在 HTML 節點上有 className 的屬性。在組件內部,我們可以使用 this.props.classname 訪問這個值,但正如前面所述,你可以傳遞任何你喜歡的內容。對我們的下拉來說,我們需要將導航配置為對象,組件將使用它作為要渲染的配置。讓我們開始吧—
var navigationConfig = []; var Navigation = React.createClass({ render: function () { return ( <div className="navigation"> </div> ); } });
React.render(<Navigation config={ navigationConfig } />, document.body);
如果現在能訪問 this.props.config 的話,我們會受到一個空數組(navigationConfig 的值)。在我們進入到真正導航的編碼前先讓我們說明一下狀態。
狀態
如之前所討論的,每一個組件都有其自己的”狀態”。當要使用狀態時,你要定義初始狀態,讓後才可以使用 this.setState 來更新狀態。無論何時狀態得到了更新,組件都會再一次調用 render 函數,拿新的值去替換或者改變之前渲染的值。這就是虛擬 DOM 的奧義 - 計算差異的算法實在 React 內部完成的,因此我們不用去以來 DOM 的更新了(因為 DOM 很慢)。React 會計算出差異並產生一堆指令的集合 (例如,加入向”navigation__link“加入”active“類,或者移除一個節點),並在 DOM 上執行它們。
使用導航,我們將下拉菜單打開保持在狀態中。為此,添加一個 getInitialState 函數到類配置對象上,並返回帶有我們想要的初始狀態的對象。
var navigationConfig = []; var Navigation = React.createClass({ getInitialState: function () { return { openDropdown: -1 }; }, render: function () { return ( <div className="navigation"> </div> ); } }); React.render(<Navigation config={ navigationConfig } />, document.body);
你會發現我們將其設置成了-1。當我們准備去打開一個下拉菜單時,將使用狀態裡面的配置數組中的導航項的位置,而由於數組索引開始於0,我們就得使用 -1 來表示還沒有指向一個導航項。
我們可以使用 this.state 來訪問狀態,因此如果我們去觀察 atthis.state.openDropdown,應該會有 -1 被返回。
組件的生命周期
每個組件都有其“生命周期”—這是一系列你可以在組件配置裡定義的函數,它們將在部件生命周期內被調用。我們已經看過了 getinitialstate 一它只被調用一次,在組件被掛載時調用。
componentWillMount
當組件要被掛載時這個函數被調用。這意味著我們可以在此運行組件功能必須的代碼。為由於 render 在組件生命周期裡被多次調用,我們一般會把只需要執行一次的代碼放在這裡,比如 XHR 請求。
var ExampleComponent = React.createClass({ componentWillMount: function () { // xhr request here to get data }, render: function () { // this gets called many times in a components life return ( <div> Hello World! </div> ); } });
componentDidMount
一旦你的組件已經運行了 render 函數,並實際將組件渲染到了 DOM 中,componentDidMount 就會被調用。我們可以在這兒做任何需要做的 DOM 操作,已經任何需要依賴於組件已經實際存在於 DOM 之後才能做的事情, 例如渲染一個圖表。你可以通過調用 this.getDOMNode 在內部訪問到 DOM 節點。
var ExampleComponent = React.createClass({ componentDidMount: function () { var node = this.getDOMNode(); // render a chart on the DOM node }, render: function () { return ( <div> Hello World! </div> ); } });
componentWillUnmount
如果你准備吧組件從 DOM 移除時,這個函數將會被調用。這讓我們可以在組件背後進行清理,比如移除任何我們已經綁定的事件監聽器。如果我們沒有在自身背後做清理,而當其中一個事件被觸發時,就會嘗試去計算一個沒有載入的組件,React 就會拋出一個錯誤。
var ExampleComponent = React.createClass({ componentWillUnmount: function () { // unbind any event listeners specific to this component }, render: function () { return ( <div> Hello World! </div> ); } });
組件方法
React 也為組件提供了更方便我們工作的方法。它們會在組件的創建過程中被調用到。例如getInitialState,能讓我們定義一個默認的狀態,這樣我們不用擔心在代碼裡面對狀態項目是否存在做進一步的檢查了。
getDefaultProps
當我們創建組件時,可以按照我們的想法為組件的屬性定義默認值。這意味著我們在調用組件時,如果給這些屬性設置值,組件會有一個默認的“配置”,而我們就不用操心在下一行代碼中檢查屬性這類事情了。
當你定義了組件時,這些默認的屬性會被緩存起來,因而針對每個組件的實例它們都是一樣的,並且不能被改變。對於導航組件,我們將配置指定為一個空的數組,因而就算沒有傳入配置,render 方法內也不會發生錯誤。
var Navigation = React.createClass({ getInitialState: function () { return { openDropdown: -1 }; }, getDefaultProps: function () { return { config: [] } }, render: function () { return ( <div className="navigation"> </div> ); } });React.render(<Navigation config={ navigationConfig } />, document.body);
propTypes
我們也可以隨意為每一個屬性指定類型。這對於我們檢查和處理屬性的意外賦值非常有用。如下面的dropdown,我們指定只有數組才能放入配置。
var Navigation = React.createClass({ getInitialState: function () { return { openDropdown: -1 }; }, getDefaultProps: function () { return { config: [] } }, propTypes: { config: React.PropTypes.array }, render: function () { return ( <div className="navigation"> </div> ); } }); React.render(<Navigation config={ navigationConfig } />, document.body);
mixins
我們也可以在組件中加入 mixins。這是一個獨立於 React 的基本組件(只是一個對象類型的配置)。這意味著,如果我們有兩個功能類似的組件,就可以共享一個配置了(如果初始狀態相同)。我們可以抽象化地在 mixin 中建立一個方法,這樣就不用把相同的代碼寫上兩次了。
var ExampleMixin = { componentDidMount: function () { // bind some event listeners here }, componentWillUnmount: function () { // unbind those events here! } }; var ExampleComponent = React.createClass({ mixins: [ExampleMixin], render: function () { return ( <div> Hello World! </div> ); } }); var AnotherComponent = React.createClass({ mixins: [ExampleMixin], render: function () { return ( <div> Hello World! </div> ); } });
這樣全部組件都有一樣的 componentDidMount 和 componentWillUnmount 方法了,保存我們重寫的代碼。無論如何,你不能 override(覆蓋)這些屬性,如果這個屬性是在mixin裡設置的,它在這個組件中是不可覆蓋的。
遍歷循環
當我們有一個包含對象的數組,如何循環這個數組並渲染每一個對象到列表項中?JSX 允許你在任意 Javascript 文件中使用它,你可以映射這個數組並返回 JSX,然後使用 React 去渲染。
var navigationConfig = [ { href: 'http://ryanclark.me', text: 'My Website' } ]; var Navigation = React.createClass({ getInitialState: function () { return { openDropdown: -1 }; }, getDefaultProps: function () { return { config: [] } }, propTypes: { config: React.PropTypes.array }, render: function () { var config = this.props.config; var items = config.map(function (item) { return ( <li className="navigation__item"> <a className="navigation__link" href={ item.href }> { item.text } </a> </li> ); }); return ( <div className="navigation"> { items } </div> ); } }); React.render(<Navigation config={ navigationConfig } />, document.body);
在 JSFilddle 中隨意使用 navigationConfigin
導航配置由數組和對象組成,包括一個指向超鏈接的 href 屬性和一個用於顯示的 text 屬性。當我們映射的時候,它會一個個依次通過這個數組去取得對象。我們可以訪問 href 和 text,並在 HTML 中使用。當返回列表時,這個數組裡的列表項都將被替換,所以我們把它放入 React 中處理時它將知道怎麼去渲染了!
混編
到目前位置,我們已經做到了所有下拉列表的展開。我們需要知道被下來的項目是哪個,我們將使用 .children 屬性去遍歷我們的 navigationConfig 數組。接下來,我們可以通過循環來操作下拉的子元素條目。
var navigationConfig = [ { href: 'http://ryanclark.me', text: 'My Website', children: [ { href: 'http://ryanclark.me/how-angularjs-implements-dirty-checking/', text: 'Angular Dirty Checking' }, { href: 'http://ryanclark.me/getting-started-with-react/', text: 'React' } ] } ]; var Navigation = React.createClass({ getInitialState: function () { return { openDropdown: -1 }; }, getDefaultProps: function () { return { config: [] } }, propTypes: { config: React.PropTypes.array }, render: function () { var config = this.props.config; var items = config.map(function (item) { var children, dropdown; if (item.children) { children = item.children.map(function (child) { return ( <li className="navigation__dropdown__item"> <a className="navigation__dropdown__link" href={ child.href }> { child.text } </a> </li> ); }); dropdown = ( <ul className="navigation__dropdown"> { children } </ul> ); } return ( <li className="navigation__item"> <a className="navigation__link" href={ item.href }> { item.text } </a> { dropdown } </li> ); }); return ( <div className="navigation"> { items } </div> ); } }); React.render(<Navigation config={ navigationConfig } />, document.body);
實例在這裡 - 但是我們還是能看見下來條目,盡管我們將 openDropdown 設置成為了 -1 。
我們可以通過在組件中訪問 this.state ,來判斷下拉是否被打開了,並且,我們可以為其添加一個新的 css 樣式 class 來展現鼠標 hover 的效果。
var navigationConfig = [ { href: 'http://ryanclark.me', text: 'My Website', children: [ { href: 'http://ryanclark.me/how-angularjs-implements-dirty-checking/', text: 'Angular Dirty Checking' }, { href: 'http://ryanclark.me/getting-started-with-react/', text: 'React' } ] } ]; var Navigation = React.createClass({ getInitialState: function () { return { openDropdown: -1 }; }, getDefaultProps: function () { return { config: [] } }, openDropdown: function (id) { this.setState({ openDropdown: id }); }, closeDropdown: function () { this.setState({ openDropdown: -1 }); }, propTypes: { config: React.PropTypes.array }, render: function () { var config = this.props.config; var items = config.map(function (item, index) { var children, dropdown; if (item.children) { children = item.children.map(function (child) { return ( <li className="navigation__dropdown__item"> <a className="navigation__dropdown__link" href={ child.href }> { child.text } </a> </li> ); }); var dropdownClass = 'navigation__dropdown'; if (this.state.openDropdown === index) { dropdownClass += ' navigation__dropdown--open'; } console.log(this.state.openDropdown, index); dropdown = ( <ul className={ dropdownClass }> { children } </ul> ); } return ( <li className="navigation__item" onMouseOut={ this.closeDropdown } onMouseOver={ this.openDropdown.bind(this, index) }> <a className="navigation__link" href={ item.href }> { item.text } </a> { dropdown } </li> ); }, this); return ( <div className="navigation"> { items } </div> ); } }); React.render(<Navigation config={ navigationConfig } />, document.body);
在這裡看實例 - 鼠標劃過“My Website”,下拉即會展現。
在前面,我已經給每個列表項添加了鼠標事件。如你所見,我用的是 .bind (綁定) 調用,而非其它的方式調用 - 這是因為,當用戶的鼠標劃出元素區域,我們並不用關注光標在哪裡,所有我們需要知曉的是,將下拉關閉掉,所以我們可以將它的值設置成為-1。但是,我們需要知曉的是當用戶鼠標劃入的時候哪個元素被下拉展開了,所以我們需要知道該參數(元素的索引)。 我們使用綁定的方式去調用而非簡單的透過函數(function)去調用是因為我們需要通過 React 去調用。如果我們直接調用,那我們就需要一直調用,而不是在事件中調用他。
現在我們可以添加很多的條目到 navigationConfig 當中,而且我們也可以給他添加樣式到下來功能當中。查看實例.