React躬行記(3)——元件
元件(Component)由若干個React元素組成,包含屬性、狀態和生命週期等部分,滿足獨立、可複用、高內聚和低耦合等設計原則,每個React應用程式都是由一個個的元件搭建而成,即組成React應用程式的最小單元正是元件。
一、構建
目前推崇的構建元件的方式總共有兩種:類和函式,而用React.createClass()構建元件的方式已經過時,本節也不會對其做講解。
1)類元件
通過ES6新增的類構建而成的元件必須繼承自React.Component,並且需要定義render()方法。此方法用於元件的輸出,即元件的渲染內容,如下程式碼所示。注意,render()是一個純函式,不會改變元件的狀態,並且其返回值有多種,包括React元素、布林值、陣列等。
class Btn extends React.Component { render() { return <button>提交</button>; } }
2)函式元件
使用函式構建的元件只關注使用者介面的展示,既無狀態,也無生命週期。其功能相當於類元件的render()方法,但能接收一個屬性物件(props),下面是一個簡單的函式元件。
function Btn(props) { return <button>{props.text}</button>; }
與類元件不同,函式元件在呼叫時不會建立新例項。
二、state(元件狀態)
元件中的state用於記錄其內部狀態,這類有狀態元件會隨著state的變化修改其最終的呈現。
1)初始化
在元件的建構函式constructor()中可以像下面這樣,通過this.state初始化元件的內部狀態,其中this.state必須是一個物件。
class Btn extends React.Component { constructor() { super(); this.state = { text: "提交" }; } render() { return <button>{this.state.text}</button>; } }
注意,在初始化之前要先呼叫super(),因為ES6對兩個類的this的初始化順序做了規定,先父類,再子類,所以super()方法要在使用this之前呼叫。
如果要讀取this.state中的資料,那麼可以像上面的程式碼那樣通過成員訪問運算子得到。但如果要更新this.state中的資料,那麼就得用setState()方法,而不是用運算子。
2)setState()
此方法能接收2個引數,第一個是函式或物件,第二個是可選的回撥函式,會在更新之後觸發。下面用示例講解第一個引數的兩種情況(省略了建構函式以及初始化的程式碼),當它是函式時,能接收2個引數,第一個是當前的state,第二個是元件的props(將在下一節講解),此處函式的功能是交替變換按鈕的文字。
class Btn extends React.Component { change() { this.setState((state, props) => { return { text: state.text == "點選" ? "提交" : "點選" }; }); } render() { return <button onClick={this.change.bind(this)}>{this.state.text}</button>; } }
當setState()方法的第一個引數是物件時,可以將要更新的資料傳遞進來,就像下面這樣(省略了render()方法)。
class Btn extends React.Component { change() { this.setState({ text: "點選" }); } }
setState()是一個非同步方法,React會將多個setState()方法合併成一個呼叫,也就是說,在呼叫setState()後,不能馬上反映出狀態的變化。例如this.state.text的值原先是“提交”,在像下面這樣更新狀態後,打印出的值仍然是“提交”。
this.setState({text: "點選"}); console.log(this.state.text); //"提交"
setState()方法在將新資料合併到當前狀態之後,就會自動呼叫render()方法,驅動元件重新渲染。由此可知,在render()方法中不允許呼叫setState()方法,以免造成死迴圈。
在後面的生命週期一節中會講解元件在各個階段可用的回撥函式。其中有些回撥函式得在render()方法被執行後再被呼叫,因此在它們內部通過this.state得到的將是更新後的內部狀態。
三、props(元件屬性)
props(properties的縮寫)能接收外部傳遞給元件的資料,當元件作為React元素使用時,props就是一個由元素屬性所組成的物件。以Btn元件為例,它的props的結構如下所示,其中children是一個特殊屬性,後續將會單獨講解。
<Btn name="strick" digit={0}>提交</Btn> props = { name: "strick", digit: 0, children: "提交" }
1)讀取
每個元件都會有一個建構函式,而它的引數正是props。由於React元件相當於一個純函式,因此props不能被修改,它的屬性都是隻讀的,像下面這樣賦值勢必會引起元件的副作用,因而React會馬上終止程式,直接丟擲錯誤。
class Btn extends React.Component { constructor(props) { super(props); props.name = "freedom"; //錯誤 } }
因為有此限制,所以若要修改props中的某個屬性,通常會先將它賦給state,再通過state更新資料,如下所示。
class Btn extends React.Component { constructor(props) { super(props); this.state = { name: props.name }; } }
在建構函式之外,可通過this.props訪問到傳遞進來的資料。
2)defaultProps
元件的靜態屬性defaultProps可為props指定預設值,例如為元件設定預設的name屬性,當props.name預設時,就能用該值,如下所示。
class Btn extends React.Component { constructor(props) { super(props); } render() { return <button>{this.props.name}</button>; } } Btn.defaultProps = { name: "freedom" };
3)children
每個props都會包含一個特殊的屬性:children,表示元件的內容,即所包裹的子元件。例如下面這個Btn元件,其props.children的值為“搜尋”。
<Btn>搜尋</Btn>
children可以是null、字串或物件等資料型別,並且當元件的內容是多個子元件時,children還能自動變成一個數組。
官方通過React.Children給出了專門處理children的輔助方法,例如用於遍歷的forEach(),如下程式碼所示,其餘方法可參考表1。
React.Children.forEach(props.children, child => { console.log(child); });
表1 輔助方法
方法 | 描述 |
map() | 當children不是null或undefined時,遍歷children並返回一個數組,否則返回null或undefined |
forEach() | 功能與map()類似,但不返回陣列 |
count() | 計算children的數量 |
only() | 當children是一個React元素時,返回該元素,否則丟擲錯誤 |
toArray() | 將children轉換成陣列 |
當children是陣列時,React.Children中的map()和forEach()兩個方法與陣列中的功能類似,並且count()方法的返回值與陣列的length屬性相同。但當children是其它型別時,用React.Children中的輔助方法會比較保險,例如children為一個子元件“strick”,呼叫count()方法得到的值為1,而呼叫length屬性得到的值為6,這與當前子元件的數量不符。
4)校驗屬性
自React v15.5起,官方棄用了React.PropTypes,改用prop-types庫。此庫能校驗props中屬性的型別,例如將Btn元件的age屬性限制為數字,可以像下面這樣設定。
Btn.propTypes = { age: PropTypes.number }
在引入該庫後,就會有一個全域性物件PropTypes。除了數字型別之外,PropTypes還提供了其它型別的校驗,具體對應關係可參考表2。
表2 對應關係
PropTypes中的屬性 | 對應型別 |
PropTypes.array | 陣列 |
PropTypes.bool | 布林值 |
PropTypes.func | 函式 |
PropTypes.number | 數字 |
PropTypes.object | 物件 |
PropTypes.string | 字串 |
PropTypes.symbol | 符號 |
PropTypes.node | 可被渲染的元素,例如數字、React元素等 |
PropTypes.element | React元素 |
PropTypes.elementType | React元素型別 |
當元件的屬性是物件或陣列時,PropTypes能校驗其成員的型別,例如要求陣列的成員都得是字串、物件的某個屬性必須是布林值,可以像下面這樣操作。
Btn.propTypes = { names: PropTypes.arrayOf(PropTypes.string), person: PropTypes.shape({ isMan: PropTypes.bool }) }
PropTypes還能在其任意屬性後加isRequired標記,例如PropTypes.number.isRequired表示必須傳數字型別的屬性,並且不能預設。下面示例中的school屬性,不限制類型,只要有值就行。
Btn.propTypes = { school: PropTypes.any.isRequired }
此節只列出了prop-types庫中的部分功能,其餘功能可參考官方文件。
5)資料流
在React中,元件之間的資料是自頂向下單向流動,即父元件通過props將資料傳遞給子元件(如圖3所示),以此實現它們之間的對話和聯絡。
圖3 單向資料流
舉個簡單的例子,有兩個元件Container和Btn,其中Container是父元件,Btn是子元件,Container元件會將它的text屬性傳遞給Btn元件,以此完成資料的流動。為了便於觀察,省略了兩個元件的建構函式,具體如下所示。
class Btn extends React.Component { render() { return <button>{this.props.text}</button>; } } class Container extends React.Component { render() { return <Btn text="提交" />; } }
四、列表和Keys
在元件中渲染列表資料是非常常見的需要,例如輸出多個按鈕,如下程式碼所示。
class Btns extends React.Component { constructor(props) { super(props); } render() { const list = this.props.names.map(value => <button>{value}</button>); return <div>{list}</div>; } } ReactDOM.render( <Btns names={[1,2,3]}>按鈕列表</Btns>, document.getElementById("container") );
在上面的render()方法中,先通過map()方法遍歷傳遞進來的names屬性,再為這個陣列的每個元素加上<button>標籤,最後得到元素列表list後,將其作為返回值輸出。不過,此時會收到一個要求為列表中的子元素新增key屬性的警告,如圖4所示。
圖4 key屬性的警告
在React中,Keys會作為元素的身份標識,能夠幫助React識別出發生變化的元素,從而只渲染這些元素。每個元素的key屬性在當前列表中要保持唯一性,即在其兄弟元素之間要獨一無二,如下程式碼所示(省略了元件的建構函式)。
class Btns extends React.Component { render() { const list1 = this.props.names.map(value => <button key={value}>{value}</button>); const list2 = this.props.names.map(value => <button key={value}>{value}</button>); return ( <div> <section>{list1}</section> <section>{list2}</section> </div> ); } }
在上面的render()方法中,有兩個元素列表:list1和list2,雖然它們包含相同key屬性的<button>元素,但分別被嵌到了兩個<section>元素中,從而將兩者隔離,達成了key屬性唯一的目標。由此可知,key屬性不是全域性唯一的。
注意,一般不建議用陣列的索引作為key屬性的值,因為一旦陣列中元素的位置發生變化,其索引也會跟著改變,不利於渲染優化。
關於在何時設定key屬性,有個簡單的規則可以參考,那就是當元素位於map()方法內時,需要為該元素新增key屬性。
&n