無論是 vue、React 還是 Angular,主流框架都支持并提倡組件化開發(fā),因?yàn)榻M件化開發(fā)不僅可以增強(qiáng)代碼的能動(dòng)性和復(fù)用性,還能夠加快團(tuán)隊(duì)協(xié)作的速度。組件化開發(fā)就像搭積木,首先把一個(gè)個(gè)積木(組件)設(shè)計(jì)好,甚至將小積木(容器組件、展示組件)組裝成具備一定功能的積木(比如一個(gè)房子),最終再將功能化的積木摞成最終的成品(比如一個(gè)社區(qū))。
本文簡(jiǎn)單介紹 React 中組件的定義,以及容器組件、展示組件、高階組件、復(fù)合組件等常見組件的應(yīng)用,并介紹組件間的通信方式。
1. 如何定義一個(gè)組件
1.1 一般組件
React 中組件的定義有兩種方式,一種是使用 Class 關(guān)鍵字以類的形式來定義組件,另一種是使用函數(shù)方式定義。比如定義一個(gè)網(wǎng)站的歡迎提示組件:
- 類定義
class WelcomeTip extends React.Component {
render() {
return (
<div>
Welcome to this website!
</div>
)
}
}
- 函數(shù)定義
function WelcomeTip(props) {
return (
<div>
Welcome to this website!
</div>
)
}
無論使用哪一種方式定義組件,組件的調(diào)用都是一致的
<WelcomeTip></WelcomeTip>
但是,組件內(nèi)狀態(tài)管理、生命周期卻有著很大的不同,本文中主要采用類定義的方式來構(gòu)建組件,關(guān)于函數(shù)定義組件的應(yīng)用可以移步 “React Hook” 的介紹。
- 組件狀態(tài)
class Counter extends React.Component {
// 寫了 constructor 就要調(diào)用 super
constructor(props) {
super(props)
// 狀態(tài)聲明
this.state = {
count: 0
}
}
// state 的調(diào)用:this.state.xxx
// state 的修改:this.setState({count: 1})
// 或者 this.setState(state => ({count: 1}))
// 支持同時(shí)設(shè)置多個(gè) key 值,key 值相同時(shí)后者覆蓋前者
// setState 是一個(gè)異步函數(shù)
render() {
return (
<div>
<p>Welcome, {this.props.name}! You have click {this.state.count} times!</p>
<button
onClick={() => this.setState(state => {count: state.count + 1})}
>Click</button>
</div>
)
}
}
- 組件的生命周期
- 初始化:
constructor,用于完成組件的初始化工作,如定義state的初始內(nèi)容、定義組件內(nèi)部變量等 - 組件的掛載:
-
componentWillMount,發(fā)生在組件掛載到 DOM 之前,此處修改state不會(huì)引起組件的重新渲染。該部分的功能也可以提前到constructor中,因此很少在項(xiàng)目中使用。 -
render,根據(jù)組件的props和state(兩者的重傳遞和重賦值,無論值是否有變化,都可以引起組件重新 render),返回?個(gè) React 元素(描述組件,即UI),不負(fù)責(zé)組件實(shí)際渲染?作,之后由 React ?身根據(jù)此元素去渲染出??DOM。純函數(shù),返回結(jié)果只依賴于傳入的參數(shù),執(zhí)行過程中沒有副作用。不能在該階段執(zhí)行setState,會(huì)造成死循環(huán)。 -
componentDidMount,組件掛載到 DOM 之后調(diào)用,且只會(huì)被調(diào)用一次。
-
- 組件的更新:當(dāng)
props或state被重新賦值時(shí),無論值是否發(fā)生改變,都會(huì)觸發(fā)組件的更新。因此有如下兩種情況會(huì)觸發(fā)組件的更新:1. 父組件重新 render,由于子組件的props被傳值,觸發(fā)子組件的更新;2. 組件本身調(diào)用setState,無論state有沒有改變,組件都會(huì)更新-
componentWillReceiveProps(nextProps),props重傳時(shí)被調(diào)用,該函數(shù)中調(diào)用setState不會(huì)引起組件的二次更新,因此即便在該函數(shù)中執(zhí)行this.setState更新了state,shouldComponentUpdatecomponentWillUpdate中的this.state依舊是原來的值。 -
shouldComponentUpdate(nextProps, nextState),此?法通過?較nextProps,nextState及當(dāng)前組件的this.props,this.state,返回true時(shí)當(dāng)前組件將繼續(xù)執(zhí)?更新過程,返回false則當(dāng)前組件更新停?,以此可?來減少組件的不必要渲染,優(yōu)化組件性能。 -
componentWillUpdate(nextProps, nextState),此?法在調(diào)?render?法前執(zhí)?,在這邊可執(zhí)??些組件更新發(fā)?前的?作,?般較少?。 -
render:同掛載時(shí)的 render。 -
componentDidUpdate(prevProps, prevState),此?法在組件更新后被調(diào)?,可以操作組件更新的 DOM ,prevProps和prevState這兩個(gè)參數(shù)指的是組件更新前的props和state。
-
- 組件的卸載:
-
componentWillUnmount:此?法在組件被卸載前調(diào)?,可以在這?執(zhí)??些清理?作,?如清除組件中使?的定時(shí)器,componentDidMount中?動(dòng)創(chuàng)建的 DOM 元素等,以避免引起內(nèi)存泄漏。
-
- 【注意】
componentWillMountcomponentWillReceiveProps和componentWillUpdate在 React 17.x 版本之后將不再支持,目前使用會(huì)提示 warning。在 16.3 之后,使用getDerivedStateFromProps代替上述三個(gè)函數(shù)-
static getDerivedStateFromProps(props, state),在組件創(chuàng)建時(shí)和更新時(shí)的 render ?法之前調(diào)?, 它應(yīng)該返回?個(gè)對(duì)象來更新狀態(tài),或者返回 null 來不更新任何內(nèi)容。 -
getSnapshotBeforeUpdate,被調(diào)?于render之后,此時(shí)可以讀取但還不能操作更新 DOM ,因此可以按需調(diào)整滾動(dòng)條等。 返回值(必須有)將作為參數(shù)傳遞給componentDidUpdate。
引自https://github.com/aermin/blog/issues/55
-
- 初始化:
1.2 組件拆分——容器組件&展示組件
在涉及復(fù)雜的數(shù)據(jù)預(yù)處理時(shí),可以考慮將組件拆分成容器組件和展示組件。其中容器組件負(fù)責(zé)請(qǐng)求并處理數(shù)據(jù),展示組件負(fù)責(zé)根據(jù) Props 顯示信息。如此可以減小組件的體積,使開發(fā)人員可以跟專注于某一功能開發(fā),并提高組件的重用性和可用性,同時(shí)易于測(cè)試和提高系統(tǒng)性能。
// 容器組件
class CommentList extends React.Component {
state = {
list: []
}
componentDidMount() {
setTimeout(() => {
this.setState({
list: [
{id: 1, text: '我喜歡蘋果', author: '小A'},
{id: 2, text: '我喜歡橙子', author: '小B'},
{id: 3, text: '我喜歡西瓜', author: '小C'},
]
})
})
}
render() {
return (
<div>
{this.state.list.map(l => {
return <Item key={l.id} text={l.text} author={l.author}/>
})}
</div>
)
}
}
// 展示組件
function Item({text, author}) {
return (<div>
{text} -- <span style={{color: 'blue'}}>{author}</span>
</div>)
}
1.3 PureComponent
在組件生命周期中組件更新過程中,提及只要發(fā)生重新掛載,無論 props state 是否變化,都會(huì)出發(fā)更新。純組件就是定制了 shouldComponentUpdate 后的Component,僅有依賴的數(shù)據(jù)發(fā)生變化時(shí)才進(jìn)行更新。 該比較過程數(shù)據(jù)淺比較,因此對(duì)象屬性或數(shù)組中元素并不適用于該特性。
// 假設(shè)父組件有 count 和 name 兩個(gè)狀態(tài)
// 子組件僅依賴父組件的 count
// 如果子組件繼承的是 React.Component,那么父組件 name 值發(fā)生變更時(shí),子組件依舊會(huì)重新 render
// 繼承的是 React.PureComponent 時(shí),則僅有父組件的 count 值變化時(shí),子組件才會(huì)重新調(diào)用 render
class Child extends React.PureComponent {
render() {
return <div>{this.props.count}</div>
}
}
React 16.6.0 之后,使用 React.memo 讓函數(shù)式的組件也有 PureComponent 的功能
const Child = React.memo(() => {
return <div>{this.props.count}</div>
})
2. 高階組件是什么
2.1 高階組件與一般組件有什么不同
高階組件是 React 中重用組件邏輯的高級(jí)技術(shù),它不是 React 的 api ,而是一種組件增強(qiáng)模式。高階組件是一個(gè)函數(shù),它返回另外一個(gè)組件,產(chǎn)生新的組件可以對(duì)被包裝組件屬性進(jìn)行包裝,也可以重寫部分生命周期。
高階組件可以為組件添加某一特殊功能,也可以多層嵌套,賦予被包裝組件多個(gè)功能。比如打印日志功能、添加標(biāo)題功能等。
// 包裝后的組件具備日志打印功能
const withLog = Component => {
class newComponent extends React.Component {
componentDidMount() {
console.log(`${Date.now()}:組件已掛載`)
}
render() {
return <Component {...this.props} />
}
}
return newComponent
}
// 包裝后的組件都帶有一個(gè)標(biāo)題
const withTitle = Component => {
const newComponent = props => {
return (<Fragment>
<h3>這是一個(gè)標(biāo)題</h3>
<hr />
<Component {...props} />
</Fragment>)
}
return newComponent
}
2.2 高階組件怎么使用
- 鏈?zhǔn)秸{(diào)用
高階組件本質(zhì)上就是一個(gè)函數(shù),因此可以采用鏈?zhǔn)秸{(diào)用的形式,將待包裝的組件作為參數(shù)傳入,并 export 出去即可。同時(shí)也可以多個(gè)高階組件嵌套,一層層包裝單一組件。
export default withLog(withTitle(CommentList))
- 裝飾者模式
ES7 中提供了裝飾者模式的寫法,可以使代碼更加簡(jiǎn)潔,但需要進(jìn)行相關(guān)配置:
暴露項(xiàng)目的所有配置項(xiàng):
npm run eject安裝:
npm install -D @babel/plugin-proposal-decorators-
配置
package.json文件中 babel 配置項(xiàng)"babel": { "presets": [ "react-app" ], "plugins": [ ["@babel/plugin-proposal-decorators", {"legacy": true}] ] }
如此,上述鏈?zhǔn)秸{(diào)用可以修改為:
export default
@withLog
@withTitle
class CommentList extends React.Component {
...
}
3. 復(fù)合組件
復(fù)合組件可以讓開發(fā)者以更便捷地創(chuàng)建組件的外觀和行為,相比繼承更加直觀和安全。
// 容器不關(guān)心內(nèi)容與邏輯
// 3. 容器中可以使用 children,但由于傳入的是 vdom 數(shù)組,故而不能修改
function Dialog(props) {
return (<div style={{border: `1px solid ${props.color || '#ccc'}`}}>
{React.Children.map(props.children, child => child.type === 'p' ? child : null)}
{props.footer}
</div>)
}
// 通過復(fù)合提供內(nèi)容
function HelloDialog(props) {
// 1. 參數(shù)可以使用 props 傳入
// 2. 可以傳入任何表達(dá)式
return (<Dialog color='blue' footer={<p>版權(quán)歸 road 所有</p>}>
<h3>你好啊,{props.name}</h3>
<p>感謝訪問本網(wǎng)站</p>
</Dialog>)
}
4. 組件間如何實(shí)現(xiàn)通信
4.1 父?jìng)髯?/h5>
通過 props 將參數(shù)傳遞給子組件,使用 class 關(guān)鍵字以類方式定義組件時(shí),使用 this.props 即可以父組件傳遞的所有參數(shù),函數(shù)方式定義時(shí)則需要在聲明時(shí)添加 props 參數(shù),或解構(gòu)參數(shù)。
// 類方式定義
class Child extends React.Component {
render() {
return (<div>
子組件:{this.props.name}
</div>)
}
}
// 函數(shù)方式定義
function Child(props) {
return (<div>
子組件:{props.name}
</div>)
}
// 函數(shù)方式
function Child({name}) {
return (<div>
子組件:{name}
</div>)
}
父組件傳參:
<Child name='road'></Child>
4.2 子傳父
父組件中聲明一個(gè)相關(guān)方法,并作為參數(shù)傳遞給子組件。子組件通過調(diào)用父組件傳遞過來的方法,修改父組件中的數(shù)據(jù)。
// 比如:父組件中有個(gè)計(jì)數(shù)值,子組件中的按鈕點(diǎn)擊之后計(jì)數(shù)值 +1
function Child({increase, step}) {
return (
<div>
<button onClick={() => increase(step)}>+{step}</button>
</div>
);
}
export class Parent extends Component {
state = {
count: 0
}
add(step) {
this.setState(state => ({count: state.count + step}))
}
render() {
return (
<div>
計(jì)數(shù)值為 {this.state.count}
{/* 注意方法傳遞過程中 this 的指向變更 */}
<Child increase={this.add.bind(this)} step={1}></Child>
<Child increase={this.add.bind(this)} step={2}></Child>
</div>
);
}
}
4.3 跨組件通信
跨組件通信有兄弟組件通信、父組件與孫組件的通信等,從上到下的數(shù)據(jù)傳遞可以通過 props 一層層傳遞,但從下到上的數(shù)據(jù)傳遞則十分麻煩。例如下圖中【子組件1】相與【父組件B】通信時(shí),就需要將信息一層層冒到祖先組件中,再通過祖先組件派發(fā)給【父組件B】。

