上一篇五六月份忙着自己的杂事。7月(7月31日还是7月),抽空整理了一下旧内容。之前的一些读者提到他们想了解更多关于高阶组件的知识。类组件的知识点刚刚梳理完毕。高阶组件HOC(HigherOrderComponent)听起来像是React的一个高级功能,但实际上它并不属于ReactAPI,而应该归类为一种使用技巧或设计模式。首先我们直奔本质:高阶组件就是一个函数,它是一个参数为组件,返回值为新组件的函数。更直截了当的一点是:Fn(component)=>一个功能更强大的新组件。这里,Fn是一个高级组件。Component是React中的一个基本单元,通常它会接受一些props属性,最终显示为一个特定的UI,但在某些场景下,传统的组件已经不足以解决问题。在这个分界线下,我们可以暂时放下枯燥的代码,说说生活中常见的一个场景——点奶茶。在如今的生活中,奶茶已经成为了很多人生活中的快乐调剂(毕竟生活已经这么苦了-_-),品种和口味也是五花八门。比如基本品类有纯茶、奶茶、咖啡、鲜榨果汁等等,还有各种配料,比如芝士、牛奶、果干、芋泥等等....(嗯,我我要先点杯奶茶,喝完再继续写)好吧,我回来了~所以现在你可以抽象出几种基本成分:纯茶、奶茶、果茶,它们都可以与以下添加物相匹配:添加奶酪和碎山核桃。不同基础茶添加的逻辑行为是相似的,所以可以将这两种添加方式设计成高Step的组件,这样可以很方便的根据需要生成不同类型的最终奶茶。套用前面的函数表达式就是:Fn(基础奶茶)=>不同口味的奶茶,其中Fn是加法函数。它的功能是通过添加成分将基础奶茶变成增强型奶茶。文到这里,相信大家已经对高阶函数的作用有了大概的了解,接下来进入正题(醒醒解闷)。从一个常见的场景出发,相信前端同学写过不少后端系统。自然少不了一些常用的功能,比如打印操作日志,权限控制等。以打印操作日志为例,需要实现如下需求:当进入某些页面时安装组件时,需要打印日志出来并发送到服务器。classPage1extendsReact.Component{componentDidMount(){//使用console.log模拟日志打印。其实一般都会发送到服务器保存console.log('enterpage1');}render(){return
page1
}}classPage2extendsReact.Component{componentDidMount(){console.log('enterpage2');}render(){return
page2
}}观察Page1和Page2的两个组件,有一部分逻辑类似:在componentDidMount阶段,需要console.log当前页面名称。现在把这部分逻辑移到一个函数中:functionwithLog(WrappedComponent,pageName){//这个函数接收一个组件作为参数returnclassextendsReact.Component{componentDidMount(){//使用console.log模拟实际日志打印通常会发送到服务器保存console.log(pageName);}render(){//这里注意this.props要继续透传return
;}}}至此,打印日志的逻辑就可以和具体组件的关联解耦了:classPage1extendsReact.Component{//不需要保留打印日志的逻辑render(){return
page1
}}classPage2extendsReact.Component{//不需要保留打印日志的逻辑render(){return
page2
}}//constPage1WithLog=withLog(Page1);constPage2WithLog=withLog(Page2);,实现了一个简单的高阶组件!高阶组件有什么作用?从上面的例子可以看出,高阶组件将传入的组件包装在一个容器组件中(容器组件就是withLog函数中返回的匿名组件),最后返回一个增强函数new组件。这里有一个很关键的点:不要修改原来的原装组件!请勿修改原装原件!请勿修改原装原件!熟悉React的同学会发现它处处贯彻了函数式编程的思想。同样,高阶组件必须是纯函数(相同的输入必须返回相同的结果),以保证组件的可重用性。高阶组件上面的组合使用介绍了使用单个高阶组件的情况,那么如果要同时使用多个高阶组件怎么办呢?继续前面的例子,我们来设计一个提供权限管理功能的高层组件:this.setState({hasPermission});}render(){//这里注意this.props要继续透明return({this.state.hasPermission?
:
你没有权限查看此页面,请联系管理员!
})}}}checkPermission函数会检查用户权限,以确定是否允许用户访问当前页面。接下来,我们要在前面的Page1组件中添加权限控制和日志打印函数://当然可以这样写//1.先添加Log函数constPage1WithLog=withLog(Page1,'pageName1');//2.添加CheckPermission函数constPage1WithLogAndPermission=withCheckPermission(Page1WithLog);其实可以直接用compose实现,这样在使用多个high-level的时候组件排序时可以更简洁://tips:compose(f,g,h)等价于(...args)=>f(g(h(...args)))import{compose}from'redux';constPage1WithLogAndPermission=compose(Page1WithLogAndPermission,(Component)=>withLog(Component,'pageName1'),);前面说过,高阶组件不会破坏被包裹的组件本身,所以很适合灵活使用多个组件,其实效果很像在原来组件的外层包裹不同的组件,然后使用命名以方便调试。由于高层组件会将组件包裹在WrapComponent的外层,在使用过程中,为了方便调试,非常有用。需要为每个高阶组件设置displayName属性,以前面的withLog为例:})`;componentDidMount(){//使用console.log模拟日志打印。其实一般都会发送到服务器保存console.log(pageName);}render(){//这里注意继续透传this.propsreturn
;}}}functiongetDisplayName(WrappedComponent){返回WrappedComponent.displayName||WrappedComponent.name||找到最终代码中的每一层组件。NotesNotes实际上,它们中的大多数与高级组件的实现性质有关。文章一直在强调,高阶组件的本质是:用一个新的组件包裹原有的WrappedComponent组件,并在新的组件上添加一些行为,那么wrap势必会带来一些关注点。注意传递props的意义传递props就不用多说了,除了一些高层组件本身需要的独占props外,其他的props应该继续返回给WrappedComponent,如下:functionwithSomeFeature(WrappedComponent){returnclassextendsReact.Component{//省略functionrender(){//这里注意extraProp只表示当前高阶函数使用的propsconst{extraProp,...passThroughProps}=this.props;//保留其余与你无关的props通过return
}}}不要在render中使用高级组件具体来说,不要使用这个:classTestextendsReact.Component{render(){constEnhancedComponent=enhance(MyComponent);返回<增强组件/>;}}在上面的代码中,每次执行render时,constEnhancedComponent=enhance(MyComponent);返回的是不同的新组件(因为组件解析到最后其实是一个对象,也就是一个引用Type值,所以每次定义都相当于重新生成一个对象),导致了状态的完全丢失组件及其所有子组件。所以正确的用法是用组件外的高层组件生成需要的新组件后直接使用新组件:constEnhancedComponent=enhance(MyComponent);类测试扩展React.Component{render(){return
;}}拷贝静态方法也是wrapping带来的问题。假设WrappedComponent上有一个非常有用的方法,但是在高层组件增强之后,如果不处理,该方法就会丢失://WrappedComponent一些原始方法WrappedComponent.staticMethod=function(){/*...*/}//使用HOCconstEnhancedComponent=enhanced(WrappedComponent);//EnhancedcomponenthasnostaticMethodtypeofEnhancedComponent.staticMethod==='undefined'//true解决解决方法是复制静态方法。复制的常用方法有两种:明确知道需要复制哪些静态方法,然后使用Enhance.staticMethod=WrappedComponent.staticMethod;一张一张地抄写;使用hoist-non-react-statics自动复制:importhoistNonReactStaticfrom'hoist-non-react-statics';functionenhance(WrappedComponent){classEnhanceextendsReact.Component{/*...*/}//核心代码hoistNonReactStatic(增强,WrappedComponent);returnEnhance;}一些使用refs的WrapComponents的processRefs不能像props属性那样透传。这应该使用React.forwardRefAPI(在React16.3中引入)来处理。ref的特殊性将在后面的其他文章中详细介绍。说反向继承在文末,顺便也说一下反向继承。之所以放在最后是因为这个方法不是React官方推荐的。官方文档中有一句话:PleasenotethatHOCwillnotmodifythepassedImportedcomponentswillnotuseinheritancetoreplicatetheirbehavior。相反,HOC通过将组件包装在容器组件中来组合新组件。HOC是纯函数,没有副作用。但是看到现有的很多文章都介绍过这种用法,我顺便简单介绍一下,仅供理解,不建议使用(实践中至今没遇到过这种场景,如果遇到会稍后补充)。回过头来看上面提到的高阶组件,装饰器模式一直是被提及的关键词,比如用新组件包裹旧组件以进行增强。不同的是反向代理的思路是这样的,直接上传示例代码:functionwithHeader(WrappedComponent){//请注意这里是extendsWrappedComponent而不是extendsReact.ComponentreturnclassextendsWrappedComponent{render(){
//注意这里需要调用父类的render函数{super.render()}
}}观察这个例子的关键部分:高阶组件实际上返回了一个继承WrappedComponent组件的新组件,这也是反向继承名称的由来。在这种模式下,主要有两个操作:渲染劫持,如上例所示,在返回的新组件中,实际上可以控制WrappedComponent的渲染结果,进行各种需要的操作,包括有选择地渲染WrappedComponent子树的操作状态。由于新组件可以通过this访问WrappedComponent,所以也可以通过this.state进行修改。如果你真的想使用这种方式实现高层组件,你必须非常谨慎。渲染劫持需要考虑条件渲染的情况(即不完全返回子树),运行状态在某些情况下也可能破坏父组件。原始逻辑。谨慎使用,谨慎使用,谨慎使用!总结水到渠成,简单回顾一下本文的主要内容:一个高阶组件的本质是一个函数,输入参数和返回值都是组件,用来增强一个组件的特定功能。高层组件推荐灵活组合在使用过程中,记住一些注意事项,大致了解反向继承的原理,但还是要谨慎使用。对于refpiece,还有很多内容要写。本着每篇文章主题清晰,内容简洁,读者10分钟内学会知识的指引,我决定单独写在最后。最后,首先感谢每一位关注的读者朋友(尤其是这位提醒我有机会请你喝一杯奶茶的读者),欢迎大家关注专栏,希望您能毫不犹豫地为您喜欢的文章点赞和收藏。如果大家对文笔的风格和内容有什么意见,欢迎私信交流。