如果一个函数 接受一个或多个组件作为参数并且返回一个组件 就可称之为 高阶组件。
最简单的属性代理实现:
// 无状态
function HigherOrderComponent(WrappedComponent) {
return props => <WrappedComponent {...props} />;
}
// or
// 有状态
function HigherOrderComponent(WrappedComponent) {
return class extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
};
}
可以发现,属性代理其实就是 一个函数接受一个 WrappedComponent
组件作为参数传入,并返回一个继承了 React.Component
组件的类,且在该类的 render()
方法中返回被传入的 WrappedComponent
组件。
那我们可以利用属性代理类型的高阶组件做一些什么呢?
因为属性代理类型的高阶组件返回的是一个标准的 React.Component
组件,所以在 React 标准组件中可以做什么,那在属性代理类型的高阶组件中就也可以做什么,比如:
props
state
ref
访问到组件实例WrappedComponent
最简单的反向继承实现:
function HigherOrderComponent(WrappedComponent) {
return class extends WrappedComponent {
render() {
return super.render();
}
};
}
反向继承其实就是 一个函数接受一个 WrappedComponent
组件作为参数传入,并返回一个继承了该传入 WrappedComponent
组件的类,且在该类的 render()
方法中返回 super.render()
方法。
会发现其属性代理和反向继承的实现有些类似的地方,都是返回一个继承了某个父类的子类,只不过属性代理中继承的是 React.Component
,反向继承中继承的是传入的组件 WrappedComponent
。
反向继承可以用来做什么:
state
之所以称之为 渲染劫持 是因为高阶组件控制着 WrappedComponent
组件的渲染输出,通过渲染劫持我们可以:
element tree
)render()
输出的 React 元素树render()
输出的 React 元素中操作 props
WrappedComponent
(同 属性代理)通过 props.isLoading
这个条件来判断渲染哪个组件。
function withLoading(WrappedComponent) {
return class extends WrappedComponent {
render() {
if(this.props.isLoading) {
return <Loading />;
} else {
return super.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;
}
};
}
利用高阶组件的 条件渲染 特性可以对页面进行权限控制,权限控制一般分为两个维度:页面级别 和 页面元素级别,这里以页面级别来举一个栗子:
// HOC.js
function withAdminAuth(WrappedComponent) {
return class extends React.Component {
state = {
isAdmin: false,
}
async componentWillMount() {
const currentRole = await getCurrentUserRole();
this.setState({
isAdmin: currentRole === 'Admin',
});
}
render() {
if (this.state.isAdmin) {
return <WrappedComponent {...this.props} />;
} else {
return (<div>您没有权限查看该页面,请联系管理员!</div>);
}
}
};
}
然后是两个页面:
// pages/page-a.js
class PageA extends React.Component {
constructor(props) {
super(props);
// something here...
}
componentWillMount() {
// fetching data
}
render() {
// render page with data
}
}
export default withAdminAuth(PageA);
// pages/page-b.js
class PageB extends React.Component {
constructor(props) {
super(props);
// something here...
}
componentWillMount() {
// fetching data
}
render() {
// render page with data
}
}
export default withAdminAuth(PageB);
使用高阶组件对代码进行复用之后,可以非常方便的进行拓展,比如产品经理说,PageC 页面也要有 Admin 权限才能进入,我们只需要在 pages/page-c.js
中把返回的 PageC 嵌套一层 withAdminAuth
高阶组件就行,就像这样 withAdminAuth(PageC)
。是不是非常完美!非常高效!!但是。。第二天产品经理又说,PageC 页面只要 VIP 权限就可以访问了。你三下五除二实现了一个高阶组件 withVIPAuth
。第三天。。。
其实你还可以更高效的,就是在高阶组件之上再抽象一层,无需实现各种 withXXXAuth
高阶组件,因为这些高阶组件本身代码就是高度相似的,所以我们要做的就是实现一个 返回高阶组件的函数,把 变的部分(Admin、VIP) 抽离出来,保留 不变的部分,具体实现如下:
// HOC.js
const withAuth = role => WrappedComponent => {
return class extends React.Component {
state = {
permission: false,
}
async componentWillMount() {
const currentRole = await getCurrentUserRole();
this.setState({
permission: currentRole === role,
});
}
render() {
if (this.state.permission) {
return <WrappedComponent {...this.props} />;
} else {
return (<div>您没有权限查看该页面,请联系管理员!</div>);
}
}
};
}
可以发现经过对高阶组件再进行了一层抽象后,前面的 withAdminAuth
可以写成 withAuth('Admin')
了,如果此时需要 VIP 权限的话,只需在 withAuth
函数中传入 'VIP'
就可以了。
借助父组件子组件生命周期规则捕获子组件的生命周期,可以方便的对某个组件的渲染时间进行记录:
class Home extends React.Component {
render() {
return (<h1>Hello World.</h1>);
}
}
function withTiming(WrappedComponent) {
return class 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(`${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`);
}
render() {
return super.render();
}
};
}
export default withTiming(Home);
withTiming
是利用 反向继承 实现的一个高阶组件,功能是计算被包裹组件(这里是 Home
组件)的渲染时间。
假设我们有两个页面 pageA
和 pageB
分别渲染两个分类的电影列表,普通写法可能是这样:
// pages/page-a.js
class PageA extends React.Component {
state = {
movies: [],
}
// ...
async componentWillMount() {
const movies = await fetchMoviesByType('science-fiction');
this.setState({
movies,
});
}
render() {
return <MovieList movies={this.state.movies} />
}
}
export default PageA;
// pages/page-b.js
class PageB extends React.Component {
state = {
movies: [],
}
// ...
async componentWillMount() {
const movies = await fetchMoviesByType('action');
this.setState({
movies,
});
}
render() {
return <MovieList movies={this.state.movies} />
}
}
export default PageB;
页面少的时候可能没什么问题,但是假如随着业务的进展,需要上线的越来越多类型的电影,就会写很多的重复代码,所以我们需要重构一下:
const withFetching = fetching => WrappedComponent => {
return class extends React.Component {
state = {
data: [],
}
async componentWillMount() {
const data = await fetching();
this.setState({
data,
});
}
render() {
return <WrappedComponent data={this.state.data} {...this.props} />;
}
}
}
// pages/page-a.js
export default withFetching(fetching('science-fiction'))(MovieList);
// pages/page-b.js
export default withFetching(fetching('action'))(MovieList);
// pages/page-other.js
export default withFetching(fetching('some-other-type'))(MovieList);
会发现 withFetching
其实和前面的 withAuth
函数类似,把 变的部分(fetching(type)) 抽离到外部传入,从而实现页面的复用。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。