無論是 vue诊笤、React 還是 Angular戴质,主流框架都支持并提倡組件化開發(fā)钱磅,因為組件化開發(fā)不僅可以增強代碼的能動性和復(fù)用性,還能夠加快團(tuán)隊協(xié)作的速度宪潮。組件化開發(fā)就像搭積木溯警,首先把一個個積木(組件)設(shè)計好趣苏,甚至將小積木(容器組件、展示組件)組裝成具備一定功能的積木(比如一個房子)梯轻,最終再將功能化的積木摞成最終的成品(比如一個社區(qū))食磕。
本文簡單介紹 React 中組件的定義,以及容器組件喳挑、展示組件彬伦、高階組件、復(fù)合組件等常見組件的應(yīng)用伊诵,并介紹組件間的通信方式单绑。
1. 如何定義一個組件
1.1 一般組件
React 中組件的定義有兩種方式,一種是使用 Class
關(guān)鍵字以類的形式來定義組件曹宴,另一種是使用函數(shù)方式定義搂橙。比如定義一個網(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è)置多個 key 值,key 值相同時后者覆蓋前者
// setState 是一個異步函數(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
不會引起組件的重新渲染诀豁。該部分的功能也可以提前到constructor
中窄刘,因此很少在項目中使用。 -
render
舷胜,根據(jù)組件的props
和state
(兩者的重傳遞和重賦值娩践,無論值是否有變化,都可以引起組件重新 render)烹骨,返回?個 React 元素(描述組件翻伺,即UI),不負(fù)責(zé)組件實際渲染?作沮焕,之后由 React ?身根據(jù)此元素去渲染出??DOM吨岭。純函數(shù),返回結(jié)果只依賴于傳入的參數(shù)峦树,執(zhí)行過程中沒有副作用辣辫。不能在該階段執(zhí)行setState
旦事,會造成死循環(huán)。 -
componentDidMount
急灭,組件掛載到 DOM 之后調(diào)用姐浮,且只會被調(diào)用一次。
-
- 組件的更新:當(dāng)
props
或state
被重新賦值時葬馋,無論值是否發(fā)生改變卖鲤,都會觸發(fā)組件的更新。因此有如下兩種情況會觸發(fā)組件的更新:1. 父組件重新 render畴嘶,由于子組件的props
被傳值蛋逾,觸發(fā)子組件的更新;2. 組件本身調(diào)用setState
掠廓,無論state
有沒有改變换怖,組件都會更新-
componentWillReceiveProps(nextProps)
,props
重傳時被調(diào)用蟀瞧,該函數(shù)中調(diào)用setState
不會引起組件的二次更新沉颂,因此即便在該函數(shù)中執(zhí)行this.setState
更新了state
,shouldComponentUpdate
componentWillUpdate
中的this.state
依舊是原來的值悦污。 -
shouldComponentUpdate(nextProps, nextState)
铸屉,此?法通過?較nextProps
,nextState
及當(dāng)前組件的this.props
切端,this.state
彻坛,返回true
時當(dāng)前組件將繼續(xù)執(zhí)?更新過程,返回false
則當(dāng)前組件更新停?踏枣,以此可?來減少組件的不必要渲染昌屉,優(yōu)化組件性能。 -
componentWillUpdate(nextProps, nextState)
茵瀑,此?法在調(diào)?render
?法前執(zhí)?间驮,在這邊可執(zhí)??些組件更新發(fā)?前的?作,?般較少?马昨。 -
render
:同掛載時的 render竞帽。 -
componentDidUpdate(prevProps, prevState)
,此?法在組件更新后被調(diào)?鸿捧,可以操作組件更新的 DOM 屹篓,prevProps
和prevState
這兩個參數(shù)指的是組件更新前的props
和state
。
-
- 組件的卸載:
-
componentWillUnmount
:此?法在組件被卸載前調(diào)?匙奴,可以在這?執(zhí)??些清理?作堆巧,?如清除組件中使?的定時器,componentDidMount
中?動創(chuàng)建的 DOM 元素等,以避免引起內(nèi)存泄漏恳邀。
-
- 【注意】
componentWillMount
componentWillReceiveProps
和componentWillUpdate
在 React 17.x 版本之后將不再支持懦冰,目前使用會提示 warning。在 16.3 之后谣沸,使用getDerivedStateFromProps
代替上述三個函數(shù)-
static getDerivedStateFromProps(props, state)
刷钢,在組件創(chuàng)建時和更新時的 render ?法之前調(diào)?, 它應(yīng)該返回?個對象來更新狀態(tài)乳附,或者返回 null 來不更新任何內(nèi)容内地。 -
getSnapshotBeforeUpdate
,被調(diào)?于render之后赋除,此時可以讀取但還不能操作更新 DOM 阱缓,因此可以按需調(diào)整滾動條等。 返回值(必須有)將作為參數(shù)傳遞給componentDidUpdate
举农。
引自https://github.com/aermin/blog/issues/55
-
- 初始化:
1.2 組件拆分——容器組件&展示組件
在涉及復(fù)雜的數(shù)據(jù)預(yù)處理時荆针,可以考慮將組件拆分成容器組件和展示組件。其中容器組件負(fù)責(zé)請求并處理數(shù)據(jù)颁糟,展示組件負(fù)責(zé)根據(jù) Props 顯示信息航背。如此可以減小組件的體積,使開發(fā)人員可以跟專注于某一功能開發(fā)棱貌,并提高組件的重用性和可用性玖媚,同時易于測試和提高系統(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
是否變化,都會出發(fā)更新障贸。純組件就是定制了 shouldComponentUpdate
后的Component错森,僅有依賴的數(shù)據(jù)發(fā)生變化時才進(jìn)行更新。 該比較過程數(shù)據(jù)淺比較篮洁,因此對象屬性或數(shù)組中元素并不適用于該特性问词。
// 假設(shè)父組件有 count 和 name 兩個狀態(tài)
// 子組件僅依賴父組件的 count
// 如果子組件繼承的是 React.Component,那么父組件 name 值發(fā)生變更時嘀粱,子組件依舊會重新 render
// 繼承的是 React.PureComponent 時,則僅有父組件的 count 值變化時辰狡,子組件才會重新調(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 中重用組件邏輯的高級技術(shù),它不是 React 的 api 宛篇,而是一種組件增強模式娃磺。高階組件是一個函數(shù),它返回另外一個組件叫倍,產(chǎn)生新的組件可以對被包裝組件屬性進(jìn)行包裝偷卧,也可以重寫部分生命周期豺瘤。
高階組件可以為組件添加某一特殊功能,也可以多層嵌套听诸,賦予被包裝組件多個功能坐求。比如打印日志功能、添加標(biāo)題功能等晌梨。
// 包裝后的組件具備日志打印功能
const withLog = Component => {
class newComponent extends React.Component {
componentDidMount() {
console.log(`${Date.now()}:組件已掛載`)
}
render() {
return <Component {...this.props} />
}
}
return newComponent
}
// 包裝后的組件都帶有一個標(biāo)題
const withTitle = Component => {
const newComponent = props => {
return (<Fragment>
<h3>這是一個標(biāo)題</h3>
<hr />
<Component {...props} />
</Fragment>)
}
return newComponent
}
2.2 高階組件怎么使用
- 鏈?zhǔn)秸{(diào)用
高階組件本質(zhì)上就是一個函數(shù)桥嗤,因此可以采用鏈?zhǔn)秸{(diào)用的形式,將待包裝的組件作為參數(shù)傳入仔蝌,并 export 出去即可泛领。同時也可以多個高階組件嵌套,一層層包裝單一組件敛惊。
export default withLog(withTitle(CommentList))
- 裝飾者模式
ES7 中提供了裝飾者模式的寫法渊鞋,可以使代碼更加簡潔,但需要進(jìn)行相關(guān)配置:
暴露項目的所有配置項:
npm run eject
安裝:
npm install -D @babel/plugin-proposal-decorators
-
配置
package.json
文件中 babel 配置項"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. 組件間如何實現(xiàn)通信
4.1 父傳子
通過 props 將參數(shù)傳遞給子組件鸵鸥,使用 class
關(guān)鍵字以類方式定義組件時奠滑,使用 this.props
即可以父組件傳遞的所有參數(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 子傳父
父組件中聲明一個相關(guān)方法,并作為參數(shù)傳遞給子組件讼油。子組件通過調(diào)用父組件傳遞過來的方法杰赛,修改父組件中的數(shù)據(jù)。
// 比如:父組件中有個計數(shù)值矮台,子組件中的按鈕點擊之后計數(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>
計數(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】通信時,就需要將信息一層層冒到祖先組件中确虱,再通過祖先組件派發(fā)給【父組件B】含友。
因此如果項目較為龐大時,可以引入 redux 進(jìn)行全局狀態(tài)管理(可參考 redux 使用實例)。當(dāng)項目量級較小時窘问,則使用 React 中的 Context 來進(jìn)行公共狀態(tài)的管理辆童,該模式包括兩個角色:
Provider:外層提供數(shù)據(jù)的組件,內(nèi)部組件都可以訪問到來自 provider 的數(shù)據(jù)
Consumer :內(nèi)層獲取數(shù)據(jù)的組件惠赫,沿上追溯到最近的 provider把鉴,消費其數(shù)據(jù)。接收一個函數(shù)作為子節(jié)點汉形,返回 react 節(jié)點纸镊。
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)部接收一個函數(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 對象的方法傳遞*/}
<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>
)
}
}
效果: