组件的复用和组合,可以帮助我们在现有的轮子上扩展新的功能,提高工作效率,避免重复造轮子。React 组件化的开发方式可以很好地实现复用和组合的功能,本章主要围绕高阶组件来讨论下这一问题。
高阶组件(HOC)
高阶组件(HOC)是 React 中用于重用组件逻辑的高级技术。 HOC 本身不是 React API 的一部分。 它们是从 React 构思本质中浮现出来的一种模式。
通俗点高阶组件就是一个函数,其接受一个组件并返回对着个组件功能上的扩展复用的新的组件。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
高阶组件常见有两种实现方式,一种是 Props Proxy(属性代理),一种是 Inheritance Inversion(继承反转)
Props Proxy
Props Proxy 模式可以对 WrappedComponent 的 props 进行操作扩展,抽离 state,并可以使用其他元素来包裹 WrappedComponend 来实现扩展组件的功能。
操作 props
|
|
可以看到,传递给 WrappedComponent 的属性首先传递给了高阶组件返回的组件,这样我们就获得了props的控制权。
抽离 state
我们可以将 WrappedComponent 中的状态提到包裹组件中,一种很常见的操作就是将不受控组件转换成受控组件。通常,我们在设计 UI 组件的时候,组件应该简单只负责展示 (不受控组件)。对组件的修改逻辑不应该放在组件中,而是由调用者来提供。这样,我们就可以用HOC来将无状态组件变成受控组件。
|
|
Inheritance Inversion
反向继承,我们采用直接继承 WrappedComponent 的方案,而不是采用包裹 WrappedComonent 的代理方案。这意味着,我们可以调用 WrappedComponent 的属性,声明周期等任何内容。
|
|
通过我们可以完全操作 WrappedComponent 上的内容,我们可以实现渲染劫持
等操作,改变 WrappedComponent 的任何行为。
Inheritance Inversion 是继承的思想,对于 WrappedComponent 也有较强的侵入性,因此并不常见。
高阶组件的应用
高阶组件的本质是统一抽象功能,强调逻辑和UI的分离
view 层分离
在设计组件的时候,我们会尽可能考虑组件的复用性,对于组件的 view 层,我们期望是组件与组件之间没有重叠的部分,重叠的部分应该被抽出来形成更细粒度的组件,这样方便我们各种各样的组件组合。每个最小的基础组件我们都期望他是一个木偶组件(Dumb Component)
。
木偶组件,指只会接受 props 并且渲染结果完全依赖 props 的组件。Dumb 组件不应该依赖除了 React.js 和 Dumb 组合以外的内容(比如不应该依赖 redux,mobx 等)。这样的组件可复用性是最好的,其他人可以放心使用。
当然仅有 Dumb 组件,是不能工作的,因为他们没有逻辑。为此还应该有这么一类组件,他们只负责应用逻辑,和各种数据打交道,然后把数据以 props 的形式传递给 Dumb 组件。
注意,Dumb 绝对不能依赖 Smart 组件,这相当增加了 Dumb 输出的不确定性。如果一个组件是 Dumb 的,那么它的子组件们都应该是 Dumb 的才对。
逻辑层的分离
组件中的交互逻辑和业务逻辑有很大部分也是重复的,我们可以将这写公共部分进行抽象封装起来,来为其他组件增加新的能力,这也就是高阶组建的思想。每个独立可重用的逻辑都是一个 Decorator
装饰器。
适用于高阶组件的逻辑层应该时那些完全不与 DOM 相关的内容。比如数据校验,权限控制,或者通过数据变化间接控制 DOM 的。
举例 Form 表单的抽离
Form 中,会包含不同的组件,input, selector, checkbox 等等,也可能会是多种常见组件的组合。
如图,一个下拉搜索框,由 input,select, list 三个纯粹的细粒度的 Dumb 组件组成。对于每个 UI 都有自己的数据 validator 验证规则,和数据变化回调规则。我们可以将这部分逻辑的对应关系和UI的绑定做成一个 HOC 组件。
|
|
高阶组件的问题
不确定性 & 命名冲突
高阶组件的变更是由 props 引起的,且高阶组件之间是互相独立的。因此当组件的 props 发生变化后,我们很难确定是哪个高级组件引起的 props 变动或者是组件本身引起的。同样多个高阶组件的引入的 props 也有可能因为同名原因导致互相覆盖,产生许多无用的组件嵌套加深组件层级,这些问题高阶组件都无法解决。
官方文档上推荐我们使用高阶组件的时候提出了如下约定:
约定: 给包裹组件传递不相关的属性(Props)
这个问题只能靠约定而没有办法约束,因此可维护性变低。
静态方法必须复制 & Refs 不会被传递
由于高阶组件包裹的特性,原有组件上的静态方法并不会得到传递。同样的道理 ref 在容器组件上应用 ref 也不会直接传递给原有组件。我们都需要在容器组件上做静态方法的复制和 ref 的传递。
高阶组件的带来的副作用有没有解决办法呢?render-props 可以在一定程度上来解决,这个之后我们再谈吧。