为什么会突然出现高阶组件的概念呢?我们使用普通组件出现的瓶颈是什么呢?
HOC
)这种模式来进行项目的设计。从本质上说,高阶组件并不是一个组件,而是一个函数,这个函数接受一个组件作为参数,在最后返回一个组件,我们得到的新的组件是经过这个函数加工过的
我在刚开始接触高阶组件的时候,以为这是一个很高级的概念,看上去是一个高级的先进编程语言的专业术语,然而事实并不是如此,这个词的缘由是因为js
中的高阶函数的概念
高阶函数就是接受函数作为输入或者输出的函数
我们写一个高阶组件的例子:
const withDoSomthing =(component)=>{ const NewComponent =(props) =>{ return <component {...props} /> } return NewComponent; } 复制代码
ps: 对于高阶组件来说,业界用 with
前缀来进行区分,命名的后面的部分来表示高阶组件真正的作用,高阶组件的基本特点我们进行一下总结
我们如何实现上面的高阶组件的内容呢?
不修改原始的组件
props
保持一致
保持可组合性
displayName
为了方便调试,最常见的高阶组件命名方式是将子组件名字包裹起来。
不要在render
方法内部使用高阶组件
render
中的高阶组件会在每次render
时重新mount
,之前组件内部的state
也会丢失。
react
的第三方库的时候,可以更加理解这样设计的原因Props Proxy
)Inheritance Inversion
)实质:包裹原来的组件来实现操作props
,举一个简单的例子
React
父子组件的生命周期是一样的,所以用这样的方法来实现高阶组件可能会影响到生命周期或是一些方法。import React,{Component} from 'React' // 高阶组件 const withHOC = (WrappedComponent) =>{ class WrapperComponent extends Component{ render(){ return (<WrappedComponent {...this.props}/>) } } } // 普通组件 class WrappedComponent extends Component { render(){ // ... 组件的内容 } } // 使用高阶组件 export default withHOC(WrappedComponent) 复制代码
上面的例子的作用就是传入了一个作为参数的组件(WrappedComponent
),现在的这个高阶组件将传入的组件,不做修改,直接传出。而且将高阶组件的props
传给了WrappedComponent
组件。这就是一个最简单的高阶组件用到的属性代理功能
对于高阶组件的返回,可以返回有状态,也可以返回无状态组件,在下面的例子中我们向WrappedComponent
组件中添加了固定的属性name
,因此WrappedComponent
组件多了一个name
的属性
// 无状态组件 const HOC = (WrappedComponent)=>{ const newProps = {name :"HOC"} return <WrappedComponent {...props} {...newProps}> } // 有状态组件 const HOC =(WrappedComponent)=> class wrapperComponent extends wrappedComponent { render(){ const newProps ={ name : 'HOC' } return <wrappedComponent {...this.props} {...newProps}> } } 复制代码
在代理属性中,我们可以直接拿到被包裹的组件的实例(ref
)
import React,{Component} from 'React' const HOC =(WrappedComponent)=> class WrapperComponent extends Component { storeRef(ref){ this.ref = ref; } render(){ return <WrappedComponent {...this.props} ref ={:: this.storeRef} /> } } 复制代码
当WrapperComponent
渲染接受后,我们就可以拿到WrappedComponent
组件的实例,进而实现调用实例方法的操作(这样的写法并不推荐)
我们将一个不受控的组件向受控组件的转换,我们的做法是将被包裹(WrappedComponent
)的组件状态提到包裹组件中(WrapperComponent
)中。
class WrappedComponent extends Component{ render(){ return <Input name ="name" {...this.props}> } } const HOC =(WrappedComponent)=>{ class WrapperComponent extends Component { constructor(props){ super(props); this.state ={ name:'' } this.onChangeName = this.onChangeName.bind(this) } onChangeName(event){ this.setState({ name : event.target.value }) } render(){ const newProps = { name :{ value:this.state.name, onChange:this.onChangeName }, } return <WrappedComponent {...this.props} {...newProps}> } } } 复制代码
我们可以通过类似:
render(){ <div> <WrappedComponent {...this.props}/> </div> } 复制代码
当我们想要实现一个统一样式的时候,我么可以使用一个div
来进行包裹
我们从生命周期的角度进行分析,我们在组件渲染阶段,先渲染WrappedComponent
在渲染WrapperComponent
组件(componentDidMount
),而在卸载的时候,我们先卸载WrapperComponent
再卸载 WrappedComponent
的时候(ComponentWillUnmount
)
反向继承就是说返回的组件去继承之前的组件
const HOC = (WrappedComponent)=> class extends WrappedComponent{ render(){ return super.render() } } 复制代码
我们返回的组件确实是继承WrappedComponent
,所有的调用将是反向调用的,所以这样的方式叫做反向继承。
我们可以根据自己的想法来控制WrappedComponent
的渲染过程,从而控制渲染的结果,我们可以通过传参数决定是否来渲染组件
const HOC =(WrappedComponent)=> class extends WrappedComponent{ render(){ if(this.props.isRender){ return super.render() }else{ return null } } } 复制代码
我们可以通过修改参数来改变render
的结果,比如说我们对组件内的值进行修改,原来应该显示hello
后来显示word
const HOC =(WrappedComponent)=> class extends WrappedComponent{ render(){ const elements = super.render(); let newProps ={} if(elements && elements.Type === 'input'){ newProps ={value:"word"} } const props =Object.assign({},elements.props,newProps) const newElements = React.cloneElement(elements,props, elements.props.children) return newElements } } const WrappedComponent extends Component { render(){ return( <input value ="hello"> ) } } export default HOC(WrappedComponent) 复制代码
在上面的例子中我们得到的结果是input
框中显示的是word
当我们又需要的时候我们可以读取props
和state
的值,甚至是修改,和删除这些值
import React,{Component} from 'react' const HOCFactory = (...params) =>{ return (WrappedComponent) =>{ return class HOC extends Component { render(){ return <WrappedComponent {...this.props} /> } } } } HOCFactory(param)(WrappedComponent) 复制代码
假设有一个简单的组件Student
,又name
和age
两个通过props
传入后初始化state
,一个年龄输入框,一个点击后就会聚焦在input
框的button
和一个静态方法
import React,{Component} from 'react' class Student extends Component{ static staticFunction(){ console.log("哇卡卡卡") } constructor(props){ super(props); console.log("构建器") this.focus = this.focus.bind(this) } componentWillMount(){ console.log("组件将要构造") this.state({ name:this.props.name, age:this.props.age }); } componentDidMound(){ console.log("构建完成了") } componentWillReceiveProps(nextProps){ console.log("组件接受的属性发生改变了") console.log(nextProps) } focus(){ this.inputElemenr.focus(); } render(){ return( <p>姓名:{this.state.name}</p> <p> 年龄: <input value ={this.state.age} ref ={(input)=>{this.inputElement = input}}/> </p> <p> <button value ="点一点" onClick ={this.focus}/> </p> ) } } 复制代码
const HOC =(WrappedComponent) =>{ const newProps ={ name :"kim" } return props => < WrappedComponent {...props} {...newProps}/> } 复制代码
无状态组件是没有自己的生命周期和state
,这样的方式常常用于对组件的props
进行简单的统一处理
可以
props
不可以
有争议
能否操作并获取到state
可以通过props
和回调函数对state
进行操作
能否通过ref
访问到原组件中的dom
元素
因为无状态组件是没有实例,所以ref
,this
都是无法访问的,但是可以控制子组件的ref回掉函数来访问子组件的ref
能否渲染劫持
可以通过props
来控制是否渲染以及传入数据,但对WrappedComponent
内部的render
的控制并不强
ref的相关访问,我们用上面的例子进行扩展,高阶组件:
const HOC =(WrappedComponent)=>{ let inputElement = null; const handleClick =()=>{ inputElement.focus(); } const wrappedComponentStaic =()=>{ WrappedComponent.staticFunction() } return props =>( <div> <WrappedComponent inputRef ={(el)=>inputElement = el} {...props} /> <button onClick={handleClick}>focus子组件input</button> <button onClick={wrappedComponentStaic}>调用子组件static</button> </div> ) } const WrapperComponent = EnhanceWrapper(Student); 复制代码
当子组件需要传入父组件传入的ref回调函数
<input ref ={(input)=>{ this.inputElement = input }} /> 复制代码
修改成
<input ref ={(input)=>{ this.inputElement = input this.props.inputRef(input); }} /> 复制代码
const HOC =(WrappedComponent)=> { return class WrappedComponent extends React.Component { render() { return <WrappedComponent {...this.props} />; } } } 复制代码
可以
props
不可以
有争议
能否操作并获取到state
可以通过props
和回调函数对state
进行操作
能否通过ref
访问到原组件中的dom
元素
ref
无法通过this
来直接的访问,但是依然可以根据上面用到的回调函数来访问
能否劫持原组件生命周期
高阶组件和原组件的生命周期完全是React
父子组件的生命周期关系
能否渲染劫持
可以通过props来控制是否渲染以及传入数据,但对WrappedComponent
内部的render
的控制并不强
const HOC =(WrappedComponent)=>{ return class WrapperComponent extends Component{ static wrappedComponentStaic(){ console.log("调用静态方法") } constructor(props) { super(props); console.log("构造器"); this.handleClick = this.handleClick.bind(this); } componentWillMount(){ console.log("组件将要构造") }; componentDidMound(){ console.log("构建完成了") } handleClick(){ this.inputElement.focus(); } render(){ return( <div> <WrappedComponent inputRef ={(el)=>inputElement = el} {...this.props} /> <button onClick={this.handleClick}>focus子组件input</button> <button onClick={this.constructor.wrappedComponentStaic}>调用子组件static</button> </div> ) } } return props =>( <div> <WrappedComponent inputRef ={(el)=>inputElement = el} {...props} /> <button onClick={handleClick}>focus子组件input</button> <button onClick={wrappedComponentStaic}>调用子组件static</button> </div> ) } const WrapperComponent = EnhanceWrapper(Student); 复制代码
const HOC =(WrappedComponent)=> { return class WrappedComponent extends WrappedComponent { render() { return super.render(); } } } 复制代码
也就是反向继承,这个方法最大的特点就是可以 可以
props
state
ref
访问到原组件中的dom
元素static
方法const HOC = (WrappedComponent) =>{ return class WrapperComponent extends WrappedComponent { constructor(props){ super(props) console.log("构造器") this.handleCLick = this.handleClick.bind(this) } handleClick (){ this.inputElement.focus(); } render(){ return( <div> {super.render()} <button onClick={this.handleClick}>focus子组件input</button> <button onClick={WrapperComponent.wrappedComponentStaic}>调用子组件static</button> </div> ) } } } 复制代码
项目中的两个ui
完全是一致的,但是犹豫业务的不同,所以数据源和部分的文案不一样,如果我们全部重写,那样代码会有很多重复的部分,所以这个时候我们可以用到高阶组件进行封装
我们的做法就是将获取数据的函数作为参数一样传递,然后返回高阶组件
import React,{Component} from 'react' class ShopList extends Component{ componentWillMount(){ // 内容 } render(){ // 使用props中的data来进行渲染 } } 复制代码
// 使用上面的普通组件 const shopListFetch (fetchData,defaultProps) =>{ return class extends Component{ constructor(props){ super(props) this.state ={ data:[] } } async componentWillMont(){ const data = await fetch(); this.setState({ data:data }) } render(){ return <ShopList data = {this.state.data} {...defaultProps} {...this.props}> } } } export default shopListFetch 复制代码
当组件真正的调用的时候 ,不同的组件只要修改不同的参数就可以
// 获取后端的数据的处理函数 getShopListFirst const defaultProps ={ emptyMessage :'暂无数据' } const FirstShop = shopListFetch(getShopListFirst,defaultProps) 复制代码
在业务中主要体现就是白名单的功能,在白名单的用户可以看到,但是不在白名单的用户是看不到这部分的功能,也不展示业务数据,一周后会去掉白名单,所有的功能对所有的用户开发,而且这个白名单影响多个页面 我们期望在后续维护中对功能的改动最小,影响也是最小的
最简单最直白的方法就是在代码中直接加判断,如果在白名单内就显示真正的业务代码,但是这样的话,就会有一个问题,就是在后续删除的时候,很多页面每一个页面都要删除,没有做到耦合
页面一
import React,{Component} from 'react'; class PageFirst extends Component { componentDidMount() { // 获取业务数据 } render() { // 页面渲染 } } export default PageFirst 复制代码
页面二
import React,{Component} from 'react'; class PageSecond extends Component { componentDidMount() { // 获取业务数据 } render() { // 页面渲染 } } export default PageSecond 复制代码
我们的想法就是在顶层进行封装
const AuthWrapper =(WrappedComponent)=>{ class AuthWrappedComponent extends Component{ construcot(props){ super(props); this.state={ permissionDenied = -1 } } async componentDidMount(){ const permissionDenied = await whiteList(); this.setState({ permissionDenied }) } render(){ const {permissionDenied} = this.state if(!permissionDenied){ return (<>功能即将上线,敬请期待</>) } return <WrappedComponent {...this.props} />; } } } } 复制代码
业务代码没有变,符合开闭原则。鉴权与业务完全解耦,也避免鉴权失败情况下多余的数据请求,只需要增加/删除一行代码,改动一行代码,即可增加/去除白名单的控制。
所有使用React
的前端项目页面需要增加PV
,UV
,性能打点。每个项目的不同页面顶层组件生命周期中分别增加打点代码无疑会产生大量重复代码。
React 高阶组件(HOC)入门指南
React高阶组件实践