聊聊React高阶组件(Higher-Order Components)
2017-09-19 19:06
387 查看
使用
高阶部件是一种用于复用组件逻辑的高级技术,它并不是 React API的一部分,而是从
大概意思就是说,
代码复用,逻辑抽象,抽离底层准备(
渲染劫持
此文件导出了一个函数,此函数返回经过一个经过处理的组件,它接受一个参数
此高阶组件的简单使用如下:
想要使用高阶组件,首先
最后,导出的是被高阶组件处理过的组件
这样,就完成了一个普通组件的包装,可以在页面上将被包装过的组件显示出来了:
页面显示如下:
可以使用
可以看出,组件
另外需要注意的一点,
现在的 DOM结构:
可以看到,原先的
这里的
想做到这一点也很简单,那就是再为
使用方式如下:
然后在页面上就可以看到效果了:
更改
对
通过
普通组件如果带有一个
使用:
反向继承(
相比于前面使用
反向继承高阶组件的功能:
能够对普通组件生命周期内的所有钩子函数进行覆写
对普通组件的
上述代码中,让
并且覆写了
在这个方法中对
在
在
并且在原有的基础上,又进行了一些额外的操作(6)
另外,如果普通组件并没有显性实现某个钩子函数,然后在
使用:
页面效果:
可以看出,
用过
只要
至于,
因而,如果你想在
当然,如果你非要像这样获取到数据,也是可以的,但肯定要多费些手脚,一般在
代码其实也不是太多,但如果每次想要在一个组件获取
一种解决方法就是将
而另外一种方法,就是要用到本文所说的
既然高阶组件能够代理到 普通组件的
相比于使用
理论上可行,但无图无代码,嘴上说说可没用,我特地实验了一番,已用实践证实了其可行性。
一种封装
然后,普通组件被此
不要在
例如,以下就是错误示范:
静态方法必须复制
为了解决这个问题,在返回之前,可以向容器组件中复制原有的静态方法:
或者使用 hoist-non-react-statics来自动复制这些静态方法
想要解决这个问题,首先是尽量避免使用
如果你喜欢我们的文章,关注我们的公众号和我们互动吧。
react已经有不短的时间了,最近看到关于
react高阶组件的一篇文章,看了之后顿时眼前一亮,对于我这种还在新手村晃荡、一切朝着打怪升级看齐的小喽啰来说,像这种难度不是太高同时门槛也不是那么低的东西如今可不多见了啊,是个不可多得的
zhuangbility的利器,自然不可轻易错过,遂深入了解了一番。
概述
高阶组件的定义
React 官网上对高阶组件的定义:高阶部件是一种用于复用组件逻辑的高级技术,它并不是 React API的一部分,而是从
React演化而来的一种模式。 具体地说,高阶组件就是一个接收一个组件并返回另外一个新组件的函数。 相比于普通组件将
props转化成界面UI,高阶组件将一个普通组件转化为另外一个组件。
大概意思就是说,
HOC并不是
react
API的一部分,而是一种实现的模式,有点类似于
观察者模式、
单例模式之类的东西,本质还是函数。
功能
既然是能够拿来zhuangbility的利器,那么不管怎么说,简单实用的招式必不可少,可以利用高阶组件来做的事情:
代码复用,逻辑抽象,抽离底层准备(
bootstrap)代码
Props更改
State抽象和更改
渲染劫持
用法示例
基本用法
一个最简单的高阶组件(HOC) 示例如下:
1 // HOCComponent.js 2 3 import React from 'react' 4 5 export default PackagedComponent => 6 class HOC extends React.Component { 7 render() { 8 return ( 9 <div id="HOCWrapper"> 10 <header> 11 <h1>Title</h1> 12 </header> 13 <PackagedComponent/> 14 </div> 15 ) 16 } 17 }
此文件导出了一个函数,此函数返回经过一个经过处理的组件,它接受一个参数
PackagedComponent,此参数就是将要被
HOC包装的普通组件,接受一个普通组件,返回另外一个新的组件,很符合高阶组件的定义。
此高阶组件的简单使用如下:
1 // main.js 2 import React from 'react' 3 // (1) 4 import HOCComponent from './HOCComponent' 5 6 // (2) 7 @HOCComponent 8 class Main extends React.Component { 9 render() { 10 return( 11 <main> 12 <p>main content</p> 13 </main> 14 ) 15 } 16 } 17 // (2) 18 // 也可以将上面的 @HOCComponent换成下面这句 19 // const MainComponent = HOCComponent(Main) 20 export default MainComponent
想要使用高阶组件,首先
(1)将高阶组件导入,然后
(2)使用此组件包装需要被包装的普通组件
Main,这里的
@符号是
ES7中的
decorator,写过
Java或者其他静态语言的同学应该并不陌生,这实际上就是一个语法糖,可以使用 react-decorators 进行转换, 在这里相当于下面这句代码:
const MainComponent = HOCComponent(Main)
@HOCComponent完全可以换成上面那句,只不过需要注意的是,类不具有提升的能力,所以若是觉得上面那句顺眼换一下,那么在换过之后,还要将这一句的位置移到类
Main定义的后面。
最后,导出的是被高阶组件处理过的组件
MainComponent
这样,就完成了一个普通组件的包装,可以在页面上将被包装过的组件显示出来了:
1 import React from 'react' 2 import { render } from 'react-dom' 3 4 // 导入组件 5 import MainComponent from './main' 6 7 render( 8 <MainComponent/>, 9 document.getElementById('root') 10 )
页面显示如下:
可以使用
React Developer Tools查看页面结构:
可以看出,组件
Main的外面包装了一层
HOC,有点类似于父组件和子组件,但很显然高阶组件并不等于父组件。
另外需要注意的一点,
HOC这个高阶组件,我们可能会用到不止一次,功能技术上没什么关系,但是不利于调试,为了快速地区分出某个普通组件的所属的
HOC到底是哪一个,我们可以给这些
HOC进行命名:
1 // 获取传入的被包装的组件名称,以便为 HOC 进行命名 2 let getDisplayName = component => { 3 return component.displayName || component.name || 'Component' 4 } 5 6 export default PackagedComponent => 7 class HOC extends React.Component { 8 // 这里的 displayName就指的是 HOC的显示名称,我们将它重新定义了一遍 9 // static被 stage-0 stage-1 和 stage-2所支持 10 static displayName = `HOC(${getDisplayName(PackagedComponent)})` 11 render() { 12 return ( 13 <div id="HOCWrapper"> 14 <header> 15 <h1>Title</h1> 16 </header> 17 <PackagedComponent/> 18 </div> 19 ) 20 } 21 }
现在的 DOM结构:
可以看到,原先的
HOC已经变成了
HOC(Main)了,这么做主要是利于我们的调试开发。
这里的
HOC,可以看做是一个简单的为普通组件增加
Title的高阶组件,但是很明显并不是所有的页面都只使用同一个标题,标题必须要可定制化才符合实际情况。
想做到这一点也很简单,那就是再为
HOC组件的高阶函数增加一个
title参数,另外考虑到
柯里化 Curry函数和函数式编程,我们修改后的
HOC代码如下:
1 // HOCComponent.js 2 3 // 增加了一个函数,这个函数存在一个参数,此参数就是要传入的`title` 4 export default PackagedComponent => componentTitle => 5 class HOC extends React.Component { 6 static displayName = `HOC(${getDisplayName(PackagedComponent)})` 7 render() { 8 return ( 9 <div id="HOCWrapper"> 10 <header> 11 <h1>{ componentTitle ? componentTitle : 'Title' }</h1> 12 </header> 13 <PackagedComponent/> 14 </div> 15 ) 16 } 17 }
使用方式如下:
1 // main.js 2 3 // ...省略代码 4 const MainComponent = HOCComponent(Main)('首页') 5 export default MainComponent
然后在页面上就可以看到效果了:
属性代理
HOC是包裹在普通组件外面的一层高阶函数,任何要传入普通组件内的
props或者
state首先都要经过
HOC。
props和
state等属性原本是要流向 目标组件的腰包的,但是却被 雁过拔毛的
HOC拦路打劫,那么最终这些
props和
states数据到底还能不能再到达 目标组件,或者哪些能到达以及到达多少就全由
HOC说了算了,也就是说,
HOC拥有了提前对这些属性进行修改的能力。
更改 Props
对 Props的更改操作包括 增、删、改、查,在修改和删除
Props的时候需要注意,除非特殊要求,否则最好不要影响到原本传递给普通组件的
Props
1 class HOC extends React.Component { 2 static displayName = `HOC(${getDisplayName(PackagedComponent)})` 3 render() { 4 // 向普通组件增加了一个新的 `Props` 5 const newProps = { 6 summary: '这是内容' 7 } 8 return ( 9 <div id="HOCWrapper"> 10 <header> 11 <h1>{ componentTitle ? componentTitle : 'Title' }</h1> 12 </header> 13 <PackagedComponent {...this.props} {...newProps}/> 14 </div> 15 ) 16 } 17 }
通过 refs
获取组件实例
普通组件如果带有一个 ref属性,当其通过
HOC的处理后,已经无法通过类似
this.refs.component的形式获取到这个普通组件了,只会得到一个被处理之后的组件,想要仍然获得原先的普通组件,需要对
ref进行处理,一种处理方法类似于
react-readux中的
connect方法,如下:
1 // HOCComponnet.js 2 ... 3 export default PackagedComponent => componentTitle => 4 class HOC extends React.Component { 5 static displayName = `HOC(${getDisplayName(PackagedComponent)})` 6 // 回调方法,当被包装组件渲染完毕后,调用被包装组件的 changeColor 方法 7 propc(wrapperComponentInstance) { 8 wrapperComponentInstance.changeColor() 9 } 10 render() { 11 // 改变 props,使用 ref 获取被包装组件的示例,以调用其中的方法 12 const props = Object.assign({}, this.props, {ref: this.propc.bind(this)}) 13 return ( 14 <div id="HOCWrapper"> 15 <header> 16 <h1>{ componentTitle ? componentTitle : 'Title' }</h1> 17 </header> 18 <PackagedComponent {...props}/> 19 </div> 20 ) 21 } 22 }
使用:
1 // main.js 2 ... 3 class Main extends React.Component { 4 render() { 5 return( 6 <main> 7 <p>main content</p> 8 <span>{ this.props.summary }</span> 9 </main> 10 ) 11 } 12 // main.js 中的changeColor 方法 13 changeColor() { 14 console.log(666); 15 document.querySelector('p').style.color = 'greenyellow' 16 } 17 } 18 ...
反向继承(Inheritance Inversion
)
相比于前面使用 HOC包装在 普通组件外面的情况,反向继承就是让
HOC继承普通组件、打入普通组件的内部,这种更厉害,前面还只是拦路打劫,到了这里就变成暗中潜伏了,这种情况下,普通组件变成了基类,而
HOC变成了子类,子类能够获得父类所有公开的方法和字段。
反向继承高阶组件的功能:
能够对普通组件生命周期内的所有钩子函数进行覆写
对普通组件的
state进行增删改查的操作。
1 // HOCInheritance.js 2 3 let getDisplayName = (component)=> { 4 return component.displayName || component.name || 'Component' 5 } 6 7 // (1) 8 export default WrapperComponent => 9 class Inheritance extends WrapperComponent { 10 static displayName = `Inheritance(${getDisplayName(WrapperComponent)})` 11 // (2) 12 componentWillMount() { 13 this.state.name = 'zhangsan' 14 this.state.age = 18 15 } 16 render() { 17 // (4) 18 return super.render() 19 } 20 componentDidMount() { 21 // 5 22 super.componentDidMount() 23 // 6 24 document.querySelector('h1').style.color = 'indianred' 25 } 26 }
上述代码中,让
Inheritance继承
WrapperComponent(1)
并且覆写了
WrapperComponent中的
componentWillMount函数(2)
在这个方法中对
WrapperComponent的
state进行操作(3)
在
render方法中,为了防止破坏
WrapperComponent原有的
render()方法,使用
super将
WrapperComponent中原有的
render方法实现了一次(4)
在
componentDidMount同样是先将
WrapperComponent中的
componentDidMount方法实现了一次(5)
并且在原有的基础上,又进行了一些额外的操作(6)
super并不是必须使用,这取决于你是否需要实现普通组件中原有的对应函数,一般来说都是需要的,类似于
mixin,至于到底是原有钩子函数中的代码先执行,还是
HOC中另加的代码先执行,则取决于
super的位置,如果
super在新增代码之上,则原有代码先执行,反之亦然。
另外,如果普通组件并没有显性实现某个钩子函数,然后在
HOC中又添加了这个钩子函数,则
super不可用,因为并没有什么可以
super的,否则将报错。
使用:
1 // main2.js 2 3 import React from 'react' 4 import Inheritance from './HOCInheritance' 5 6 class Main2 extends React.Component { 7 state = { 8 name: 'wanger' 9 } 10 render() { 11 return ( 12 <main> 13 <h1>summary of </h1> 14 <p> 15 my name is {this.state.name}, 16 I'm {this.state.age} 17 </p> 18 </main> 19 ) 20 } 21 22 componentDidMount() { 23 document.querySelector('h1').innerHTML += this.state.name 24 } 25 } 26 27 const InheritanceInstace = Inheritance(Main2) 28 export default InheritanceInstace
页面效果:
可以看出,
HOC为原有组件添加了
componentWillMount函数,在其中覆盖了
Main2中
state的 'name'属性,并且其上添加了一个
age属性
HOC还将
Main的
componentDidMount方法实现了一次,并且在此基础上,实现了自己的
componentDidMount方法。
用法拓展
HOC的用处很多,例如代替简单的父组件传递
props,修改组件的
props数据等,除此之外,基于以上内容,我还想到了另外一种让
HOC配合
redux的使用技巧。
用过
vue与
vuex的人都知道,这两个可谓是天作之合的一对好基友,后者基本上就是为前者量身定做,贴心的很,几乎不用多做什么事情,就能在
vue的任何组件中获取存储在
vuex中的数据,例如:
this.$store.state.data
只要
vuex中存储了
data这个值,那么一般情况下,在
vue的任何组件中,都是可以通过上面的一行代码获取到
data的。
至于,
react和
redux,看起来似乎和
vue与
vuex之间的关系差不多,用起来似乎也是二者搭配干活不累,
but,实际上他们之间的关系并没有那么铁。
redux能够搭配的东西不仅是
react,还有
jquery、
vue、
Angular、
Ember等任意框架,原生
js也
ok,颇有种搭天搭地搭空气的倾向,所以,其与
react之间肯定不可能像
vue与
vuex那么融洽和谐。
因而,如果你想在
react中像在
vue中那么毫不费力地通过类似于以下代码在任意
react组件中获取到
redux中的数据,那么我只能说,你大概又写了个
bug
this.$store.state.data
当然,如果你非要像这样获取到数据,也是可以的,但肯定要多费些手脚,一般在
react中获取
redux中数据的方法都要像这样:
1 // 首先,导入相关文件 2 import { bindActionCreators } from 'redux' 3 import { connect } from 'react-redux' 4 import * as commonInfoActionsFromOtherFile from 'actions/commoninfo.js' 5 6 // ... 7 8 // 然后,传递数据和方法 9 10 let mapStateToProps = (state)=>{ 11 return { 12 commonInfo: state.commonInfo 13 } 14 } 15 16 let mapDispatchToProps = (dispatch)=>{ 17 return { 18 commonInfoActions: bindActionCreators(commonInfoActionsFromOtherFile, dispatch) 19 } 20 } 21 // 最终,将组件导出 22 export default connect( 23 mapStateToProps, 24 mapDispatchToProps 25 )(ExampleComponent)
代码其实也不是太多,但如果每次想要在一个组件获取
redux中的数据和方法都要将这段代码写一遍,实在是有些啰嗦。
一种解决方法就是将
redux中所有的数据和
dispatch方法全都暴露给根组件,让根组件往下传递到所有的子组件中,这确实是一种方法,但似乎有些冗余了,
redux中的数据暴露在项目所有组件中,但有些组件根本用不到
redux中的数据,干嘛还非要塞给它?
而另外一种方法,就是要用到本文所说的
HOC了。
既然高阶组件能够代理到 普通组件的
Props和
state等属性,那么在使用诸如
redux等库的时候,是不是可以让高阶组件来承接这些由
redux传递到全局的属性,然后再用高阶组件包装普通组件,将获得的属性传递给普通组件,这样普通组件就能获取到 这些全局属性了。
相比于使用
redux一个个地初始化所有需要使用到全局属性的组件,使用高阶组件作为载体,虽然结构上多了一层,但是操作上明显方便简化了许多。
理论上可行,但无图无代码,嘴上说说可没用,我特地实验了一番,已用实践证实了其可行性。
一种封装
HOC,让其承载
redux的示例代码如下:
1 // HocRedux.js 2 3 import { bindActionCreators } from 'redux' 4 import { connect } from 'react-redux' 5 import * as actionsLists from '../actions/actionsLists' 6 7 let getDisplayName = component=> { 8 return component.displayName || component.name || 'Component' 9 } 10 11 let mapStateToProps = (state)=>{ 12 return { 13 reduxState: state 14 } 15 } 16 let mapDispatchToProps = (dispatch)=>{ 17 return { 18 reduxActions: bindActionCreators(actionsLists, dispatch) 19 } 20 } 21 22 export default ChildComponent => 23 connect( 24 mapStateToProps, 25 mapDispatchToProps 26 )(class HocInheritance extends ChildComponent { 27 static displayName = `HocInheritance(${getDisplayName(ChildComponent)})` 28 })
然后,普通组件被此
HOC处理后,就可以轻松获取
redux中的数据了,想让哪个组件获取
redux,哪个组件就能获取到,不想获取的就获取不到,简单明了,使用方法和上面一样:
1 import HocRedux from 'HocRedux' 2 // 省略代码 3 const InheritanceInstace = Inheritance(Main2) 4 export default InheritanceInstace
注意事项
react官网 上还给出了几条关于使用HOC时的注意事项。
不要在
render函数中使用高阶组件
例如,以下就是错误示范:
1 // 这是个 render 方法 2 render() { 3 // 在 render 方法中使用了 HOC 4 // 每一次render函数调用都会创建一个新的EnhancedComponent实例 5 // EnhancedComponent1 !== EnhancedComponent2 6 const EnhancedComponent = enhance(MyComponent); 7 // 每一次都会使子对象树完全被卸载或移除 8 return <EnhancedComponent />; 9 }
静态方法必须复制
HOC虽然可以自动获得 普通组件的
props和
state等属性,但静态方法必须要手动挂载。
1 // 定义静态方法 2 WrappedComponent.staticMethod = function() {/*...*/} 3 // 使用高阶组件 4 const EnhancedComponent = enhance(WrappedComponent); 5 6 // 增强型组件没有静态方法 7 typeof EnhancedComponent.staticMethod === 'undefined' // true
为了解决这个问题,在返回之前,可以向容器组件中复制原有的静态方法:
1 function enhance(WrappedComponent) { 2 class Enhance extends React.Component {/*...*/} 3 // 必须得知道要拷贝的方法 4 Enhance.staticMethod = WrappedComponent.staticMethod; 5 return Enhance; 6 }
或者使用 hoist-non-react-statics来自动复制这些静态方法
Refs不会被传递 对于
react组件来说,
ref其实不是一个属性,就像
key一样,尽管向其他
props一样传递到了组件中,但实际上在组件内时获取不到的,它是由
React特殊处理的。如果你给高阶组件产生的组件的元素添加
ref,
ref引用的是外层的容器组件的实例,而不是被包裹的组件。
想要解决这个问题,首先是尽量避免使用
ref,如果避免不了,那么可以参照本文上面提到过的方法。
如果你喜欢我们的文章,关注我们的公众号和我们互动吧。
相关文章推荐
- 助你完全理解React高阶组件(Higher-Order Components)
- React 高阶组件(Higher-Order Components)
- 助你完全理解React高阶组件(Higher-Order Components)
- React:组件的生命周期
- React 组件的三种写法总结
- react-native 组件封装示例
- 在react组件中监控滚动
- ReactNative--组件的声明周期
- react-native-0.16.1 自定义Android组件部分的源码初探
- React高阶组件原理与在Redux中的实践
- React高阶组件
- react-native-smart-barcode目前最好用的二维码扫描组件(IOS、android)
- React 组件生命周期
- 利用angular、react和vue实现相同的面试题组件
- 深入理解React中es6创建组件this的方法
- 使用ES6语法重构React组件
- React Native填坑之旅--Stateless组件
- React组件定义验证属性值和默认值的配置
- React-Native学习十九:组件之间的通信-1
- React-Component(组件)