React 精要面试题讲解(五) 高阶组件真解
说明与目录
在学习本章内容之前,最好是具备react中‘插槽(children)’及‘组合与继承’ 这两点的知识积累。
详情请参照React 精要面试题讲解(四) 组合与继承不得不说的秘密。
哦不好意思忘记了,四还没写呢。==!回头补上。
__首先,我们要知道高阶组件能够做到什么: 对复用UI、数据逻辑等进行封装,对参数组件进行制式处理,从而让参数组建具备特定的ui或功能__
那么本节的学习目录:
- 高阶函数的认知
- 类的修饰器(decorator)的认知(类比高阶函数)
- 高阶组件的认知(类比修饰器)
- 高阶组件的两种形式(类比插槽)
- 高阶组件之组合(代理)类型的高阶组件
- 高阶组件之继承类型的高阶组件
-
高阶组件的认知总结
必须深刻认知,以上内容是循序渐进且相互关联的关系,按照流程,我们将彻底把高阶组件安排的明明白白,玩个透彻。
1. 高阶函数的认知
在本系列之前的学习中,你应当明白——组件的本质是函数。
那么什么是高阶函数?
高阶函数也是一个函数,__它接受函数为参数,返回处理过后的函数或者一个新函数__。
那么我们知道,高阶函数的作用是对参数函数进行统一的加工处理。
形如:
//这里我们写了一个函数作为之后的参数
//思考一下为什么把参数函数定义在上面
function testedFunc(){
this.a = 1;
}
// 在此定义一个高阶函数
function highOrderFunc(func){
func.prototype.aaa = 3;
return func;
}
//这里测试一下我们写的高阶函数
var newFunc = highOrderFunc(testedFunc);
//打印看看处理后的函数原型及实例
console.log(newFunc.prototype,1);
console.log(new newFunc(),2);
(别那么懒,赶紧复制粘贴f12)
打印结果如下:
那么我们知道了,高阶函数 作用是 处理(加工)传入的函数,以达成某种目的…
2. 类的修饰器(decorator)的认知
ES6增添了class类,我们知道,类的本质也是函数。
class testClass{
a = 1;
}
console.log( typeof testClass)
打印结果如下:
那么类的修饰器——decorator 是个怎样的东西咧?
__类的修饰器是es6的提案之一,在es7标准中实现。 修饰器也是一个函数,把传入的原有类修饰一下,return 出处理后的类或新类。__
这时候我们脑海中应该闪过一个词——高阶类…(???黑人问号)
不觉得太难听了吗?
ok,我们还是以代码来演示一下:
//注意,下面这种@xx的写法是修饰器的标准写法,是属于es7的产物,要用babel哦~
//定义一个修饰器decF
function decF(adornedClass){
return class extends adornedClass{
b= 2
}
}
// 使用方式1 : @decF
@decF
class foo{
a = 1
}
console.log( new foo(),1)
class bar{
a='bar'
}
// 使用方式2 : decF();
const newBar = decF(bar);
console.log( new newBar(),2);
打印如下:
瞧啊,修饰器就是这么个东西。
要注意的是,类的修饰器能否修饰函数?为什么?
// 可以自己去找答案,再讲别的就跑题了。
___
3. 高阶组件的认知(类比修饰器)
那么经过类的修饰器的认知,高阶组件的概念就很明朗了。
React高阶组件(high-order-component,简称hoc)就是一个类的修饰器啊…它接受一个组件类为参数,返回出一个新的组件类;
形如:
// 用法1 高阶函数式写法
hoc(WrapedComponent);
// 用法2 decorator 修饰器写法
@hoc
class A extends React.Component{
//...
}
大家有木有很眼熟啊 ?
ok, 我们写一个常规的高阶组件并暴露出去;
export default WrapedComponent => class NewComponent extends React.Component{
render(){
return <WrapedComponent a = '添加了一个属性' />
}
}
这种箭头函数的写法好理解吧。
如代码所示,我们写了一个高阶组件,返回的新组件 NewComponent里,用组合的形式使用了传入的组件WrapedComponent。(所以不明白组合与继承的童鞋,赶紧补一补再来看啊)
这里有人问 connect为啥两个括号啊
//形如
connect(func1,func2 )( WrapedComponent)
OK ,我们也手写一下它。
export default (func1,func2)=>WrapedComponent=>class NewComponent extends React.Component{
// ....
}
这个两层箭头函数好理解吧?
顺便说下,一般像connect这样多嵌套了一层的高阶函数,我称之为二阶高阶函数。此后类推,三个括号就叫三阶高阶函数…
4. 高阶组件的两种形式(类比插槽)
同children 一样,高阶组件也存在组合(也可称之为代理)和继承两种形式。
那么高阶组件和children插槽有什么关系呢?
我们来类比以下代码:
//写一个组合形式的具备插槽功能的组件Provider
class Provider extends React.Component{
render(){
return (
<div>
{this.props.children}
</div>
)
}
}
// 下面是使用的方式
import ComA from './ComA'
const useB = (<Provider > <ComA a='使用Provider时添加了a属性'/> </Provider >)
// 写一个组合形式的高阶组件
const hocAddA = WrapedComponent => class NewComponent extends React.component{
render(){
return (
<div>
<WrapedComponent a ='定义hocAddA时添加了a属性'/>
</div>
)
}
}
// 我们来尝试使用它
const NewA = hocAddA(ComA);
// 或者
@hocAddA
class ComB ...
ok, 上述两种代码在使用后的表现几乎是一致的(同样实现了给ComA添加属性a的功能)。
但是注意,插槽(children)的实现,不关心插槽组件的功能变化。只是把插槽当作当前组件的子组件去使用(这就是组合)。
//同样的,我现在这样使用
import ComA from './ComA'
const useB = (<Provider > <ComA b='使用Provider时添加了b属性'/> </Provider >)
而高阶函数,在定义时就写死了参数组件的功能变化。
传入组件,得出的组件只会添加属性a。
当然,我们也可以通过二阶高阶函数实现 用参数控制参数组件的功能变化:
定义一个二阶高阶组件
const hocAddProps = props => WrapedComponent => class NewComponent extends React.Component{
render(){
return (
<div>
<WrapedComponent {...props}/>
</div>
)
}
}
// 于是我们这样使用它
const propsAdded = {
a: '添加了一个属性a',
b: ‘添加了一个属性b'
}
const NewA = hocAddProps(propsAdded)(ComA)
诸如此类。实际上组合形式的高阶组件能做到的事,用children基本都能做到。
那么组合形式的高阶组件和继承形式的高阶组件的区别在哪呢?
组合形式(也称之为代理形式): 返回的新组件,继承的还是React.Component,只是把参数组件作为新组件的子组件去使用,能够实现给参数组件进行包装、属性的增删改、状态抽离等功能.
继承形式: 返回的新组件,继承的是 参数组件 ,从而实现以参数组件为模版,改写参数组件的功能。
上述划重点,要考。
我们再回过头来思考类的修饰器——返回一个新的类或改写参数类。
是不是一样的道理啊。
所以说高阶组件啥的,还是js啊,最多加了jsx的语法嘛。
5. 高阶组件之组合(代理)类型的高阶组件
上述我们已经知道了组合(代理)类型的高阶组件的概念和思想,以及它能实现的功能。
那么我们上demo代码
import React,{Component,createRef} from 'react';
export default title=>WrapedComponent=> class NewComponent extends Component{
//抽离状态
state={
value:''
}
// 访问refs
myref=createRef();
handleInputChange=(e)=>{
this.setState({
value:e.target.value
})
}
render(){
const {wap,...otherprops} = this.props;
const newProps = {
value:this.state.value,
onChange:this.handleInputChange
}
//包装组件
return (
<div>
我是组件NewComponent,是典型的代理形式的高阶组件,我除了做自己的事,还可以对 我的参数组件:
1增加/删减props 2抽离状态 3访问ref 4包装组件
<div>我的title:{title}</div>
<WrapedComponent {...otherprops} ref={this.myref} inputProps={newProps}/>
</div>
)
}
}
这里要单独说一下上述功能中的状态抽离。
状态抽离(状态提升): 把参数组件(即代理形式中使用的子组件)的状态提升到NewComponent(即代理形式中的当前组件,也就是父组件) 中,这样一来,子组件只负责UI渲染,而父组件通过props传递state实现数据的控制
也就是说, NewComponent 成为参数组件的容器组件,参数组建单纯作为UI组件
ps: 容器组件和UI组件的概念是相对的。 例如 把B的状态抽离到父组件A上,那么A相对于B来说是B的容器组件,要这么去理解。后续讲react-redux中会提到。
___
6. 高阶组件之继承类型的高阶组件
同样的,上述我们已经知道了 继承类型的高阶组件的概念和思想,那么我们也直接上demo代码
import React from 'react'
//这个是给返回的新组件起名用的函数,有兴趣可以结合调试器玩玩。
function getDisplayName(WrapedComponent){
return WrapedComponent.displayName||WrapedComponent.name||'component'
}
export default color=>WrapedComponent=> class NewComponent extends WrapedComponent{
// static displayName = `E(${getDisplayName(Inconponent)})`
;
aaa = '我改写了参数组件中的aaa属性'
compoenentDidMount(){
console.log('我不仅可以改写属性和方法,我还能改写钩子')
}
render(){
const {wap,...otherprops} = this.props;
const element = super.render();
console.log(element);
const newStyle = {
color:element.type==='div'?color:null
}
const newProps = {
...otherprops,
style:newStyle
}
// 我甚至还改写了参数组件的UI
return React.cloneElement(element,newProps,element.props.children)
}
}
如上述代码所示(跟着敲一下啊懒虫),我们成功做到了以参数组件为模版,改写了参数组件中已定义的属性、方法、钩子,甚至UI,增添了参数组件中未定义的属性、方法、钩子等。
当然,同官方文档中 ‘组合和继承’ 这一章中的思想一致,绝大部分情况下,我们用不到继承类型的高阶组件,也不提倡这种形式的用法(其实我个人觉得挺好玩的)。
7. 高阶组件的认知总结
那么我们通过以上学习,已经完完整整掌握了高阶组件的使用。
在日常项目中,我们也可以在合适的场景中使用高阶组件完成对应的需求。
回顾最上面提到过的高阶组件的使用场景:
__对复用UI、数据逻辑等进行封装,对参数组件进行制式处理,从而让参数组建具备特定的ui或功能__
再回顾下上述讲到过的高阶函数,类的修饰器等——
你get到了吗?
面试中会问到高阶组件的问题,消化掉这一篇,那么你便可以连续不断的给面试官讲上半个小时征服他。
最后,如果本章内容对你的react学习有帮助,记得点个关注,等待更新哦。