高阶组件用法及封装
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
简单点说,就是组件作为参数,返回值也是组件的函数,它是纯函数,不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。
使用HOC的原因
- 抽取重复代码,实现组件复用:相同功能组件复用
- 条件渲染,控制组件的渲染逻辑(渲染劫持):权限控制。
- 捕获/劫持被处理组件的生命周期,常见场景:组件渲染性能追踪、日志打点。
HOC实现方式
属性代理
使用组合的方式,将组件包装在容器上,依赖父子组件的生命周期关系来;
- 返回stateless的函数组件
- 返回class组件
- 操作props
// 可以通过属性代理,拦截父组件传递过来的porps并进行处理。
// 返回一个无状态的函数组件
function HOC(WrappedComponent) {
const newProps = { type: 'HOC' };
return props => <WrappedComponent {...props} {...newProps}/>;
}
// 返回一个有状态的 class 组件
function HOC(WrappedComponent) {
return class extends React.Component {
render() {
const newProps = { type: 'HOC' };
return <WrappedComponent {...this.props} {...newProps}/>;
}
};
}
- 抽象state
// 通过属性代理无法直接操作原组件的state,可以通过props和cb抽象state
function HOC(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
name: '',
};
this.onChange = this.onChange.bind(this);
}
onChange = (event) => {
this.setState({
name: event.target.value,
})
}
render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onChange,
},
};
return <WrappedComponent {...this.props} {...newProps} />;
}
};
}
// 使用
@HOC
class Example extends Component {
render() {
return <input name="name" {...this.props.name} />;
}
}
- 通过props实现条件渲染
// 通过props来控制是否渲染及传入数据
import * as React from 'react';
function HOC (WrappedComponent) {
return (props) => (
<div>
{
props.isShow ? (
<WrappedComponent
{...props}
/>
) : <div>暂无数据</div>
}
</div>
);
}
export default HOC;
- 其他元素wrapper传入的组件
function withBackgroundColor(WrappedComponent) {
return class extends React.Component {
render() {
return (
<div style={{ backgroundColor: '#ccc' }}>
<WrappedComponent {...this.props} {...newProps} />
</div>
);
}
};
}
反向继承
使用一个函数接受一个组件作为参数传入,并返回一个继承了该传入组件的类组件,且在返回组件的 render() 方法中返回 super.render() 方法
const HOC = (WrappedComponent) => {
return class extends WrappedComponent {
render() {
return super.render();
}
}
}
- 允许HOC通过this访问到原组件,可以直接读取和操作原组件的state/ref等;
- 可以通过super.render()获取传入组件的render,可以有选择的渲染劫持;
- 劫持原组件生命周期方法
function HOC(WrappedComponent){
const didMount = WrappedComponent.prototype.componentDidMount;
// 继承了传入组件
return class HOC extends WrappedComponent {
async componentDidMount(){
// 劫持 WrappedComponent 组件的生命周期
if (didMount) {
await didMount.apply(this);
}
...
}
render(){
//使用 super 调用传入组件的 render 方法
return super.render();
}
}
}
- 读取/操作原组件的state
function HOC(WrappedComponent){
const didMount = WrappedComponent.prototype.componentDidMount;
// 继承了传入组件
return class HOC extends WrappedComponent {
async componentDidMount(){
if (didMount) {
await didMount.apply(this);
}
// 将 state 中的 number 值修改成 2
this.setState({ number: 2 });
}
render(){
//使用 super 调用传入组件的 render 方法
return super.render();
}
}
}
- 条件渲染
const HOC = (WrappedComponent) =>
class extends WrappedComponent {
render() {
if (this.props.isRender) {
return super.render();
} else {
return <div>暂无数据</div>;
}
}
}
- 修改react树
// 修改返回render结果
function HigherOrderComponent(WrappedComponent) {
return class extends WrappedComponent {
render() {
const tree = super.render();
const newProps = {};
if (tree && tree.type === 'input') {
newProps.value = 'something here';
}
const props = {
...tree.props,
...newProps,
};
const newTree = React.cloneElement(tree, props, tree.props.children);
return newTree;
}
};
}
属性代理和反向继承对比
- 属性代理:从“组合”角度出发,有利于从外部操作wrappedComp,可以操作props,或者在wrappedComp 外加一些拦截器(如条件渲染等);
- 反向继承:从“继承”角度出发,从内部操作wrappedComp,可以操作组件内部的state,生命周期和render等,功能能加强大;
具体实例
- 页面复用(属性代理)
// views/PageA.js
import React from 'react';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';
class PageA extends React.Component {
state = {
movieList: [],
}
/* ... */
async componentDidMount() {
const movieList = await fetchMovieListByType('comedy');
this.setState({
movieList,
});
}
render() {
return <MovieList data={this.state.movieList} emptyTips="暂无喜剧"/>
}
}
export default PageA;
// views/PageB.js
import React from 'react';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';
class PageB extends React.Component {
state = {
movieList: [],
}
// ...
async componentDidMount() {
const movieList = await fetchMovieListByType('action');
this.setState({
movieList,
});
}
render() {
return <MovieList data={this.state.movieList} emptyTips="暂无动作片"/>
}
}
export default PageB;
// 冗余代码过多
// HOC
import React from 'react';
const withFetchingHOC = (WrappedComponent, fetchingMethod, defaultProps) => {
return class extends React.Component {
async componentDidMount() {
const data = await fetchingMethod();
this.setState({
data,
});
}
render() {
return (
<WrappedComponent
data={this.state.data}
{...defaultProps}
{...this.props}
/>
);
}
}
}
// 使用:
// views/PageA.js
import React from 'react';
import withFetchingHOC from '../hoc/withFetchingHOC';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';
const defaultProps = {emptyTips: '暂无喜剧'}
export default withFetchingHOC(MovieList, fetchMovieListByType('comedy'), defaultProps);
// views/PageB.js
import React from 'react';
import withFetchingHOC from '../hoc/withFetchingHOC';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';
const defaultProps = {emptyTips: '暂无动作片'}
export default withFetchingHOC(MovieList, fetchMovieListByType('action'), defaultProps);;
// views/PageOthers.js
import React from 'react';
import withFetchingHOC from '../hoc/withFetchingHOC';
import fetchMovieListByType from '../lib/utils';
import MovieList from '../components/MovieList';
const defaultProps = {...}
export default withFetchingHOC(MovieList, fetchMovieListByType('some-other-type'), defaultProps);
更符合 里氏代换原则(Liskov Substitution Principle LSP),任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为
- 权限控制(属性代理)
import React from 'react';
import { whiteListAuth } from '../lib/utils'; // 鉴权方法
function AuthWrapper(WrappedComponent) {
return class AuthWrappedComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
permissionDenied: -1,
};
}
async componentDidMount() {
try {
await whiteListAuth(); // 请求鉴权接口
this.setState({
permissionDenied: 0,
});
} catch (err) {
this.setState({
permissionDenied: 1,
});
}
}
render() {
if (this.state.permissionDenied === -1) {
return null; // 鉴权接口请求未完成
}
if (this.state.permissionDenied) {
return <div>功能即将上线,敬请期待~</div>;
}
return <WrappedComponent {...this.props} />;
}
}
}
export default AuthWrapper;
- 组件渲染性能(反向继承)
如何计算一个组件render期间的渲染耗时?
import React from 'react';
// Home 组件
class Home extends React.Component {
render () {
return (<h1>Hello World.</h1>);
}
}
// HOC
function withTiming (WrappedComponent) {
let start, end;
return class extends WrappedComponent {
constructor (props) {
super(props);
start = 0;
end = 0;
}
componentWillMount () {
if (super.componentWillMount) {
super.componentWillMount();
}
start = +Date.now();
}
componentDidMount () {
if (super.componentDidMount) {
super.componentDidMount();
}
end = +Date.now();
console.error(`${WrappedComponent.name} 组件渲染时间为 ${end - start} ms`);
}
render () {
return super.render();
}
};
}
export default withTiming(Home);