[1]JSX [2]樣式設置 [3]組件 [4]State [5]事件處理 [6]列表 [7]表單 [8]propTypes [9]返回多個元素 [10]context ...
前面的話
React讓組件化成為了前端開發的基本思路,比傳統思路可以更好的控制前端複雜度,舊的開發方法受到了影響,如分離式的HTML/CSS、非侵入式JS、模板語言、MVC、CSS文件、Bootstrap等。在React中,組件把數據翻譯成UI,數據通過組件props屬性傳入,組件自身狀態通過state狀態值來控制。 每個組件都是一個狀態機,也就是聲明式編程。數據有變化,組件自動刷新。本文將詳細介紹React基本概念
JSX
JSX是Javascript的語法擴展(extension),可以讓我們在Javascript中可以編寫像HTML一樣的代碼。
JSX用來聲明 React 當中的元素,JSX 中使用 JavaScript 表達式,JSX中的表達式要包含在大括弧里
【模板字元串】
可以在JSX中使用模板字元串
{`Joined in ${time}`}
【屬性】
可以使用引號來定義以字元串為值的屬性:
const element = <div tabIndex="0"></div>;
也可以使用大括弧來定義以 JavaScript 表達式為值的屬性:
const element = <img src={user.avatarUrl} />;
下麵這兩個 JSX 表達式是等價的
<MyComponent message="hello world" /> <MyComponent message={'hello world'} />
【預設為true】
如果沒有給屬性傳值,它預設為 true
<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />
【擴展屬性】
如果已經有了個 props 對象,並且想在 JSX 中傳遞它,可以使用 ... 作為擴展操作符來傳遞整個屬性對象。下麵兩個組件是等效的:
function App1() { return <Greeting firstName="Ben" lastName="Hector" />; } function App2() { const props = {firstName: 'Ben', lastName: 'Hector'}; return <Greeting {...props} />; }
【return】
return一定要緊挨著左括弧,否則不生效
【JSX是進步還是倒退】
長期以來,一直不倡導在HTML中使用onclick,為什麼在JSX中卻要使用onClick這樣的方式來添加事件處理函數呢?
在React出現之初,很多人對React這樣的設計非常反感,因為React把類似HTML的標記語言和Javascript混在一起了。但是,隨著時間的推移,業界逐漸認可了這種方式,因為大家發現,以前用HTML來代表內容,用CSS代表樣式,用Javascript來定義交互行為,這三種語言分在三種不同的文件裡面,實際上是把不同技術分開管理了,而不是邏輯上的“分而治之”
根據做同一件事的代碼應該有高耦合性的設計原則,為什麼不把實現這個功能的所有代碼集中在一個文件里呢?
在JSX中使用onClick來添加事件處理函數,是否代表網頁應用開發兜了一個大圈,最終回到了起點呢?
不是的,在HTML中直接使用onclick很不專業,因為onclick添加的事件處理函數是在全局環境下執行的,這污染了全局環境,很容易產生意料不到的後果;給很多DOM元素添加onclick事件,可能會影響網頁的性能;對於使用onclick的DOM元素,如果在DOM元素刪除後忘了註銷事件處理函數,可能會造成記憶體泄漏
上面說的這些問題在JSX中都不存在
onClick掛載的每個函數,都可以控制在組件範圍內,不會污染全局空間;在JSX中使用了onClick,但並沒有產生直接使用onclick的HTML,而是使用事件委托的方式處理,無論多少個onclick出現,最後都只在DOM樹上添加了一個事件處理函數,掛在最頂層的DOM節點上;因為React控制了組件的生命周期,在unmount時自然能夠清除相關的所有事件處理函數,記憶體泄漏也不再是一個問題
樣式設置
【行內樣式】
當屬性的類型不是字元串類型時,在JSX中必須用花括弧{}把prop值包住。所以style的值有兩層花括弧
行內樣式使用如下寫法
{{color:'red',backgroundColor:'blue'}}
【圖片】
圖片的相對引用使用如下寫法
<img src={require('./common/img/128H.jpg')} alt="" />
【CSS引入】
require('./common/style/main.css')
或者
import '@/assets/global.css'
【class設置】
<div className="test"></div>
【自定義屬性】
<div data-abc="123"></div>
組件
作為軟體設計的通用原則,組件的劃分要滿足高內聚和低耦合。高內聚是指把邏輯緊密相關的內容放在一個組件中。低耦合是指不同組件之間的依賴關係要儘量弱化,也就是每個組件要儘量獨立
組件從概念上看就像是函數,它可以接收任意的輸入值(稱之為“props”),並返回一個需要在頁面上展示的React元素
[註意]組件可以嵌套自身
【函數組件】
定義一個組件最簡單的方式是使用JavaScript函數
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
【類組件】
class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }
prop
當React遇到的元素是用戶自定義的組件,它會將JSX屬性作為單個對象傳遞給該組件,這個對象稱之為“props”
function Welcome(props) { return <h1>Hello, {props.name}</h1>; } const element = <Welcome name="Sara" />; ReactDOM.render( element, document.getElementById('root') );
【只讀性】
無論是使用函數或是類來聲明一個組件,它決不能修改它自己的props
【隱藏組件】
讓 render
方法返回 null
可以隱藏組件
【父傳子】
下麵的例子來展示父級如何通過props把數據傳遞給子級
class ControlPanel extends Component { render() { return ( <div> <Counter caption="First"/> <Counter caption="Second" initValue={10} /> <Counter caption="Third" initValue={20} /> <button onClick={ () => this.forceUpdate() }> Click me to re-render! </button> </div> ); } }
【讀取props】
下麵的例子展示子級如何讀取父級傳遞來的props
class Counter extends Component { constructor(props) { super(props);this.state = { count: props.initValue } }
【props檢查】
一個組件應該規範以下內容:這個組件支持哪些prop,以及每個prop應該是什麼樣的格式。React通過propTypes來支持這些功能
Counter.propTypes = { caption: PropTypes.string.isRequired, initValue: PropTypes.number }; Counter.defaultProps = { initValue: 0 };
【子傳父】
React組件要反饋數據在父組件時,可以使用prop。函數類型的prop等於讓父組件交給子組件一個回調函數,子組件在恰當的時機調用函數類型的prop,可以帶上必要的參數,這樣就可以反過來把信息傳遞給父級
下麵的例子中,onUpdate是子組件向父組件傳遞數據的渠道
//子組件 class Counter extends Component { constructor(props) { super(props); this.onClickIncrementButton = this.onClickIncrementButton.bind(this); this.onClickDecrementButton = this.onClickDecrementButton.bind(this); this.state = {count: props.initValue} } onClickIncrementButton() { this.updateCount(true); } onClickDecrementButton() { this.updateCount(false); } updateCount(isIncrement) { const previousValue = this.state.count; const newValue = isIncrement ? previousValue + 1 : previousValue - 1; this.setState({count: newValue}) this.props.onUpdate(newValue, previousValue) } render() { const {caption} = this.props; return ( <div> <button style={buttonStyle} onClick={this.onClickIncrementButton}>+</button> <button style={buttonStyle} onClick={this.onClickDecrementButton}>-</button> <span>{caption} count: {this.state.count}</span> </div> ); } } Counter.propTypes = { caption: PropTypes.string.isRequired, initValue: PropTypes.number, onUpdate: PropTypes.func }; Counter.defaultProps = { initValue: 0, onUpdate: f => f }; export default Counter;
//父組件 class ControlPanel extends Component { constructor(props) { super(props); this.onCounterUpdate = this.onCounterUpdate.bind(this); this.initValues = [ 0, 10, 20]; const initSum = this.initValues.reduce((a, b) => a+b, 0); this.state = {sum: initSum}; } onCounterUpdate(newValue, previousValue) { const valueChange = newValue - previousValue; this.setState({ sum: this.state.sum + valueChange}); } render() { return ( <div> <Counter onUpdate={this.onCounterUpdate} caption="First" /> <Counter onUpdate={this.onCounterUpdate} caption="Second" initValue={this.initValues[1]} /> <Counter onUpdate={this.onCounterUpdate} caption="Third" initValue={this.initValues[2]} /> <div>Total Count: {this.state.sum}</div> </div> ); } } export default ControlPanel;
【局限】
設想一下,在一個應用中,包含三級或三級以上的組件結構,頂層的祖父級組件想要傳遞一個數據給最低層的子組件,用prop的方式,就只能通過父組件中轉,也許中間那一層根本用不上這個prop,但是依然需要支持這個prop,扮演好搬運工的角色,只因為子組件用得上,這明顯違反了低耦合的設計要求。於是,提出了專門的狀態管理的概念
State
如何組織數據是程式的最重要問題。React組件的數據分為兩種:prop和state。無論prop還是state的改變,都可能引發組件的重新渲染
狀態state與屬性props十分相似,但是狀態是私有的,完全受控於當前組件。prop是組件的對外介面,state是組件的內部狀態
由於React不能直接修改傳入的prop,所以需要記錄自身數據變化,就要使用state
【state與prop的區別】
下麵來總結下state與prop的區別
1、prop用於定義外部介面,state用於記錄內部狀態
2、prop的賦值在父組件使用該組件時,state的賦值在該組件內部
3、組件不可修改prop的值,而state存在的目的就是讓組件來改變的
組件的state,相當於組件的記憶,其存在意義就是被改變,每一次通過this.setState函數修改state就改變了組件的狀態,然後通過渲染過程把這種變化體現出來
【正確使用state】
1、不要直接更新狀態,構造函數是唯一能夠初始化 this.state
的地方
如果直接修改this.state的值,雖然事實上改變了組件的內部狀態,但只是野蠻地修改了state,但沒有驅動組件進行重新渲染。而this.setState()函數所做的事情,就是先改變this.state的值,然後驅動組件重新渲染
// Wrong this.state.comment = 'Hello'; // Correct this.setState({comment: 'Hello'});
2、狀態更新可能是非同步的
setState是非同步更新,而不是同步更新,下麵是一個例子
setYear(){ let {year} = this.state this.setState({ year: year + 10 //新值 }) console.log(this.state.year)//舊值 }
setYear(){ setTimeout(() => { this.setState({ year: year + 10 //新值 }) console.log(this.state.year)//新值 }) }
因為 this.props
和 this.state
可能是非同步更新的,不應該依靠它們的值來計算下一個狀態
// Wrong this.setState({ counter: this.state.counter + this.props.increment, });
要修複它,要使用第二種形式的 setState()
來接受一個函數而不是一個對象。 該函數將接收先前的狀態作為第一個參數,將此次更新被應用時的props做為第二個參數:
// Correct this.setState((prevState, props) => ({ counter: prevState.counter + props.increment }));
3、狀態更新合併
可以調用 setState()
獨立地更新它們,但React將多個setState()
調用合併成一個調用來提高性能。
componentDidMount() { fetchPosts().then(response => { this.setState({ posts: response.posts }); }); fetchComments().then(response => { this.setState({ comments: response.comments }); }); }
這裡的合併是淺合併,也就是說this.setState({comments})
完整保留了this.state.posts
,但完全替換了this.state.comments
4、回調函數
由於setState是非同步更新的,如果需要確定setState更新後,再進行某些操作,可以使用setState的回調函數
this.setState({ val:value },() => { this.ref.editInput.focus() })
事件處理
React 元素的事件處理和 DOM元素的很相似。但是有一點語法上的不同:
1、React事件綁定屬性的命名採用駝峰式寫法,而不是小寫
2、如果採用 JSX 的語法需要傳入一個函數作為事件處理函數,而不是一個字元串(DOM元素的寫法)
<button onClick={activateLasers}>
Activate Lasers
</button>
[註意]在 React 中不能使用返回 false
的方式阻止預設行為。必須明確的使用 preventDefault
【綁定this】
可以使用bind()方法
this.handleClick = this.handleClick.bind(this);
也可以使用屬性初始化器語法
handleClick = () => { console.log('this is:', this); }
如果沒有使用屬性初始化器語法,可以在回調函數中使用箭頭函數
class LoggingButton extends React.Component { handleClick() { console.log('this is:', this); } render() { return ( <button onClick={(e) => this.handleClick(e)}> Click me </button> ); } }
使用這個語法有個問題就是每次 LoggingButton
渲染的時候都會創建一個不同的回調函數。在大多數情況下,這沒有問題。然而如果這個回調函數作為一個屬性值傳入低階組件,這些組件可能會進行額外的重新渲染。通常建議在構造函數中綁定或使用屬性初始化器語法來避免這類性能問題
【傳遞參數】
以下兩種方式都可以向事件處理程式傳遞參數:
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button> <button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
[註意]通過 bind
方式向監聽函數傳參,在類組件中定義的監聽函數,事件對象 e
要排在所傳遞參數的後面
class Popper extends React.Component{ preventPop(name, e){ e.preventDefault(); alert(name); } render(){ return (<a href="https://reactjs.org" onClick={this.preventPop.bind(this,this.state.name)}>Click</a> ); } }
【原生事件對象】
handleClick(e){
e.nativeEvent
}
列表
【keys】
Keys可以在DOM中的某些元素被增加或刪除的時候幫助React識別哪些元素髮生了變化。因此應當給數組中的每一個元素賦予一個確定的標識
const numbers = [1, 2, 3, 4, 5]; const listItems = numbers.map((number) => <li key={number.toString()}> {number} </li> );
一個元素的key最好是這個元素在列表中擁有的一個獨一無二的字元串。通常,使用來自數據的id作為元素的key
const todoItems = todos.map((todo) => <li key={todo.id}> {todo.text} </li> );
當元素沒有確定的id時,可以使用序列號索引index作為key
const todoItems = todos.map((todo, index) => <li key={index}> {todo.text} </li> );
[註意]如果列表可以重新排序,不建議使用索引來進行排序,因為這會導致渲染變得很慢
JSX允許在大括弧中嵌入任何表達式
function NumberList(props) { const numbers = props.numbers; return ( <ul> {numbers.map((number) => <ListItem key={number.toString()} value={number} /> )} </ul> ); }
表單
【受控組件】
在HTML當中,像<input>,<textarea>, 和 <select>這類表單元素會維持自身狀態,並根據用戶輸入進行更新。但在React中,可變的狀態通常保存在組件的狀態屬性中,並且只能用 setState() 方法進行更新
通過使react變成一種單一數據源的狀態來結合二者。React負責渲染表單的組件仍然控制用戶後續輸入時所發生的變化。相應的,其值由React控制的輸入表單元素稱為“受控組件”
class NameForm extends React.Component { constructor(props) { super(props); this.state = {value: ''}; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } handleChange(event) { this.setState({value: event.target.value}); } handleSubmit(event) { alert('A name was submitted: ' + this.state.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> Name: <input type="text" value={this.state.value} onChange={this.handleChange} /> </label> <input type="submit" value="Submit" /> </form> ); } }
由於 value 屬性是在表單元素上設置的,因此顯示的值將始終為 React數據源上this.state.value 的值。由於每次按鍵都會觸發 handleChange 來更新當前React的state,所展示的值也會隨著不同用戶的輸入而更新
【textarea】
在HTML當中,<textarea> 元素通過子節點來定義它的文本內容。在React中,<textarea>會用value屬性來代替。這樣的話,表單中的<textarea> 非常類似於使用單行輸入的表單:
<textarea value={this.state.value} onChange={this.handleChange} />
【select】
在React中,並不使用之前的selected屬性,而在根select標簽上用value屬性來表示選中項。這在受控組件中更為方便,因為只需要在一個地方來更新組件
<select value={this.state.value} onChange={this.handleChange}> <option value="grapefruit">Grapefruit</option> <option value="lime">Lime</option> </select>
【多個input】
有處理多個受控的input元素時,可以通過給每個元素添加一個name屬性,來讓處理函數根據 event.target.name的值來選擇做什麼
class Reservation extends React.Component { constructor(props) { super(props); this.state = { isGoing: true, numberOfGuests: 2 }; this.handleInputChange = this.handleInputChange.bind(this); } handleInputChange(event) { const target = event.target; const value = target.type === 'checkbox' ? target.checked : target.value; const name = target.name; this.setState({ [name]: value }); } render() { return ( <form> <label> Is going: <input name="isGoing" type="checkbox" checked={this.state.isGoing} onChange={this.handleInputChange} /> </label> <br /> <label> Number of guests: <input name="numberOfGuests" type="number" value={this.state.numberOfGuests} onChange={this.handleInputChange} /> </label> </form> ); } }
propTypes
要檢查組件的屬性,需要配置特殊的 propTypes 屬性
import PropTypes from 'prop-types'; class Greeting extends React.Component { render() { return ( <h1>Hello, {this.props.name}</h1> ); } } Greeting.propTypes = { name: PropTypes.string };
react支持如下驗證
import PropTypes from 'prop-types'; MyComponent.propTypes = { // 可以將屬性聲明為以下 JS 原生類型 optionalArray: PropTypes.array, optionalBool: PropTypes.bool, optionalFunc: PropTypes.func, optionalNumber: PropTypes.number, optionalObject: PropTypes.object, optionalString: PropTypes.string, optionalSymbol: PropTypes.symbol, // 任何可被渲染的元素(包括數字、字元串、子元素或數組)。 optionalNode: PropTypes.node, // 一個 React 元素 optionalElement: PropTypes.element, // 也可以聲明屬性為某個類的實例 optionalMessage: PropTypes.instanceOf(Message), // 也可以限制屬性值是某個特定值之一 optionalEnum: PropTypes.oneOf(['News', 'Photos']), // 限制它為列舉類型之一的對象 optionalUnion: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, PropTypes.instanceOf(Message) ]), // 一個指定元素類型的數組 optionalArrayOf: PropTypes.arrayOf(PropTypes.number), // 一個指定類型的對象 optionalObjectOf: PropTypes.objectOf(PropTypes.number), // 一個指定屬性及其類型的對象 optionalObjectWithShape: PropTypes.shape({ color: PropTypes.string, fontSize: PropTypes.number }), // 也可以在任何 PropTypes 屬性後面加上 `isRequired` 尾碼 requiredFunc: PropTypes.func.isRequired, // 任意類型的數據 requiredAny: PropTypes.any.isRequired, // 也可以指定一個自定義驗證器。它應該在驗證失敗時返回 // 一個 Error 對象而不是 `console.warn` 或拋出異常。 // 不過在 `oneOfType` 中它不起作用。 customProp: function(props, propName, componentName) { if (!/matchme/.test(props[propName])) { return new Error( 'Invalid prop `' + propName + '` supplied to' + ' `' + componentName + '`. Validation failed.' ); } }, // 可以提供一個自定義的 `arrayOf` 或 `objectOf` 驗證器,它應該在驗證失敗時返回一個 Error 對象。 它被用於驗證數組或對象的每個值。驗證器前兩個參數的第一個是數組或對象本身,第二個是它們對應的鍵。 customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) { if (!/matchme/.test(propValue[key])) { return new Error( 'Invalid prop `' + propFullName + '` supplied to' + ' `' + componentName + '`. Validation failed.' ); } }) };
【限制單個子代】
使用 PropTypes.element 可以指定只傳遞一個子代
import PropTypes from 'prop-types'; class MyComponent extends React.Component { render() { const children = this.props.children; return ( <div> {children} </div> ); } } MyComponent.propTypes = { children: PropTypes.element.isRequired };
【屬性預設值】
可以通過配置 defaultProps
為 props
定義預設值
class Greeting extends React.Component { render() { return ( <h1>Hello, {this.props.name}</h1> ); } } // 為屬性指定預設值: Greeting.defaultProps = { name: 'Stranger' }; // 渲染 "Hello, Stranger": ReactDOM.render( <Greeting />, document.getElementById('example') );
返回多個元素
React 中一個常見模式是為一個組件返回多個元素。Fragments 可以讓你聚合一個子元素列表,並且不在DOM中增加額外節點
Fragments 看起來像空的 JSX 標簽:
render() { return ( <> <ChildA /> <ChildB /> <ChildC /> </> ); }
[註意]<></>
語法不能接受鍵值或屬性
另一種使用片段的方式是使用 React.Fragment
組件,React.Fragment
組件可以在 React 對象上使用,<></>
是 <React.Fragment/>
的語法糖
class Columns extends React.Component { render() { return ( <React.Fragment> <td>Hello</td> <td>World</td> </React.Fragment> ); } }
如果需要一個帶 key 的片段,可以直接使用 <React.Fragment />
。 一個使用場景是映射一個集合為一個片段數組 — 例如:創建一個描述列表:
function Glossary(props) { return ( <dl> {props.items.map(item => ( <React.Fragment key={item.id}> <dt>{item.term}</dt> <dd>{item.description}</dd> </React.Fragment> ))} </dl> ); }
context
在嵌套層級較深的場景中,不想要向下每層都手動地傳遞需要的 props。這就需要強大的 context API了。其中,react-redux中的provider組件就是使用context實現的
【手動傳遞props】
下麵是手動傳遞props的例子
class Button extends React.Component { render() { return ( <button style={{background: this.props.color}}> {this.props.children} </button> ); } } class Message extends React.Component { render() { return ( <div> {this.props.text} <Button color={this.props.color}>Delete</Button> </div> ); } } class MessageList extends React.Component { render() { const color = "purple"; const children = this.props.messages.map((message) => <Message text={message.text} color={color} /> ); return <div>{children}</div>; } }
【使用context】
下麵使用context來自動傳遞
通過在MessageList(context提供者)中添加childContextTypes和getChildContext,React會向下自動傳遞參數,任何組件只要在它的子組件中(這個例子中是Button),就能通過定義contextTypes來獲取參數。
const PropTypes = require('prop-types'); class Button extends React.Component { render() { return ( <button style={{background: this.context.color}}> {this.props.children} </button> ); } } Button.contextTypes = { color: PropTypes.string }; class Message extends React.Component { render() { return ( <div> {this.props.text} <Button>Delete</Button> </div> ); } } class MessageList extends React.Component { getChildContext() { return {color: "purple"}; } render() { const children = this.props.messages.map((message) => <Message text={message.text} /> ); return <div>{children}</div>; } } MessageList.childContextTypes = { color: PropTypes.string };
[註意]如果contextTypes沒有定義,那麼context將會是個空對象
【生命周期】
如果一個組件中定義了contextTypes,那麼下麵這些生命周期函數將會接收到額外的參數,即context對象
constructor(props, context)
componentWillReceiveProps(nextProps, nextContext)
shouldComponentUpdate(nextProps, nextState, nextContext)
componentWillUpdate(nextProps, nextState, nextContext)
componentDidUpdate(prevProps, prevState, prevContext)
【無狀態組件】
如果contextTypes作為函數參數被定義的話,無狀態函數組件也是可以引用context。以下代碼展示了用無狀態函數組件寫法的Button組件
const PropTypes = require('prop-types'); const Button = ({children}, context) => <button style={{background: context.color}}> {children} </button>; Button.contextTypes = {color: PropTypes.string};