因此如果項(xiàng)目較為龐大時(shí),可以引入 redux 進(jìn)行全局狀態(tài)管理(可參考 redux 使用實(shí)例)。當(dāng)項(xiàng)目量級(jí)較小時(shí),則使用 React 中的 Context 來進(jìn)行公共狀態(tài)的管理,該模式包括兩個(gè)角色:
Provider:外層提供數(shù)據(jù)的組件,內(nèi)部組件都可以訪問到來自 provider 的數(shù)據(jù)
Consumer :內(nèi)層獲取數(shù)據(jù)的組件,沿上追溯到最近的 provider,消費(fèi)其數(shù)據(jù)。接收一個(gè)函數(shù)作為子節(jié)點(diǎn),返回 react 節(jié)點(diǎn)。
function Display(props) {
// 6. props 重新賦值,組件更新
return (
<div>
<h2>{props.title}</h2>
<p>你的名字是:{props.name}</p>
<p>你的郵箱是:{props.email}</p>
</div>
)
}
class FormItem extends Component {
state = {
val: ''
}
render() {
const {keyName, label, type} = this.props
// 3. consumer 內(nèi)部接收一個(gè)函數(shù),參數(shù) value 來源于最近的 provider
return (<SurveyContext.Consumer>
{(value, _this) => {
return (<div>
<label htmlFor={keyName}>{label}</label>
<input
id = {keyName}
type = {type}
placeholder={value[keyName]}
onChange = {e => {this.setState({val: e.target.value})}}
onKeyDown = {e => {
if( 13 === e.keyCode ) {
// 4. 調(diào)用操作方法,也即 Survey 組件中的 changeState 方法,修改 provider 中的數(shù)據(jù)
value.change(keyName, this.state.val)
}
}}
/>
</div>)
}}
</SurveyContext.Consumer>)
}
}
// 2. 中間組件不需要傳遞數(shù)據(jù)和方法
class Form extends Component {
render() {
return (
<div>
<FormItem keyName='name' label='名字' type='text'/>
<FormItem keyName='email' label='郵箱' type='text'/>
</div>
);
}
}
const SurveyContext = React.createContext()
export default class Survey extends Component {
state = {
name: 'abc',
email: '123@163.com'
}
changeState(key, val) {
this.setState({[key]: val})
}
// 5. setState 方法觸發(fā)組件更新,重新 render
render() {
return (
<div>
{/* 1. provider 提供 value 給 consumer,可以將修改 state 的方法也作為 value 對(duì)象的方法傳遞*/}
<SurveyContext.Provider
value={{
...this.state,
change: this.changeState.bind(this)
}}
>
<Form></Form>
</SurveyContext.Provider>
<hr />
<Display title='問卷調(diào)查' name={this.state.name} email={this.state.email}></Display>
</div>
)
}
}
效果:

