前面的話
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> ); }
[注意]如果使用create-react-app構建的專案,不支援<></>,但支援<React.Fragment />的形式
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};
獲取尺寸
如果在react中獲取尺寸,可以使用offset、getBoudingClientRect()等原生JS的尺寸屬性
e.target.offsetHeight