本文章重點講述React高階組件的使用場景,附帶講述高階函數,因為兩者很類似。對比起來更容易理解
什么是高階函數:一個函數的參數是一個函數,或者 函數的返回值是一個函數,我們稱這類函數是高階函數。
例如:
const Mozi = function(name1){
return function (name2){
return name1 + ' ' + name2;
};
};
上面函數的執行過程如下
> Mozi('墨子')
<- function(name2){
return name1 + ' ' + name2;
}
> Mozi('墨子')('工程')
<- '墨子 工程'
執行函數Mozi('墨子') 返回的是一個函數,我們執行 Mozi('墨子')('工程') 相當于執行了返回的函數
高階函數實際的作用是什么?
以下面例子,我們要把數組的每一項變成mozi_n的方式
我們一般會用下面的方法進行實現
function format(item) {
return `mozi_${item}`;
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var result = [];
for (var i=0; i<arr.length; i++) {
result.push(format(arr[i]));
}
console.log(result) // ["mozi_1", "mozi_2", "mozi_3", "mozi_4", "mozi_5", "mozi_6", "mozi_7", "mozi_8", "mozi_9"]
如果用高階函數map來實現,如下
function format(item) {
return `mozi_${item}`;
}
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(format); // ["mozi_1", "mozi_2", "mozi_3", "mozi_4", "mozi_5", "mozi_6", "mozi_7", "mozi_8", "mozi_9"]
上例中map把對數組的遍歷進行了抽象封裝,我們在對數組進行操作的時候不用關心map內部是怎么實現的,只需要將我們對數組的每一項的處理函數傳進去,就會返回處理后的新數組。
這樣做的好處是簡潔,通用,我們再對數組進行其他操作的時候就不需要重復寫for循環來處理,例如
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// 把數組每一項轉換成字符串,只需要一行代碼
arr.map(String); // ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
上面兩個例子來簡單描述了一下什么是高階函數,以及簡單用法。(有空再看一下MOZI中對redux connect的封裝)
什么是React高階組件:一個組件的參數是組件,并且返回值是一個組件,我們稱這類組件為高階組件
React 中的高階組件主要有兩種形式:屬性代理 和 反向繼承。
屬性代理: 是 一個函數接受一個 WrappedComponent 組件作為參數傳入,并返回一個繼承了 React.Component 組件的類,且在該類的 render() 方法中返回被傳入的 WrappedComponent 組件
屬性代理形如:
function MoziComponent(WrappedComponent) {
return class extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
};
}
屬性代理類高階組件可以做什么?
- 操作 props
- 用其他元素包裹傳入的組件 WrappedComponent
- 通過 ref 訪問到組件實例
- 抽離 state
一:操作 props(增加新props)
function MoziComponent(WrappedComponent) {
return class extends WrappedComponent {
render() {
const newProps = {
mozi: 'mozi'
}
return <WrappedComponent {...this.props,..newProps} />
}
};
}
二:用其他元素包裹傳入的組件 WrappedComponent
function MoziComponent(WrappedComponent) {
return class extends WrappedComponent {
render() {
const newProps = {
mozi: 'mozi'
}
return <div style={{display: 'block'}}>
<WrappedComponent/>
</div>
}
};
}
三:通過 ref 訪問到組件實例(在高階組件中獲取到組件實例可以直接操作dom,比如input,但是實用場景很少,有些很難用state去實現的復雜動畫之類的效果,用ref直接操作dom更方便一些)
import React, { Component } from 'react';
const header_hoc = WrappedComponent => {
return class header_hocmozi extends Component {
renderHeader = () => {
return <div style={{margin:'0 auto',textAlign:'center'}}>
全局頭部
</div>
}
getRef(ComponentRef) { // 看這里
if(ComponentRef && ComponentRef.home) {
console.log('wrappedComponentInstance-----',ComponentRef.home)
}
}
render() {
const __props ={...this.props,ref:this.getRef.bind(this) }
return <div>
{ this.renderHeader() }
<WrappedComponent {...__props}/>
</div>
}
}
}
export default header_hoc;
四:抽離 state(因為屬性代理類的高階組件可以造作一切react有的方法或者屬性,但是不建議在高階函數里操作state,因為state屬于一個組件的私有行為,一旦在高階組件里做了一些state的更改刪除之類的,容易把原有組件覆蓋或者誤操作,出了問題也很難去定位。)
反向繼承:是 一個函數接受一個 WrappedComponent 組件作為參數傳入,并返回一個繼承了該傳入 WrappedComponent 組件的類,且在該類的 render() 方法中返回 super.render() 方法。
是因為傳入組件被動地被返回的Mozi繼承,而不是 WrappedComponent 去繼承 Mozi。通過這種方式他們之間的關系倒轉了,所以叫反向繼承。
反向繼承形如:
function MoziComponent(WrappedComponent) {
return class Mozi extends WrappedComponent {
render() {
return super.render();
}
};
}
反向繼承因為繼承了傳入的組件,所以你可以獲取傳入組件(WrappedComponent)的state,props,render,以及整個生命周期
可以用反向繼承高階組件做什么?
- 渲染劫持
- State 抽象和更改
1 - 條件渲染【渲染劫持】
如下,通過 props.isLoading 這個條件來判斷渲染哪個組件
const withLoading = (WrappedComponent) => {
return class extends WrappedComponent {
render() {
if(this.props.isLoading) {
return <Loading />;
} else {
return super.render();
}
}
};
}
2 - 修改要渲染的元素【渲染劫持】
以下demo只是展示了如何通過修改element,意味著你可以在高階組件中隨意控制返回你要渲染的元素。
比如:我們的權限控制有頁面級別的,也有細粒度的按鈕級別的,渲染劫持類高階組件就最適合細粒度的把控每個小組件的渲染權限,根據接口返回是否要展示某一個按鈕 or 某一個組件。(并且不侵入具體業務代碼,如果沒有用高階組件去抽離這部分細粒度的權限控制你就需要在業務代碼中去寫,會變得很難維護)
/*
組件修改,組件級別權限控制
*/
const edit_hoc = WrappedComponent => {
return class extends WrappedComponent {
render() {
const tree = super.render();
let props = {
...tree.props,
};
props.children.map(item=>{
item && item.type == 'input' && (item.props.value = '墨子工程')
})
const newTree = React.cloneElement(tree, props, tree.props.children);
return newTree;
}
};
};
export default edit_hoc;
實用場景
1- 組件渲染性能追蹤
//performance_hoc.js
const hoc = WrappedComponent => {
return class performance_hoc extends WrappedComponent {
constructor(props) {
super(props);
this.start = 0;
this.end = 0;
}
componentWillMount() {
super.componentWillMount && super.componentWillMount();
this.start = Date.now();
}
componentDidMount() {
super.componentDidMount && super.componentDidMount();
this.end = Date.now();
console.log(`%c ${WrappedComponent.name} 組件渲染時間為 ${this.end - this.start} ms`,'font-size:20px;color:red;');
}
render() {
return super.render()
}
};
};
export default hoc;
使用方式
import React, { Component } from 'react';
import hoc from './performance_hoc';
@hoc
export default class Performance extends Component {
render() {
return <div>墨子工程</div>
}
}
2 - 權限控制
import React, { Component } from 'react';
const auth_hoc = role => WrappedComponent => {
return class extends React.Component {
state = {
permission: false,
loading: true
}
showLoading = (isShow) => {
this.setState({
loading: isShow,
});
}
checkAuth = () => { // 模擬返回角色的api
return new Promise((reslove,reject)=>{
this.showLoading(true)
setTimeout(_=>{
reslove('admin')
this.showLoading(false)
},2000)
})
}
async componentWillMount() {
const __role = await this.checkAuth();
this.setState({
permission: __role == role,
});
}
render() {
const { loading, permission } = this.state
if(loading) {
return <div>loading...</div>
}
if (permission) {
return <WrappedComponent {...this.props} />;
} else {
return (<div>您沒有權限查看該頁面!</div>);
}
}
};
}
export default auth_hoc;
使用(比如:產品需求是這個頁面只有admin方式的才能展示 or 只有vip方式的才能展示,就可以通過高階組件去抽離,你也不需要每一個權限都寫一個高階組件,只需要 傳遞不同的權限去接口驗證就ok)
import React, { Component } from 'react';
import auth_hoc from './auth_hoc';
@auth_hoc('admin') //vip
export default class AuthPage extends Component {
constructor(...args) {
super(...args);
}
render() {
return (
<div className="Home">
墨子工程
</div>
);
}
}
3 - 組件&邏輯復用
//header_hoc.js
import React, { Component } from 'react';
const header_hoc = WrappedComponent => {
return class header_hocmozi extends Component {
renderHeader = () => {
return <div style={{margin:'0 auto',textAlign:'center'}}>
全局頭部
</div>
}
render() {
return <div>
{ this.renderHeader() }
<WrappedComponent {...this.props}/>
</div>
}
}
}
export default header_hoc;
使用
import React, { Component } from 'react';
import header_hoc from './header_hoc';
@header_hoc
export default class Home extends Component {
render() {
return (
<div className="Home">
首頁
</div>
);
}
}
高階組件其實就是裝飾器模式在 React 中的實現:通過給函數傳入一個組件,在函數內部對該組件進行功能的增強,最后返回這個組件
總結:
- 高階組件 不是組件,是 一個把某個組件轉換成另一個組件的 函數
- 高階組件的主要作用是 代碼復用
- 高階組件是 裝飾器 在 React 中的實現