高階組件是對既有組件進行包裝,以增強既有組件的功能。其核心實現(xiàn)是一個無狀態(tài)組件(函數(shù)),接收另一個組件作為參數(shù),然后返回一個新的功能增強的組件。
看一個最簡單的例子:
App.js:
import React,{ PureComponent } from "react";
const DisplayUser = (props) => {
return(
<div>
姓名:{ props.username }
</div>
);
}
const HOC = (InnerComponent) => {
return class HOCComponent extends PureComponent{
render(){
const { username } = this.props;
return(
<InnerComponent username = { username } />
);
}
}
}
export default HOC(DisplayUser);
index.js:
import React from "react";
import ReactDOM from "react-dom";
import HOC from "./App";
ReactDOM.render(<HOC username = "Mike" birth = "1999-09-09" />,document.getElementById("root"));
HOC 函數(shù)接受一個組件作為參數(shù),返回一個新的組件,新組件中渲染了參數(shù)組件,這樣就可以對參數(shù)組件做一些配置,例如本例的過濾 props,只傳遞給參數(shù)組件合法的 props,不傳遞非法的 props。
高階組件的分類
高階組件分為兩種類型:
- 代理型高階組件
- 繼承型高階組件
上面的例子中的高階組件類型就是代理型高階組件。
二者的主要區(qū)別是對參數(shù)組件的操作上,代理型高階組件是返回一個新的組件類,該類繼承于根組件(Component 或 PureComponent),然后讓這個新的組件類去渲染參數(shù)組件,這樣就可以對參數(shù)組件實現(xiàn)包裝或代理 props 的操作。繼承型高階組件也是返回一個新的組件類,但這個新的類不繼承根組件類,而是繼承于參數(shù)組件類,這樣我們就可以重寫參數(shù)組件類中的一些方法。
這里的高階組件應(yīng)該叫做高階組件生成函數(shù),其執(zhí)行的返回值才叫高階組件,為了方便我們這里統(tǒng)一稱為高階組件了,請知悉。
下面分別來說下這兩類的高階組件。
代理型高階組件
代理型高階組件接受一個組件作為參數(shù),返回一個新的繼承于根組件(Component、PureComponent)的組件,并用該新組件渲染參數(shù)組件,其基本結(jié)構(gòu)為:
export const HOC = (SomeComponent) => {
return class HOCComponent extends PureComponent{
render(){
doSomeThing()...
return(
<SomeComponent />
);
}
}
}
代理型高階組件有以下幾個作用:
- 代理 props
- 訪問 ref
- 抽取狀態(tài)
- 包裝組件
代理 props
代理型高階組件可以接收被包裝組件的 props,并對這些 props 進行操作,如增刪改操作。在渲染被包裝組件時,傳入被修改的 props,如本文開頭的例子。
基本結(jié)構(gòu)如下:
export const HOC = (SomeComponent) => {
return class HOCComponent extends PureComponent{
render(){
const newProps = doSomeThing(this.props);
return(
<SomeComponent {...newProps} />
);
}
}
}
訪問 ref
ref 是一個特殊的屬性,可以聲明為函數(shù),如果 ref 屬性是一個函數(shù),那么將在組件裝載完成后自動執(zhí)行該函數(shù),并將當(dāng)前組件的實例傳入該函數(shù)中。
看一個栗子:
class SubComponent extends PureComponent{
render(){
return(
<div className = "box">
我想要被獲取!!!
</div>
);
}
}
const HOC = (InnerComponent) => {
return class HOCComponent extends PureComponent{
constructor(...args){
super(...args);
this.handSubRef = this.handSubRef.bind(this);
}
// 獲取子組件的實例
// 該函數(shù)在子組件被裝載后自動調(diào)用
handSubRef(subEle){
console.log(subEle);
}
render(){
return(
<InnerComponent ref = { this.handSubRef } />
);
}
}
}
export default HOC(SubComponent);
這樣,在參數(shù)組件被裝載后,就會自動執(zhí)行 handSubRef 函數(shù),并將參數(shù)組件的實例傳入 handSubRef 函數(shù)。在 handSubRef 中可以獲取參數(shù)組件的一些列屬性,如 state、props 等。
注意,如果參數(shù)組件是一個無狀態(tài)組件,那么傳入 handSubRef 的參數(shù)將是 null。
const SubComponent = (props) => {
return(
<div className = "box">
我想要被獲取!!!
</div>
);
}
const HOC = (InnerComponent) => {
return class HOCComponent extends PureComponent{
constructor(...args){
super(...args);
this.handSubRef = this.handSubRef.bind(this);
}
// 參數(shù)組件是無狀態(tài)組件
// subEle 為 null
handSubRef(subEle){
console.log(subEle);
}
render(){
return(
<InnerComponent ref = { this.handSubRef } />
);
}
}
}
export default HOC(SubComponent);
抽取狀態(tài)
當(dāng)一個組件的功能較復(fù)雜時,我們建議將組件拆分為容器組件和UI組件,UI組件負(fù)責(zé)展示,容器組件用來進行邏輯管理。這個步驟就是“抽取狀態(tài)”。
我們將前面的計算器應(yīng)用中的組件進行一些修改:
...
const UICounter = (props) => {
const { increase,decrease,value} = props;
return(
<div className = "counter">
<div>
<button onClick = { increase }>+</button>
<button onClick = { decrease }>-</button>
</div>
<span>當(dāng)前的值為:{ value }</span>
</div>
);
}
const HOC = (SubEle) => {
return class HOCComponent extends PureComponent{
constructor(props) {
super(props);
// 獲取初始狀態(tài)
this.state = {
value:store.getState().value,
};
}
componentWillMount() {
// 監(jiān)聽 store 變化
store.subscribe(this.watchStore.bind(this));
}
componentWillUnmount() {
// 對 store 變化取消監(jiān)聽
store.unsubscribe(this.watchStore.bind(this));
}
// 監(jiān)聽回調(diào)函數(shù),當(dāng) store 變化后執(zhí)行
watchStore(){
// 回調(diào)函數(shù)中重新設(shè)置狀態(tài)
this.setState(this.getCurrentState());
}
// 從 store 中獲取狀態(tài)
getCurrentState(){
return{
value:store.getState().value,
}
}
// 增加函數(shù)
increase(){
// 派發(fā) INCREMENT Action
store.dispatch(ACTIONS.increament());
}
// 減少函數(shù)
decrease(){
// 派發(fā) DECREAMENT Action
store.dispatch(ACTIONS.decreament());
}
render(){
return(
<UICounter
increase = { this.increase.bind(this)}
decrease = { this.decrease.bind(this)}
value = { this.state.value }
/>
);
}
}
}
export default HOC(UICounter);
...
包裝組件
包裝組件就是對某些組件進行組合,返回不同的組合形式,同時可以設(shè)置展示樣式。如將展示性 select 和輸入表單包裝成可搜索的 select等。
基本結(jié)構(gòu)如下:
// 可以傳入一個數(shù)組,或者單個組件
const HOC = (SubComponents) => {
return class HOCComponent extends PureComponent{
render(){
const style = ...
// 將子組件組合成一個大組件,同時可以修改樣式
return(
<div style = { style }>
{...SubComponents}
</div>
);
}
}
}
export default HOC([Select,SearchBox]);
至此,代理型高階組件就說完了,下面說說繼承型高階組件。
繼承型高階組件
和代理型高階組件創(chuàng)建一個繼承于根組件(Component、PureComponent)的組件類不同的是,繼承型高階組件是創(chuàng)建一個繼承于參數(shù)組件的組件類,以此可以覆寫父組件中的一些方法。
最常用的應(yīng)用是重寫父組件的生命周期函數(shù):
const HOC = (ParComponents) => {
return class HOCComponent extends ParComponents{
// 覆寫父組件的生命周期函數(shù)
shouldComponentUpdate(nextProps, nextState) {
...
}
render(){
// 渲染時仍然按照父組件的 render 函數(shù)進行渲染
return super.render();
}
}
}
export default HOC(ParComponents);
子組件覆寫了父組件的生命周期方法,以獲取更好的渲染性能,在渲染(render)時,還是調(diào)用父類的 render 方法。
函數(shù)作為子組件
前面用高階組件實現(xiàn)了代理 props 功能,對于特定的組件,需要傳入特定的 props 名,由于每個組件需要的 props 可能有差異,如果使用高階組件對參數(shù)組件進行判斷,再傳入合適的 props 名顯然太麻煩了。這時如果我們需要更通用的方案,可以接收函數(shù)作為子組件,利用函數(shù)的形參的特性來排除組件 props 名之間的差異性。看一下演示代碼:
...
// 此組件需要使用 user props
const SubComponent1 = (user) => {
return(
<TestComponent user = { user } />
);
}
// 此組件需要使用 username props
const SubComponent2 = (user) => {
return(
<TestComponent2 username = { user } />
);
}
// 高階組件接受 testUser 作為 props
const HOC = (props) => {
const { testUser } = props;
return(
props.children(testUser)
);
}
...
使用此高階組件傳遞 props:
...
<HOC testUser = "MIKE">
{ SubComponent1 }
</HOC>
<HOC testUser = "JACK">
{ SubComponent2 }
</HOC>
...
高階組件接收一個函數(shù)作為子組件,然后調(diào)用這個組件函數(shù),并傳入相應(yīng)的 props,函數(shù)調(diào)用的結(jié)果返回一個組件,組件需要接收什么樣的 props 名可以自行定義,不用在高階組件中進行判斷了。
以函數(shù)作為組件的形式主要用于不同的組件需要接收同一份 props,但是它們需要的 props 名稱不一樣的情況,可以用函數(shù)的形參巧妙的化解掉父組件的判斷。雖然用途較少,但不失為一個優(yōu)秀的模式,將 children 進行調(diào)用簡直是神來之筆有木有。
總結(jié)
本文談到了 React 中的高階組件,高階組件主要包括兩種類型:
- 代理型高階組件
- 繼承型高階組件
代理型高階組件的主要用途是:
- 代理子組件的 props(對 props 進行增刪改操作)
- 獲取子組件的 ref(實例)
- 抽取子組件狀態(tài)(拆分邏輯)
- 將子組件進行包裝(組合子組件,調(diào)整樣式等)
繼承型高階組件的主要用途是覆寫父組件的生命周期方法,通常是 shouldComponentUpdate 方法,以此來提高渲染性能,渲染時調(diào)用父類的 render 函數(shù)(super.render())。
最后,介紹了函數(shù)作為子組件的情形,主要用于不同的組件接受同一份 props,但是它們各自需要的 props 名不同的情況,利用函數(shù)的形參巧妙化解父組件的額外判斷操作。這是一種優(yōu)秀的模式,讓人耳目一新。
完。