React高阶组件
索引
在React组件的构建过程中,常常有这样的场景,有一类功能需要被不同的组件公用,此时,就涉及抽象的话题,在不同设计理念下,有许多的抽象方法,而针对React,我们重点讨论两种:mixin和高阶组件。
Mixin
mixin的特性一直广泛存在于各种面向对象语言中。比如在Ruby中,include关键词即是mixin。是将一个模块混入到一个另一个模块中,或是一个类中。为什么编程语言要引入这样一种特性呢?事实上,包括C++等一些年龄较大的OOP语言,它们都有一个强大但危险的多重继承特性。在现代语言中,为了权衡利弊,大都舍弃了多重继承,只采用单继承,但单继承在实现抽象时有很多不方便的地方,为了弥补缺失,java引入了接口interface。其他一些语言则引入了像mixin的技巧。
Mixin的问题
从上面的代码,我们不难看出,对于广义的mixin方法,就是用赋值的方式将mixin对象里的方法都挂载到原对象上,来实现对对象的混入。
从上述的实现,我们可以联想到 underscore库中的extend 或 lodash库中的 assign方法,或者说ES6中的Object.assign()方法。
React官方已不推荐使用Mixins的技术来实现代码的重用,Mixins技术有一系列的缺点,首先Mixins会造成命名冲突,如果你需要注入多个mixins,其中一个是自己的,另外的可能是第三方的。那有可能在两个mixins里使用了相同名称的方法,这会使得其中的一个不起作用,而你能做的只有修改其中一个方法的名称。另一方面,一个mixins一开始可能是非常简单的,仅仅需要实现某一个功能,但当业务越加的复杂,需要往其中加入更多的方法的时候,就会变得非常复杂。针对这些困扰,React社区提出来新的方式来取代mixin,那就是高阶组件。
概念
说到高阶组件,就先得说到高阶函数了,高阶函数是至少满足下列条件的函数在javascript这门函数为一等公民的语言中,高阶函数的使用还是非常之多的,像我们平时的回调函数等等,都用到了高阶函数的知识。我们先来看一个简单的高阶函数
从上面的代码,我们不难看出,对于广义的mixin方法,就是用赋值的方式将mixin对象里的方法都挂载到原对象上,来实现对对象的混入。从上述的实现,我们可以联想到 underscore库中的extend 或 lodash库中的 assign方法,或者说ES6中的Object.assign()方法。
高阶组件其实是差不多的用法,类比高阶函数的定义,高阶组件就是接受一个组件作为参数,在函数中对组件做一系列的处理,随后返回一个新的组件作为返回值。高阶组件(HOC)是 React 中用于重用组件逻辑的高级技术。 HOC 本身不是 React API 的一部分。 它们是从 React 构思本质中浮现出来的一种模式,这种模式是由React自身的组合性质必然产生的。具体来说,高阶组件是一个函数,能够接受一个组件作为参数,在函数中对组件做一系列的处理,随后返回一个新的组件作为返回值。在我们项目中使用react-redux框架的时候,有一个connect的概念,这里的connect其实就是一个高阶组件。也包括类似react-router-dom中的withRouter的概念。
装饰器模式
先来一个最简单的demo
组件Usual通过simpleHoc的包装,打了一个log… 那么形如simpleHoc就是一个高阶组件了,通过接收一个组件class Usual,并返回一个组件class。 其实我们可以看到,在这个函数里,我们可以做很多操作。 而且return的组件同样有自己的生命周期,function,另外,我们看到也可以把props传给WrappedComponent(被包装的组件)。
高阶组件可以看做是装饰器模式(Decorator Pattern)在React的实现。即允许向一个现有的对象添加新的功能,同时又不改变其结构,属于包装模式(Wrapper Pattern)的一种ES7中添加了一个decorator的属性,使用@符表示,可以更精简的书写。那上面的例子就可以改成 是同样的效果。当然兼容性是存在问题的,通常都是通过babel去编译的。 babel提供了plugin,transform-decorators-legacy。
实现方式
实现高阶组件的方法有如下两种:属性代理、反向继承。
属性代理:高阶组件通过被包裹的React组件来操作props
属性代理有如下4点常见作用:
-
操作props
-
通过refs访问组件实例
-
提取state
-
用其他元素包裹WrappedComponent,实现布局等目的
可以通过传入 props 和回调函数把 state 提取出来使用ppHOC装饰器之后,组件的props被添加了name属性,可以通过下面的方法,将value和onChange添加到input上面 input会成为受控组件
反向继承:高阶组件继承于被包裹的React组件。
反向继承可以劫持被继承class的render内容,进行修改,过滤后,返回新的显示内容。之所以被称为渲染劫持是因为 HOC 控制着 WrappedComponent 的渲染输出,可以用它做各种各样的事。
如上代码。高阶组件返回的组件继承于 WrappedComponent 。因为被动地继承了 WrappedComponent,所有的调用都会反向,这也是种方法的由来。这种方法与属性代理不太一样。它通过继承WrappedComponent来实现,方法可以通过super来顺序调用。因为依赖于继承机制。HOC的调用顺序和队列是一样的。在由 render输出的任何 React 元素中读取、添加、编辑、删除 props读取和修改由 render 输出的 React 元素树有条件地渲染元素树把样式包裹进元素树,就行Props Proxy那样包裹其他的元素
应用场景
1、两个页面UI几乎一样,功能几乎相同,仅仅几个操作不太一样,却写了两个耦合很多的页面级组件
2、之前写过一个组件A,做完上线,之后加了一个新需求,很奇怪要做的组件B跟A几乎一模一样,但稍微有区别
3、Container解决不了的时候甚至不太优雅的时候
4、实现一个从loaclstorage返回记录的功能
5、Mobx-react、react-redux
A).Container解决不了的时候甚至不太优雅的时候。其实大部分时候包一层Container组件也能做到差不多的效果,比如操作props,渲染劫持。但其实还是有很大区别的。比如我们现在有两个功能的container,添加样式和添加处理函数的,对Usual进行包装.
B).mobx-react就是高阶组件是一个实际应用@observer装饰器将组件包装为高阶组件,传入组件MyComponent后,mobx-react会对其生命周期进行各种处理,并通过调用forceUpdate来进行刷新实现最小粒度的渲染。mobx提倡一份数据引用,而redux中则提倡immutable思想,每次返回新对象。
C).react-redux中的connect
D).connect([mapStateToProps],[mapDispatchToProps],[mergeProps],[options])(WrappedComponent)
例如,我们把组件ComponentA连接到Redux上的写法类似于:
const ConnectedComponentA = connect(mapStateToProps,mapDispatchToProps)(ComponentA);
我们可以把它拆分来看:
// connect 是一个函数,返回值enhance也是一个函数
const enhance = connect(mapStateToProps, mapDispatchToProps);
// 返回的函数就是一个高阶组件,该高阶组件返回一个与Redux store关联起来的新组件
const ConnectedComponentA = enhance(ComponentA);
connect这个函数会将一个React组件连接到Redux 的 store
这个函数会将一个React组件连接到Redux 的 store。在连接的过程中,connect通过参数 mapStateToProps,从全局store中取出当前组件需要的state,并把state转化成当前组件的props;同时通过参数 mapDispatchToProps,把当前组件用到的Redux的action creators,以props的方式传递给当前组件。
总结
1、高阶组件不会修改子组件,也不拷贝子组件的行为,建议使用组合。
2、要给hoc添加class名,便于debugger。
3、静态方法要复制。
4、refs不会传递。
5、不要在render方法内部使用高阶组件