第2章 React的元件
在React中,元件是應用程式的基石,頁面中所有的界面和功能都是由元件堆積而成的。在前端元件化開發之前,一個頁面可能會有成百上千行代碼邏輯寫在一個.js檔案中,這種代碼可讀性很差,如果增加功能,很容易出現一些意想不到的問題。合理的元件設計有利于降低系統各個功能的耦合性,并提高功能内部的聚合性。這對于前端工程化及降低代碼維護成本來說,是非常必要的。
本章主要介紹React中元件的建立、成員、通信和生命周期,最後會通過一個實戰案例——TodoList示範元件的使用。
2.1 元件的聲明方式
簡單來說,在React中建立元件的方式有3種。
- ES 5寫法:React.createClass()(老版本用法,不建議使用);
- ES 6寫法:React.Component;
- 無狀态的函數式寫法,又稱為純元件SFC。
2.1.1 ES 5寫法:React.createClass()
React.createClass()是React剛出現時官方推薦的建立元件方式,它使用ES 5原生的JavaScript來實作React元件。React.createClass()這個方法建構一個元件“類”,它接受一個對象為參數,對象中必須聲明一個render()方法,render()方法将傳回一個元件執行個體。
使用React.createClass()建立元件示例:
var Input = React.createClass({
// 定義傳入props中的各種屬性類型
propTypes: {
initialValue: React.PropTypes.string
},
//元件預設的props對象
defaultProps: {
initialValue: ''
},
// 設定initial state
getInitialState: function() {
return {
text: this.props.initialValue || 'placeholder'
};
},
handleChange: function(event) {
this.setState({
text: event.target.value
});
},
render: function() {
return (
<div>
Type something:
<input onChange={this.handleChange} value={this.state.text} />
</div>
);
}
});
createClass()本質上是一個工廠函數。createClass()聲明的元件方法的定義使用半形逗號隔開,因為creatClass()本質上是一個函數,傳遞給它的是一個Object。通過propTypes對象和getDefaultProps()方法來設定props類型和擷取props。createClass()内的方法會正确綁定this到React類的執行個體上,這也會導緻一定的性能開銷。React早期版本使用該方法,而在新版本中該方法被廢棄,是以不建議讀者使用。
2.1.2 ES 6寫法:React.Component
React.Component是以ES 6的形式來建立元件的,這是React目前極為推薦的建立有狀态元件的方式。相對于React.createClass(),此種方式可以更好地實作代碼複用。本節将2.1.1節介紹的React.createClass()形式改為React.Component形式。
使用React.Component建立元件示例:
class Input extends React.Component {
constructor(props) {
super(props);
// 設定initial state
this.state = {
text: props.initialValue || 'placeholder'
};
// ES 6類中的函數必須手動綁定
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({
text: event.target.value
});
}
render() {
return (
<div>
Type something:
<input onChange={this.handleChange}
value={this.state.text} />
</div>
);
}
}
React.Component建立的元件,函數成員不會自動綁定this,需要開發者手動綁定,否則this無法擷取目前元件的執行個體對象。當然綁定this的方法有多種,除了上面的示例代碼中在constructor()中綁定this外,最常見的還有通過箭頭函數來綁定this,以及在方法中直接使用bind(this)來綁定這兩種。
通過箭頭函數來綁定this示例:
// 使用bind來綁定
<div onClick={this.handleClick.bind(this)}></div>
在方法中直接使用bind(this)來綁定this示例:
// 使用arrow function來綁定
<div onClick={()=>this.handleClick()}></div>
2.1.3 無狀态元件
下面來看看無狀态元件,它是React 0.14之後推出的。如果一個元件不需要管理state,隻是單純地展示,那麼就可以定義成無狀态元件。這種方式聲明的元件可讀性好,能大大減少代碼量。無狀态函數式元件可以搭配箭頭函數來寫,更簡潔,它沒有React的生命周期和内部state。
無狀态函數式元件示例:
const HelloComponent = (props) =>(
?<div>Hello {props.name}</div>?
)
ReactDOM.render(<HelloComponent?name="marlon"?/>, mountNode)
無狀态函數式元件在需要生命周期時,可以搭配高階元件(HOC)來實作。無狀态元件作為高階元件的參數,高階元件記憶體放需要的生命周期和狀态,其他隻負責展示的元件都使用無狀态函數式的元件來寫。
有生命周期的函數式元件示例:
import React from 'react';
export const Table = (ComposedComponent) => {
return class extends React.Component {
constructor(props) {
super(props)
}
componentDidMount() {
console.log('componentDidMount');
}
render() {
return (
<ComposedComponent {...this.props}/>
)
}
}
}
注意:React 16.7.0-alpha(内測)中引入了Hooks,這使得在函數式元件内可以使用state和其他React特性。
2.2 元件的主要成員
在React中,資料流是單向流動的,從父節點向子節點傳遞(自上而下)。子元件可以通過屬性props接收來自父元件的狀态,然後在render()方法中渲染到頁面。每個元件同時又擁有屬于自己内部的狀态state,當父元件中的某個屬性發生變化時,React會将此改變了的狀态向下遞歸周遊元件樹,然後觸發相應的子元件重新渲染(re-render)。
如果把元件視為一個函數,那麼props就是從外部傳入的參數,而state可以視為函數内部的參數,最後函數傳回虛拟DOM。
本節将學習元件中最重要的成員state和props。
2.2.1 狀态(state)
每個React元件都有自己的狀态,相比于props,state隻存在于元件自身内部,用來影響視圖的展示。可以使用React内置的setState()方法修改state,每當使用setState()時,React會将需要更新的state合并後放入狀态隊列,觸發調和過程(Reconciliation),而不是立即更新state,然後根據新的狀态結構重新渲染UI界面,最後React會根據差異對界面進行最小化重新渲染。
React通過this.state通路狀态,調用this.setState()方法來修改狀态。
React通路狀态示例:
(源碼位址為https://jsfiddle.net/allan91/etbj6gsx/1/)
class App extends React.Component {
constructor(props){
super(props);
this.state = {
data: 'World'
}
}
render(){
return(
<div>
Hello, {this.state.data}
</div>
)
}
}
ReactDOM.render(
<App />,
document.querySelector(''#app'') // App元件挂載到ID為app的DOM元素上
)
上述代碼中,App元件在UI界面中展示了自身的狀态state。下面使用setState()修改這個狀态。
React修改狀态示例:
(源碼位址為https://jsfiddle.net/allan91/etbj6gsx/3/)
class App extends React.Component {
constructor(props){
super(props);
this.state = {
data: 'World'
}
}
handleClick = () => {
this.setState({
data:'Redux'
})
}
render(){
return(
<div>
Hello, {this.state.data}
<button onClick={this.handleClick}>更新</button>
</div>
)
}
}
ReactDOM.render(
<App />,
document.querySelector("#app")
)
上述代碼中通過單擊“更新”按鈕使用setState()方法修改了state值,觸發UI界面更新。本例狀态更改前後的展示效果,如圖2.1所示。

2.2.2 屬性(props)
state是元件内部的狀态,那麼元件之間如何“通信”呢?這就是props的職責所在了。通俗來說,props就是連接配接各個元件資訊互通的“橋梁”。React本身是單向資料流,是以在props中資料的流向非常直覺,并且props是不可改變的。props的值隻能從預設屬性和父元件中傳遞過來,如果嘗試修改props,React将會報出類型錯誤的提示。
props示例應用:
(源碼位址為https://jsfiddle.net/n5u2wwjg/35076/)
function Welcome(props) {
return <p>Hello, {props.name}</p>
}
function App(){
return (
<Welcome name='world' /> // 引用Welcome元件,name為該元件的屬性
)
}
ReactDOM.render(
<App />,
document.querySelector("#app")
)
上述代碼使用了函數定義元件。被渲染的App元件内引用了一個外部元件Welcome,并在該元件内定義了一個名為name的屬性,指派為world。Welcome元件接收到來自父元件的name傳遞,在界面中展示Hello,World。
當然,也可以使用class來定義一個元件:
class Welcome extends React.Component{
render() {
return <p>Hello, {this.props.name}</p>;
}
}
這個Welcome元件與上面函數式聲明的元件在React中的效果是一樣的。
2.2.3 render()方法
render()方法用于渲染虛拟DOM,傳回ReactElement類型。
元素是React應用的最小機關,用于描述界面展示的内容。很多初學者會将元素與“元件”混淆。其實,元素隻是元件的構成,一個元素可以構成一個元件,多個元素也可以構成一個元件。render()方法是一個類元件必須擁有的特性,其傳回一個JSX元素,并且外層一定要使用一個單獨的元素将所有内容包裹起來。比如:
render() {
return(
<div>a</div>
<div>b</div>
<div>c</div>
)
}
上面這樣是錯誤的,外層必須有一個單獨的元素去包裹:
render() {
return(
<div>
<div>a</div>
<div>b</div>
<div>c</div>
</div>
)
}
1.render()傳回元素數組
2017年9月,React釋出的React 16版本中為render()方法新增了一個“支援傳回數組元件”的特性。在React 16版本之後無須将清單項包含在一個額外的元素中了,可以在 render()方法中傳回元素數組。需要注意的是,傳回的數組跟其他數組一樣,需要給數組元素添加一個key來避免key warning。
render()方法傳回元素數組示例:
(源碼位址為https://jsfiddle.net/n5u2wwjg/35080/)
render() {
return [
<div key="a">a</div>,
<div key="b">b</div>,
<div key="c">c</div>,
];
}
除了使用數組包裹多個同級子元素外,還有另外一種寫法如下:
import React from 'react';
export default function () {
return (
<>
<div>a</div>
<div>b</div>
<div>c</div>
</>
);
}
簡寫的<>>其實是React 16中React.Fragment的簡寫形式,不過它對于部分前端工具的支援還不太好,建議使用完整寫法,具體如下:
import React from 'react';
export default function () {
return (
<React.Fragment>
<div>a</div>
<div>b</div>
<div>c</div>
</React.Fragment>
);
}
最後輸出到頁面的标簽也能達到不可見的效果,也就是在同級元素外層實際上是沒有包裹其他元素的,這樣能減少DOM元素的嵌套。
2.render()傳回字元串
當然,render()方法也可以傳回字元串。
render()方法傳回字元串示例:
(源碼位址為https://jsfiddle.net/n5u2wwjg/35079/)
render() {
return 'Hello World';
}
運作程式,界面中将展示以上這段字元串。
3.render()方法中的變量與運算符&&
render()方法中可以使用變量有條件地渲染要展示的頁面。常見做法是通過花括号{}包裹代碼,在JSX中嵌入任何表達式,比如邏輯與&&。
render()方法中使用運算符示例:
const fruits = ['apple', 'orange', 'banana'];
function Basket(props) {
const fruitsList = props.fruits;
return (
<div>
<p>I have: </p>
{fruitsList.length > 0 &&
<span>{fruitsList.join(', ')}</span>
}
</div>
)
}
ReactDOM.render(<Basket fruits={fruits}/>, document.querySelector("#app"))
上述代碼表示,如果從外部傳入Basket元件的數組不為空,也就是表達式左側為真, &&右側的元素就會被渲染。展示效果如圖2.2所示。如果表達式左側為false,&&右側元素就會被React忽略渲染。
4.render()方法中的三目運算符
在render()方法中還能使用三目運算符condition ? true : false。
在render()方法中使用三目運算符示例:
(源碼位址為https://jsfiddle.net/n5u2wwjg/35239/)
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isUserLogin: false
}
}
render() {
const { isUserLogin } = this.state;
return (
<div>
{ isUserLogin ? <p>已登入</p> : <p>未登入</p> }
</div>
)
}
}
ReactDOM.render(<App/>, document.querySelector("#app"))
上述代碼根據isUserLogin的真和假來動态顯示p标簽的内容,當然也可以動态展示封裝好的元件,例如:
return (
<div>
{ isUserLogin ? <ComponentA /> : <ComponentB /> }
</div>
)
2.3 元件之間的通信
React編寫的應用是以元件的形式堆積而成的,元件之間雖互相獨立,但互相之間還是可以通信的。本節将介紹元件中的幾種通信方式。
2.3.1 父元件向子元件通信
前面章節已經提到過,React的資料是單向流動的,隻能從父級向子級流動,父級通過props屬性向子級傳遞資訊。
父元件向子元件通信示例:
(源碼位址為https://jsfiddle.net/n5u2wwjg/35403/)
class Child extends React.Component {
render (){
return (
<div>
<h1>{ this.props.fatherToChild }</h1>
</div>
)
}
}
class App extends React.Component {
render() {
let data = 'This message is from Dad!'
return (
<Child fatherToChild={ data } />
)
}
}
ReactDOM.render(
<App/>,
document.querySelector("#app")
)
上述代碼中有兩個元件:子元件Child和父元件App。子元件在父元件中被引用,然後在父元件内給子元件定了一個props:fatherToChild,并将父元件的data傳遞給子元件中展示。
注意:父元件可以通過props向子元件傳遞任何類型。
2.3.2 子元件向父元件通信
雖然React資料流是單向的,但并不影響子元件向父元件通信。通過父元件可以向子元件傳遞函數這一特性,利用回調函數來實作子元件向父元件通信。當然也可以通過自定義事件機制來實作,但這種場景會顯得過于複雜。是以為了簡單友善,還是利用回調函數來實作。
子元件向父元件通信示例:
class Child extends React.Component {
render (){
return <input type="text" onChange={(e)=>this.props.handleChange
(e.target.value)} />
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: ''
}
}
handleChange = text => {
this.setState({
data: text
})
}
render() {
return (
<div>
<p>This message is from Child:{this.state.data}</p>
<Child handleChange={ this.handleChange } />
</div>
)
}
}
ReactDOM.render(
<App/>,
document.querySelector("#app")
)
上述代碼中有兩個元件:子元件Child和父元件App。子元件被父元件引用,在父元件中定義了一個handleChange事件,并通過props傳給子元件讓子元件調用該方法。子元件接收到來自父元件的handleChange方法,當子元件input框内輸入的值Value發生變化時,就會觸發handleChange方法,将該值傳遞給父元件,進而達到子對父通信。
注意:一般情況下,回調函數會與setState()成對出現。
2.3.3 跨級元件通信
當元件層層嵌套時,要實作跨元件通信,首先會想到利用props一層層去傳遞資訊。雖然可以實作資訊傳遞,但這種寫法會顯得有點“啰嗦”,也不優雅。這種場景在React中,一般使用context來實作跨級父子元件通信。
context的設計目的就是為了共享對于一個元件樹而言是“全局性”的資料,可以盡量減少逐層傳遞,但并不建議使用context。因為當結構複雜的時候,這種全局變量不易追溯到源頭,不知道它是從哪裡傳遞過來的,會導緻應用變得混亂,不易維護。
context适用的場景最好是全局性的資訊,且不可變的,比如使用者資訊、界面顔色和主題制定等。
context實作的跨級元件通信示例(React 16.2.0):
(源碼位址為https://jsfiddle.net/allan91/Lbecjy18/2/)
// 子(孫)元件
class Button extends React.Component {
render() {
return (
<button style={{background: this.context.color}}>
{this.props.children}
</button>
);
}
}
// 聲明contextTypes用于通路MessageList中定義的context資料
Button.contextTypes = {
color: PropTypes.string
};
// 中間元件
class Message extends React.Component {
render() {
return (
<div>
<Button>Delete</Button>
</div>
);
}
}
// 父元件
class MessageList extends React.Component {
// 定義context需要實作的方法
getChildContext() {
return {
color: "orange"
};
}
render() {
return <Message />;
}
}
// 聲明context類型
MessageList.childContextTypes = {
color: PropTypes.string
};
ReactDOM.render(
<MessageList />,
document.getElementById('container')
);
上述代碼中,MessageList為context的提供者,通過在MessageList中添加childContextTypes和getChildContext()和MessageList。React會向下自動傳遞參數,任何組織隻要在它的子元件中(這個例子中是Button),就能通過定義contextTypes來擷取參數。如果contextTypes沒有定義,那麼context将會是個空對象。
context中有兩個需要了解的概念:一個是context的生産者(provider);另一個是context的消費者(consumer),通常消費者是一個或多個子節點。是以context的設計模式是屬于生産-消費者模式。在上述示例代碼中,生産者是父元件MessageList,消費者是孫元件Button。
在React中,context被歸為進階部分(Advanced),屬于React的進階API,是以官方不推薦在不穩定的版本中使用。值得注意的是,很多優秀的React第三方庫都是基于context來完成它們的功能的,比如路由元件react-route通過context來管理路由,react-redux的通過context提供全局Store,拖曳元件react-dnd通過context分發DOM的Drag和Drop事件等。
注意:不要僅僅為了避免在幾個層級下的元件傳遞props而使用context,context可用于多個層級的多個元件需要通路相同資料的情景中。
2.3.4 非嵌套元件通信
非嵌套元件就是沒有包含關系的元件。這類元件的通信可以考慮通過事件的釋出-訂閱模式或者采用context來實作。
如果采用context,就是利用元件的共同父元件的context對象進行通信。利用父級實作中轉傳遞在這裡不是一個好的方案,會增加子元件和父元件之間的耦合度,如果元件層次嵌套較深的話,不易找到父元件。
那麼釋出-訂閱模式是什麼呢?釋出-訂閱模式又叫觀察者模式。其實很簡單,舉個現實生活中的例子:
很多人手機上都有微信公衆号,讀者所關注的公衆号會不定期推送資訊。
這就是一個典型的釋出-訂閱模式。在這裡,公衆号就是釋出者,而關注了公衆号的微信使用者就是訂閱者。關注公衆号後,一旦有新文章或廣告釋出,就會推送給訂閱者。這是一種一對多的關系,多個觀察者(關注公衆号的微信使用者)同時關注、監聽一個主體對象(某個公衆号),當主體對象發生變化時,所有依賴于它的對象都将被通知。
釋出-訂閱模式有以下優點:
- 耦合度低:釋出者與訂閱者互不幹擾,它們能夠互相獨立地運作。這樣就不用擔心開發過程中這兩部分的直接關系。
- 易擴充:釋出-訂閱模式可以讓系統在無論什麼時候都可進行擴充。
- 易測試:能輕易地找出釋出者或訂閱者是否會得到錯誤的資訊。
- 靈活性:隻要共同遵守一份協定,不需要擔心不同的元件是如何組合在一起的。
React在非嵌套元件中隻需要某一個元件負責釋出,其他元件負責監聽,就能進行資料通信了。下面通過代碼來示範這種實作。
非嵌套元件通信示例:
(1)安裝一個現成的events包:
npm install events —save
(2)建立一個公共檔案events.js,引入events包,并向外提供一個事件對象,供通信時各個元件使用:
import { EventEmitter } from "events";
export default new EventEmitter();
(3)元件App.js:
import React, { Component } from 'react';
import ComponentA from "./ComponentA";
import ComponentB from "./ComponentA";
import "./App.css";
export default class App extends Component{
render(){
return(
<div>
<ComponentA />
<ComponentB />
</div>
);
}
}
(4)元件ComponentA:
import React,{ Component } from "react";
import emitter from "./events";
export default class ComponentA extends Component{
constructor(props) {
super(props);
this.state = {
data: React,
};
}
componentDidMount(){
// 元件加載完成以後聲明一個自定義事件
// 綁定callMe事件,處理函數為addListener()的第2個參數
this.eventEmitter = emitter.addListener("callMe",(data)=>{
this.setState({
data
})
});
}
componentWillUnmount(){
// 元件銷毀前移除事件監聽
emitter.removeListener(this.eventEmitter);
}
render(){
return(
<div>
Hello,{ this.state.data }
</div>
);
}
}
(5)元件ComponentB:
import React,{ Component } from "react";
import emitter from "./events";
export default class ComponentB extends Component{
render(){
const cb = (data) => {
return () => {
// 觸發自定義事件
// 可傳多個參數
emitter.emit("callMe", "World")
}
}
return(
<div>
<button onClick = { cb("Hey") }>點選</button>
</div>
);
}
}
當在非嵌套元件B内單擊按鈕後,會觸發emitter.emit(),并且将字元串參數World傳給callMe。元件A展示的内容由Hello,React變為Hello,World。這就是一個典型的非嵌套元件的通信。
注意:元件之間的通信要保持簡單、幹淨,如果遇到了非嵌套元件通信,這時候讀者需要仔細審查代碼設計是否合理。要盡量避免使用跨元件通信和非嵌套元件通信等這類情況。
2.4 元件的生命周期
生命周期(Life Cycle)的概念應用很廣泛,特别是在政治、經濟、環境、技術、社會等諸多領域經常出現,其基本涵義可以通俗地了解為“從搖籃到墳墓”(Cradle-to- Grave)的整個過程。在React元件的整個生命周期中,props和state的變化伴随着對應的DOM展示。每個元件提供了生命周期鈎子函數去響應元件在不同時刻應該做和可以做的事情:建立時、存在時、銷毀時。
本節将從React元件的“誕生”到“消亡”來介紹React的生命周期。由于React 16版本中對生命周期有所修改,是以本節隻介紹最新版本的内容,React 15版本的生命周期不推薦使用,如需了解請讀者自行查閱。這裡以React 16.4以上版本為例講解。
2.4.1 元件的挂載
React将元件渲染→構造DOM元素→展示到頁面的過程稱為元件的挂載。一個元件的挂載會經曆下面幾個過程:
- constructor();
- static getDerivedStateFromProps();
- render();
- componentDidMount()。
元件的挂載示例:
(源碼位址為https://jsfiddle.net/allan91/n5u2wwjg/225709/)
class App extends React.Component {
constructor(props) {
super(props);
console.log("constructor")
}
static getDerivedStateFromProps(){
console.log("getDerivedStateFromProps")
return null;
}
// React 17中将會移除componentWillMount()
// componentWillMount() {
// console.log("componentWillMount")
//}
render() {
console.log("render")
return 'Test'
}
// render()之後構造DOM元素插入頁面
componentDidMount() {
console.log("componentDidMount")
}
}
ReactDOM.render(
<App/>,
document.querySelector("#app")
)
打開控制台,上述代碼執行後将依次列印:
constructor
getDerivedStateFromProps
render
componentDidMount
constructor()是ES 6中類的預設方法,通過new指令生成對象執行個體時自動調用該方法。其中的super()是class方法中的繼承,它是使用extends關鍵字來實作的。子類必須在constructor()中調用super()方法,否則建立執行個體會報錯。如果沒有用到constructor(),React會預設添加一個空的constructor()。
getDerivedStateFromProps()在元件裝載時,以及每當props更改時被觸發,用于在props(屬性)更改時更新元件的狀态,傳回的對象将會與目前的狀态合并。
componentDidMount()在元件挂載完成以後,也就是DOM元素已經插入頁面後調用。而且這個生命周期在元件挂載過程中隻會執行一次,通常會将頁面初始資料的請求在此生命周期内執行。
注意:其中被注釋的componentWillMount()是React舊版本中的生命周期,官方不建議使用這個方法,以後會被移除,是以這裡不做介紹。
2.4.2 資料的更新過程
元件在挂載到DOM樹之後,當界面進行互動動作時,元件props或state改變就會觸發元件的更新。假如父元件render()被調用,無論此時props是否有改變,在render()中被渲染的子元件就會經曆更新過程。一個元件的資料更新會經曆下面幾個過程:
- shouldComponentUpdate();
- componentWillUpdate()/UNSAFE_componentWillUpdate();
- getSnapshotBeforeUpdate();
- componentDidUpdate()。
資料更新可以分為下面兩種情況讨論:
1.元件自身state更新
元件自身state更新會依次執行:
shouldComponentUpdate()—> render()—> getSnapBeforeUpdate()—> componentDidUpdate()
2.父元件props更新
父元件props更新會依次執行:
static getDerivedStateFromProps() —> shouldComponentUpdate()—> render()—> getSnapBeforeUpdate()—> componentDidUpdate()
相對于自身state更新,這裡多了一個getDerivedStateFromProps()方法,它的位置是元件在接收父元件props傳入後和渲染前setState()的時期,當挂載的元件接收到新的props時被調用。此方法會比較this.props和nextProps并使用this.setState()執行狀态轉換。
上面兩種更新的順序情況基本相同,下面來看看它們分别有何作用和差別:
- shouldComponentUpdate(nextProps, nextState):用于判斷元件是否需要更新。它會接收更新的props和state,開發者可以在這裡增加判斷條件。手動執行是否需要去更新,也是React性能優化的一種手法。預設情況下,該方法傳回true。當傳回值為false時,則不再向下執行其他生命周期方法。
- componentDidUpdate(object nextProps, object nextState):很容易了解,從字面意思就知道它們分别代表元件render()渲染後的那個時刻。componentDidUpdate()方法提供了渲染後的props和state。
注意:無狀态函數式元件沒有生命周期,除了React 16.7.0的新特性Hooks。
2.4.3 元件的解除安裝(unmounting)
React提供了一個方法:componentWillUnmount()。當元件将要被解除安裝之前調用,可以在該方法内執行任何可能需要清理的工作。比如清除計時器、事件回收、取消網絡請求,或清理在componentDidMount()中建立的任何監聽事件等。
元件的解除安裝示例:
import React, { Component } from "react";
export default class Hello extends Component {
componentDidMount() {
this.timer = setTimeout(() => {
console.log("挂在this上的定時器");
}, 500);
}
componentWillUnmount() {
this.timer && clearTimeout(this.timer);
}
}
2.4.4 錯誤處理
在渲染期間,生命周期方法或構造函數constructor()中發生錯誤時将會調用componentDidCatch()方法。
React錯誤處理示例:
import React from "react";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
this.setState({
hasError: true
});
}
render() {
if (this.state.hasError) {
return <h1>這裡可以自定義一些展示,這裡的内容能正常渲染。</h1>;
}
return this.props.children;
}
}
在componentDidCatch()内部把hasError狀态設定為true,然後在渲染方法中檢查這個狀态,如果出錯狀态是true,就渲染備用界面;如果狀态是false,就正常渲染應該渲染的界面。
錯誤邊界不會捕獲下面的錯誤:
- 錯誤邊界本身錯誤,而非子元件抛出的錯誤。
- 服務端渲染(Server side rendering)。
- 事件處理(Event handlers),因為事件處理不發生在React渲染時,報錯不影響渲染)。
- 異步代碼。
2.4.5 老版React中的生命周期
老版本的React中還有如下生命周期:
- componentWillMount();
- componentWillReceiveProps();
- componentWillUpdate()。
老版本中的部分生命周期方法有多種方式可以完成一個任務,但很難弄清楚哪個才是最佳選項。有的錯誤處理行為會導緻記憶體洩漏,還可能影響未來的異步渲染模式等。鑒于此,React決定在未來廢棄這些方法。
React官方考慮到這些改動會影響之前一直在使用生命周期方法的元件,是以将盡量平緩過渡這些改動。在React 16.3版本中,為不安全生命周期引入别名:
- UNSAFE_componentWillMount;
- UNSAFE_componentWillReceiveProps;
- UNSAFE_componentWillUpdate。
舊的生命周期名稱和新的别名都可以在React16.3版本中使用。将要廢棄舊版本的生命周期會保留至React 17版本中删除。
同時,React官方也提供了兩個新的生命周期:
- getDerivedStateFromProps();
- getSnapshotBeforeUpdate()。
getDerivedStateFromProps()生命周期在元件執行個體化及接收新props後調用,會傳回一個對象去更新state,或傳回null不去更新,用于确認目前元件是否需要重新渲染。這個生命周期将可以作為componentWillReceiveProps()的安全替代者。
getDerivedStateFromProps()生命周期示例:
class App extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
...
}
}
getSnapshotBeforeUpdate()生命周期方法将在更新之前被調用,比如DOM被更新之前。這個生命周期的傳回值将作為第3個參數傳遞給componentDidUpdate()方法,雖然這個方法不經常使用,但是對于一些場景(比如儲存滾動位置)非常有用。配合componentDidUpdate()方法使用,新的生命周期将覆寫舊版componentWillUpdate()的所有用例。
getSnapshotBeforeUpdate()生命周期(官方示例):
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 是否添加新項目到清單
// 捕獲滾動定位用于之後調整滾動位置
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果有新值,就添加進新項目
// 調整滾動位置,新項目不會把老項目推到可視視窗外
// (這裡的snapshot來自于getSnapshotBeforeUpdate()這個生命周期的傳回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/- ...contents... */}</div>
);
}
}
2.4.6 生命周期整體流程總結
React元件的整個生命周期流程圖如圖2.3所示來描述。
2.5 元件化實戰訓練——TodoList
前面章節中學習了如何配置Webpack來搭建Hello World項目,以及React的元件、元件通信和生命周期等。接下來繼續基于前面的這個項目來實作一個簡單的TodoList,以此加深讀者對元件化的了解。
在這個簡單的TodoList項目中,需要實作:
- 通過input輸入框輸入todo内容;
- 單擊Submit按鈕将輸入的内容展示在頁面上。
在1.5節腳手架中,Webpack的loader隻對JS和JSX做了識别,現在需要在項目中加入CSS的相關loader,目的是讓Webpack識别和加載樣式檔案。
(1)安裝CSS的相關loader:
npm install css-loader style-loader --save-dev
(2)配置Webpack中的loader:
var webpack = require("webpack");
var path = require("path");
const CleanWebpackPlugin = require("clean-webpack-plugin");
var BUILD_DIR = path.resolve(__dirname, "dist");
var APP_DIR = path.resolve(__dirname, "src");
const HtmlWebpackPlugin = require("html-webpack-plugin");
var config = {
entry: APP_DIR + "/index.jsx",
output: {
path: BUILD_DIR,
filename: "bundle.js"
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
},
{
test: /\.css$/, // 隻加載.css檔案
loader: 'style-loader!css-loader' // 同時運作兩個loader
}
]
},
devServer: {
port: 3000,
contentBase: "./dist"
},
plugins: [
new HtmlWebpackPlugin({
template: "index.html",
// favicon: 'theme/img/favicon.ico',
inject: true,
sourceMap: true,
chunksSortMode: "dependency"
}),
new CleanWebpackPlugin(["dist"])
]
};
module.exports = config;
至此,TodoList的項目腳手架配置結束。
(3)接下來是相應元件的代碼,入口頁面App.jsx負責渲染元件頭部Header和清單ListItems,并在目前元件内部state維護清單的項目和輸入的内容。
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
todoItem: "",
items: []
};
}
render() {
return (
<div>
</div>
);
}
}
從上述代碼可以看到App元件的state内有todoItem和items。todoItem用于存儲輸入框輸入的值;items用于存儲輸入框内送出的值,之後用于清單的渲染。
(4)再來編寫輸入框輸入内容時的onChange事件:
onChange(event) {
this.setState({
todoItem: event.target.value
});
}
<input value={this.state.todoItem} onChange={this.onChange} />
從上述代碼中可以看到,input的值來自于App元件内的state。使用者每次輸入後,onChange事件監聽其變化,然後調用this.setState()将改變的值實時寫入input中展示。
(5)表單送出:
onSubmit(event) {
event.preventDefault();
this.setState({
todoItem: "",
items: [
...this.state.items,
this.state.todoItem
]
});
}
<form className="form-wrap" onSubmit={this.onSubmit}>
<input value={this.state.todoItem} onChange={this.onChange} />
<button>Submit</button>
</form>
當單擊Submit按鈕時,輸入框的值将通過表單送出的方式觸發onSubmit事件,然後調用this.setState()添加輸入框中的值到items數組,同時清空輸入框。
(6)将内容整理為3部分:頭Header、表單form和清單ListItems。其中,Header和ListItems各為一個元件。
./src/Header.js内容如下:
import React from 'react';
const Header = props => (
<h1>{props.title}</h1>
);
export default Header;
./src/ListItems.js内容如下:
import React from 'react';
const ListItems = props => (
<ul>
{
props.items.map(
(item, index) => <li key={index}>{item}</li>
)
}
</ul>
);
export default ListItems;
Header和ListItems都是無狀态函數式元件,接收父級./src/app.jsx傳入的props資料,用于各自的展示。
(7)在入口./src/app.jsx 中引入元件:
import React, { Component } from "react";
import { render } from "react-dom";
+ import ListItems from "./ListItems";
+ import Header from "./Header";
(8)引入樣式:
import React, { Component } from "react";
import { render } from "react-dom";
import ListItems from "./ListItems";
import Header from "./Header";
+ import "./index.css";
至此,所有内容完成,此時這個項目的結構如下:
.
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── src
│ ├── Header.js
│ ├── ListItems.js
│ ├── app.jsx
│ └── index.css
└── webpack.config.js
最終入口app.jsx檔案的代碼如下:
/src/app.jsx内容如下:
import React, { Component } from "react";
import { render } from "react-dom";
import PropTypes from 'prop-types'; // 定義元件屬性類型校驗
import "./index.css";
import ListItems from "./ListItems";
import Header from "./Header";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
todoItem: "",
items: ["吃蘋果","吃香蕉","喝奶茶"]
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
// 輸入框onChange事件
onChange(event) {
this.setState({
todoItem: event.target.value
});
}
// 表單送出按鈕單擊事件
onSubmit(event) {
event.preventDefault();
this.setState({
todoItem: "",
items: [
...this.state.items,
this.state.todoItem
]
});
}
render() {
return (
<div className="container">
<Header title="TodoList"/>
<form className="form-wrap" onSubmit={this.onSubmit}>
<input value={this.state.todoItem} onChange={this.onChange} />
<button>Submit</button>
</form>
<ListItems items={this.state.items} />
</div>
);
}
}
App.propTypes = {
items: PropTypes.array,
todoItem: PropTypes.string,
onChange: PropTypes.func,
onSubmit: PropTypes.func
};
render(
<App />,
document.getElementById("app")
);
本例最終的展示效果如圖2.4所示。
項目源碼可在GitHub進行下載下傳 ,位址是
https://github.com/khno/react-comonent-todolist。