書籍完整目錄
1.3 React 組件
1.3.1 React 組件介紹
在 React 中組件是第一元素,是 React 的基礎,一個 React 應用就是基於 React 組件的組合而成。
前面的 JSX 練習過後,大家應該對 React 組件不陌生了,在這一節我們將温習以及深入學習 React 組件。
1.3.2 創建一個 React 組件
創建一個 React 組件的方法為,調用 React.createClass 方法,傳入的參數為一個對象,對象必須定義一個 render 方法,render 方法返回值為組件的渲染結構,也可以理解為一個組件實例(React.createElement 工廠方法的返回值),返回值有且只能為一個組件實例,或者返回 null/false,當返回值為 null/false 的時候,React 內部通過 <noscript/> 標籤替換。
eg:
var MyComponent = React.createClass({
render: function() {
return <p>....</p>;
}
});
組件命名空間
可以看出 React.createClass 生成的組件類為一個 Javascript 對象。 當我們想設置命名空間組件時,可以在組件下面添加子組件:
eg:
MyComponent.SubComponent = React.createClass({...});
MyComponent.SubComponent.Sub = React.createClass({....});
在組件較多的情況下,可以藉助命名空間優化組件維護結構以及解決組件名稱衝突問題。
無狀態組件
除了可以通過 React.createClass 來創建組件以外,組件也可以通過一個普通的函數定義,函數的返回值為組件渲染的結果。
eg:
function StatelessComponent(props) {
return <div> Hello {props.name} </div>
}
無狀態組件能夠優化性能,因為其內部不會維護狀態,React 內部不會有一個對應的組件實例,並且也沒有生命週期 hook。
1.3.3 將組件渲染到 DOM 中
當創建好了組件過後,為了將組件渲染到 DOM 中顯示出來,需要兩個步驟
-
在 HTML 中定義一個元素,設置 id 屬性
-
JSX 中調用 ReactDOM.render 方法, 第一個參數為 組件,第二個為剛才定義的 DOM 元素
eg:
<!-- 定義的 DOM 元素 -->
<div id="example"></div>
<script type="text/babel">
// 自定義組件
var MyComponent = React.createClass({
render: function() {
return <p>....</p>;
}
});
// 調用 render 方法
ReactDOM.render(
<MyComponent/>,
document.getElementById('example')
);
</script>
對於組件的渲染,可能涉及到的一些問題:
-
Q1: 只能 render 到一個元素嗎?
-
Q2: 在程序運行時能夠動態的調用 render 嗎?
-
Q3: 修改了數據過後,還需要重新調用 render 方法嗎?
這裏要先提一下 React 的設計初衷,React 在開發時候的目標就是簡單精巧,可以和其他框架結合起來使用。簡單而言我們可以當 React 是一個渲染數據對象到 DOM 中的 Javascript 函數工具類,工具類當然可以多次使用。
那麼對於上面的問題:
-
A1: 不是,React 可以渲染到多個元素,任意位置的元素。
-
A2: 可以,比如以一個彈出層組件為例,我們希望創建一個 append 到 body 最後的組件來實現全屏遮罩, 那麼我們可以動態的創建一個 div append 到 body 最後,然後將彈出層 render 到那個 div 內。
-
A3: 假設每次數據改變都重新調用 render 方法,那麼每次 render 帶來的結果是重新創建一個頂級組件實例,以及子組件實例。 如果只調用 render 一次,將數據的變更放在組件內部,那麼就不會重複創建頂級組件。
1.3.4 組件狀態 State
React 中每個組件可以存儲自己的當前狀態, 以一個 switch 開關組件為例,開關的狀態可以存儲在組件內部。
React 的渲染結果是由組件屬性和狀態共同決定的,狀態和屬性的區別是,狀態維護在組件內部,屬性是由外部控制,我們先介紹組件狀態相關細節:
控制狀態的 API 為:
-
this.state:組件的當前狀態
-
getInitialState:獲取組件的初始狀態,在組件加載的時候會被調用一次,返回值賦予 this.state 作為初始值
-
this.setState:
-
組件狀態改變時,可以通過 this.setState 修改狀態
-
setState 方法支持按需修改,如 state 有兩個字段,僅當 setState 傳入的對象包含字段 key 才會修改屬性
-
每次調用 setState 會導致重渲染調用 render 方法
-
直接修改 state 不會重渲染組件
-
eg:
var Switch = React.createClass({
// 定義組件的初始狀態,初始為關
getInitialState: function() {
return {
open: false
}
},
// 通過 this.state 獲取當前狀態
render: function() {
console.log('render switch component');
var open = this.state.open;
return <label className="switch">
<input type="checkbox" checked={open}/>
</label>
},
// 通過 setState 修改組件狀態
// setState 過後會 React 會調用 render 方法重渲染
toggleSwitch: function() {
var open = this.state.open;
this.setState({
open: !open
});
}
})
1.3.5 組件屬性 Props
前面已經提到過 React 組件可以傳遞屬性給組件,傳遞方法和 HTML 中無異, 可以通過 this.props 獲取組件屬性
屬性相關的 API 為:
-
this.props: 獲取屬性值
-
getDefaultProps: 獲取默認屬性對象,會被調用一次,當組件類創建的時候就會被調用,返回值會被緩存起來,當組件被實例化過後如果傳入的屬性沒有值,會返回默認屬性值
-
this.props.children:子節點屬性
-
propTypes: 屬性類型檢查
以一個代辦事項的列表項組件為例:
// props.name 表示代辦事項的名稱
var TodoItem = React.createClass({
render: function() {
var props = this.props;
return <div className="todo-item">
<span className="todo-item__name">{props.name}</span>
</div>
}
});
ReactDOM.render(
<TodoItem name="代辦事項1"/>,
document.getElementById('example'));
children 屬性
組件屬性中會有一個特殊屬性 children ,表示子組件, 還是以上面一個組件為例子,我們可以換一種方式定義 name:
var TodoItem = React.createClass({
render: function() {
var props = this.props;
return <div className="todo-item">
<span class="todo-item__name">{props.children}</span>
</div>
}
});
ReactDOM.render(
<TodoItem>代辦事項1</TodoItem>,
document.getElementById('example'));
需要注意的是,children 只能為一個元素,不能為多個組件
屬性類型檢查
為了保證組件傳遞屬性的正確性, 我們可以通過定義 propsType 對象來實現對組件屬性的嚴格校驗:
var MyComponent = React.createClass({
propTypes: {
optionalArray: React.PropTypes.array,
optionalBool: React.PropTypes.bool,
optionalFunc: React.PropTypes.func,
optionalNumber: React.PropTypes.number,
optionalObject: React.PropTypes.object,
optionalString: React.PropTypes.string,
// 任何可以被渲染的包括,數字,字符串,組件,或者數組
optionalNode: React.PropTypes.node,
// React 元素
optionalElement: React.PropTypes.element,
// 枚舉
optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),
// 任意一種類型
optionalUnion: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number,
React.PropTypes.instanceOf(Message)
]),
// 具體類型的數組
optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),
// 具體類型的對象
optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),
// 符合定義的對象
optionalObjectWithShape: React.PropTypes.shape({
color: React.PropTypes.string,
fontSize: React.PropTypes.number
}),
requiredFunc: React.PropTypes.func.isRequired,
requiredAny: React.PropTypes.any.isRequired,
// 自定義校驗
customProp: function(props, propName, componentName) {}
}
});
屬性傳遞的單向性
我們已經提到過 React 的單向數據流模式,數據的流動管道就是 props,流動的方向就是組件的層級自定向下的方向。所以一個組件是不能修改自身的屬性的,組件的屬性一定是通過父組件傳遞而來(或者默認屬性)。
無狀態組件屬性
對於無狀態組件,可以添加 .propTypes 和 .defaultProps 屬性到函數上。
1.3.6 組件的嵌套組合
在 1.2 節的 JSX 實例子中,當我們循環輸出 todo 列表的時候,React 會提示對於循環輸出的組件,需要有一個唯一的 key 屬性。這個問題的原因在於 React 的調和機制(Reconciliation)上。
什麼叫調和?
在每次數據更新過後,React 會重新調用 render 渲染出新的組件結構,新的結構應用到 DOM 中的過程就叫做調和過程。
為什麼需要調和?
想一想,假設我們有一個輸入組件,這個時候我們正聚焦在輸入框中,當修改值過後觸發事件導致了數據改變,數據改變導致了重渲染, 這個時候輸入框被替換成了新的 DOM。 這個過程對用户來説應該是無感知的,所以那原來的聚焦狀態應該被保存, 那怎麼做到的呢? DOM 都被替換了,輸入狀態,選擇狀態為什麼還能保存。 我們先不急着知道 How,目前只需要知道這就是調和過程,後面我們會在 React 實現原理章節進行詳細介紹。
除了保存狀態以外,調和過程還做了很多 DOM 優化。 比如輸出一個數組的時候,數據新增加或者減少了一下,或者數組項值改變了,實際上我們沒有必要刪除原來的 DOM 結構,只需要修改 DOM 的值或者刪除 DOM 就能實現重渲染。
這就是為什麼要有 key 屬性,key 屬性能夠幫助定位 DOM 與數組元素的關係,在重渲染的時候能夠實現渲染優化。
1.3.7 實例練習:通過組件化的方式優化之前的待辦事項列表
問題
優化 JSX 語法練習的 TODOMVC 頁面, 通過組件化的方式拆分頁面!
組件如下:
-
App 組件:整個頁面的最完成組件
-
Header 組件:頭部輸入組件
-
TodoList 組件:列表組件
-
TodoItem 組件: 列表項
-
Footer 組件:底部操作組件
Tips
循環輸出組件的方式
方式一:先計算出組件
function render() {
var todos = this.props.todos;
var $todos = todos.map(function(todo) {
return <Todo data={todo}/>
});
return <div>
{$todos}
</div>
}
方式二:{} 內直接計算
function render() {
var todos = this.props.todos;
return <div>
{todos.map(function(todo) {
return <Todo data={todo}/>
})}
</div>
}
參考答案
https://github.com/leanklass/leanreact/tree/component