前言
自从2013年 React
开源以来,关于 React
组件的讨论层出不穷。一些组件类型在发展中逐渐淘汰,而另一些组件类型和组件设计模式逐渐沉淀下来,并演变为约定成俗的为 React
应用程序标准。本文将对React
中组件类型进行一个梳理总结。旨在让 React
开发者清晰地了解 React
组件类型。读完本文后,你应当能够从 React
程序与 React
技术文章中分辨出不同类型的 React
组件,并能在设计 React
应用时选择合适类型的组件。
我们将在下文由浅入深的介绍以下 React
组件类型:
- 受控组件与非受控组件
- 托管组价与非托管组件
- 状态组件与无状态组件
- 容器组件与展示组件
- 高阶组件与渲染属性
看到一堆组件名称,刚上手 React
的同学可能瞬间就头大了。其实,观察这些组件名称会发现它们是一对对的,理解一个组件即理解了另一个组件。另一方面,有一部分的组件并不常用,了解一下只是为了查漏补缺。下面来分别讲一下这几种组件类型:
一、受控组件与非受控组件
首先,厘清一个概念。React中受控组件与非受控组件是针对表单元素而言。
1. 受控组件
在HTML中,像
这类表单元素会维持自身状态,并根据用户输入进行更新。但在React中,可变的状态通常保存在组件的状态属性
state
中,并且只能通过setState()
方法更新。相应的,其值由React控制的输入表单元素称为“受控组件”。
上述这两句话是React官方文档关于受控组件的介绍,我们先来看看 React
中的受控组件长什么样子
上方代码中, input
元素数据依赖 state.userName
。每次按下键盘都会触发 onChange
事件去更新 state
, state
更新触发 input
的 value
值变化。有一点需要注意的是:在受控组件中,如果没有给表单元素绑定 onChange
事件。将会收到 React
的警告。此时,输入框除了默认值是无法输入任何值的。
有人看到这可能会疑惑,这不就是最普通的 React
组件书写方式吗?没错,抛开表单元素受控组件这个概念,大家对这种写书方式也都习以为常了,本质上都是通过 React
自上而下的单向数据流,通过 state
来更新UI。
总结 React
受控组件的特征:
- 特指表单元素。
- 表单元素数据依赖状态。
- 受控组件必须要在表单元素上使用
onChange
事件来绑定对应的事件。 - 表单元素的修改会实时映射到状态值上。
- 受控组件只有继承
React.Component
才会有状态。
2. 非受控组件
与受控组件正相反,非受控组件即不受状态的控制,通过虚拟DOM来获取数据。
总结Reac非受控组件的特征:
- 特指表单元素。
- 表单元素数据存储在
DOM
中。 - 通过虚拟
DOM
的方式获取表单元素数据。
3. 特点与区别
受控组件与非受控组件的特点
- 均特指表单元素
受控组件与非受控组件的区别
- 数据存储方式不同,受控组件的数据存储在
state
中,非受控组件的数据存储在DOM
中。 - 改变数据方式不同,受控组件通过事件回调改变
state
数据,非受控组件通过DOM
改变数据。 - 获取数据方式同步。受控组件通过访问
state
获取数据,非受控组件通过访问虚拟DOM
获取数据。
相信通过上述代码示例,大家已经完全理解什么是受控组件与非受控组件了。在 React
程序开发过程中,大家借助于 ant-desing
、 element-ui
等优秀的UI组件库,直接编写受控组件与非受控组件的业务场景并不多。对这两种组件类型,大家理解基本概念即可。
二、托管组件与非托管组件
随着React程序功能的增加,开发者会遇到一个问题。随着组件数量增加,组件之间如何通信?React官方给出的解决方法: 状态提升(Lifting State Up)。将多个子组件的状态提取到共同的父级组件中,而子组件本身不具有状态。
1. 托管组件
托管组件,顾名思义,将数据委托给其它组件控制。下面的例子中
与
组件本身不处理用户输入的数据,而是将数据托管给父组件
上面的例子中
与
本身不处理用户输出的数据,而是委托给父组件
来处理。在没有使用Mobx
、Redux
等状态管理的React
项目中。我们通常会采用状态提升(lifting-state-up)的方式,把所有的数据委托给一个相同的父组件,由父组件来控制数据与逻辑。这种把数据托管给其它组件控制的组件,称之为托管组件。
此时,回想ant-design
的组件是如何设计的,其
Input
组件既可以作为托管组件也可以作为非托管组件,如果存在Input.props.value
则把该组件作为托管组件,如果没有props.value
则作为非托管组件。我们来j简单实现一遍Antd.Input
的设计
2. 非托管组件
非托管组件的概念比较基蛋,即组件通过拥有状态处理自身的数据。下面有个输入框组件
,该组件监听用户的输入,并存储在自身的 state
,通过 state
改变来变化UI。这也是 React
最基本的组件模式。
3.特点与区别
特点:受控组件与非受控组件以外的组件均是托管组件或非托管组件
区别:托管组件将数据托管给其它组件的状态控制。而非托管组件通过自身状态自行管理。
三、状态组件与无状态组件
- 无状态组件(Stateless Component)
在上面中的托管组件中,我们了解到一个组件可以不需要拥有自己的状态,而将自己需要数据托管给其它的组件。 React
程序中,无状态组件的特征比较明显,概念也比较简单,没有自身的状态的组件都可以称之为无状态组件。
无状态组件有什么用处呢,我们将在下文的展示组件中介绍。而无状态组件通常由两种写法:
1.1 无状态类组件 Stateless Class Component: SCC
export default class StatelessClassComponent extends React.Component{ render() { return(
1.2 无状态函数组件 Stateless Function Component:SFC
const StatelessFunctionComponent = (props) => (
上面这两种组件均是无状态组件, SCC
与 SFC
这两种无状态组件类型的区别:
- 在
SCC
中仍然可以访问React
的生命周期,如ComponentWillMount
、ComponentDidMount
等 - 在
SFC
中无法访问React
的声明周期 SFC
在渲染性能上要明显高于SCC
组件。
在目前的 React
应用中,无状态组件更多采用第二种写法 SFC
。但在一些对生命周期钩子有特殊需求的场景中,你同样可以使用第一种无状态组件 SCC
。
2. 状态组件
与无状态组件相对应,状态组件会带有state用以处理业务逻辑、UI逻辑、数据处理等。通常还会调用React的生命周期函数,用以在特殊的时刻控制状态。
class StatefulComponent extends Component { constructor(props) { super(props); this.state = { //定义状态 } } componentWillMount() { //do something } componentDidMount() { //do something } ... //其他生命周期 render() { return ( //render ); } }
- 状态组件与无状态组件区别
- 在组件写法上,两者区别为组件本身是否具有状态。
- 在业务场景上,状态组件、无状态组件与托管组件、非托管组件类似。状态组件通常作为托管组件,管理数据、处理业务逻辑与UI逻辑,而无状态组件往往是非托管组件,将自身的UI逻辑委托给状态组件来处理。
四、高阶组件与渲染属性
上述介绍的三类React中最基本的组件类型。在React应用程序的发展与设计历程中,为了提高组件的可复用性,社区与官方逐渐提出了一些组件设计模式,我们在本文中也将之归入组件类型中。
1. 高阶组件(HOC)
高阶组件(HOC)是 React 中用于重用组件逻辑的高级技术。 HOC 本身不是 React API 的一部分。 它们是从 React 构思本质中浮现出来的一种模式。具体来说, 高阶组件是一个函数,能够接受一个组件并返回一个新的组件。
上述是React官方对高阶组件的描述,关于如何编写高阶组件以及注意事项,官网给出了详细说明,在本文中我们不再介绍。我们以一个简单的高阶组件例子,来分析高阶组件的特点
假如,我们有一个需求在鼠标移动时,在界面上显示鼠标的坐标值。而且,这个功能可能会用到多个组件中,甚至不同的页面中。意味界面上显示数据坐标值的需求是固定的,但UI组件显示的方式可能是变化的。
这个例子中经过 showMouse装饰的组件,会具备一个能力,拥有鼠标的坐标数据。并且组件会拥有一个新的 props.tag
与 props.mouse
。此时的 props.mouse
相当于
组件将数据托管给了
组件,而且
组件的 props
将完全由
组件控制,而不对外开放。由于高阶组件的低侵入性, showMouse这个函数装饰过的组件都会拥有数据坐标数据的能力。
值得一提的是,与高阶组件相匹配的另外一个杀手锏是ES7的装饰器模式,使用装饰器模式来搭配高阶组价可极大提高代码的可阅读性与高可复用性,最出名的当属 Mobx
的 @observer
装饰器。
2. 渲染属性(Render Props)
术语Render Props是指一种技术,用于使用一个值为函数的 prop 在 React 组件之间的代码共享。
假如,我们有一个需求在鼠标移动时,在界面上显示鼠标的坐标值。而且,这个功能可能会用到多个组件中,甚至不同的页面中。意味界面上显示数据坐标值的需求是固定的,但UI组件显示的方式可能是变化的。
在
组件中,其会追踪鼠标坐标位置。但没有决定如何显示这个坐标值。反而是将如何展示UI交给了调用者来控制。在前端开发工作中,UI是高频次变化的。业务逻辑与业务数据反而是相对稳定的。这种将展数据展示方式交由调用者来控制的方式,极大提高了UI的部分的高扩展性。在一些定制系统的需求场景中,往往会内置几种系统交互方式、UI风格,而这种组件则是非常适合的组件类型。
值得一提的是,该组件类型的官方名称是 Render Props
,但并不意味着你一定要通过一个 props.render
来实现。该组件的核心思想是:由调用者决定如何展示组件。现有的 React
程序中,该组件类型还有一种变体,写法如下:
- Render as Child
React
中的 this.props.children
API本身也是个函数,因此,上述例子中使用 this.props.children
方法,同样将UI的展示方式交由了调用者来决定。
3. 特点与区别
高阶函数的特点:
- 接收一个组件,并返回一个组件
- 代理
props
,如上述例子中, <WrappedComponent/>
组件的props
完全由 <Wrapper />
组件来控制。 - 低侵入性。
函数为组件赋予了
组件的数据,而并没有影响组件内部的逻辑。 - 拥有反向继承能力。
渲染属性的特点:
- 本身不决定数据展示方式,将数据展示方式完全交由调用者决定。
- 无侵入性。上述高阶函数的例子,由于组件被赋予
props.mouse
。因此组件在调用时不能再拥有props.mouse
,因此,我们说高阶函数是低侵入性,而不是无侵入性。但渲染属性回调形参的方式,决定了其不会有任何侵入性。
高阶函数与渲染属性的共同特点
- 均有解决逻辑复用的作用。
- 均通过逻辑分离、组件拆分,组合调用的方式,实现代码复用,逻辑复用。
高阶函数与渲染属性的区别
- 实现方式的区别。高阶函数通过柯里化函数实现,渲染属性通过回调函数来实现。
- 调用方式的不同。高级函数通过函数调用、装饰器模式调用。渲染属性通过回调函数重写实现。
五、展示组件与容器组件
到目前为止,上述四种类型组件,可以从写法上、特定元素类型上,明显区分出来。下面我们要介绍两种组件,它们没有什特定的写法,更着重于组件设计模式的概念。与上述高阶组件与渲染回调相似的是,它们有相似的设计思路:逻辑分离与组件复用。
我们知道React组件化的主题思想: 组件高可复用性--将一个页面拆成一堆独立、可复用的组件,并且通过自上而下的单向数据流的形式将这些组件串联起来。但在实际的开发过程中,大家意识到React的组件不仅仅是UI展示,往往会牵扯大量的业务逻辑、UI逻辑、业务数据及状态。比如我们需要处理数据请求,需要处理点击事件、改变事件、UI状态等等。这么一坨业务逻辑与UI操作逻辑放在组件里。不仅逻辑复杂的组件代码冗长,难以开发与维护,而且这些组件很难复用到其它的地方去。那怎么办呢?
将笨重的组件拆分开来,将通用的业务逻辑与UI逻辑提取出来!将组件拆分为职责单一的业务逻辑组件、UI逻辑组件、UI展示组件等。再将合适粒度的组件组合成一个完整的功能点。
将业务逻辑与UI组件拆分开来,完全没有必要把业务逻辑跟UI展示混杂在一起。什么意思呢?我们用代码示例说明一下,一个简化版TodoList的例子。
上面这个组件很简单,加载数据,显示为一个列表。点击时弹出点击项的名称。我们将上述组件拆分为两部分,一部分加载数据,一部分负责UI展示
对上述
组件来说,其本身是一个获取数据、存储数据的容器组件,获取数据的url可能会变化,存储的数据接口可能会变化。但它对展示组件
没有什么要求。
对上述
组件来说,即便是换一个有应用场景,只要给定的props
上有同样是数据结构的list
,它就一定能运行,而且点击事件也正常运行。
组件对使用者只有数据源的要求,而无其它的要求,增大了组合的可能性。
此时,上述两个组件的拆分,虽然提升了一些复用的能力,但复用的可能性并不大。比如
这个组件,在实际的业务中,它作为一个复用组件仍然是鸡肋般的存在。不过没关系,我们先有有一个拆分与组合的概念,下面通过一个稍微复杂的例子,来进一步阐述容器组件与展示组件。
1. 容器组件(Container Component)
前端有一句俗语:一切从数据中来,一切向数据中去。任何一个系统绕不过请求数据这个需求。上述的
组件中有请求数据、存储数据,及响应UI的能力。我们设计一个具有同样能力的容器组件。
上述
即为一个容器组件,我们将请求数据的逻辑、所有UI响应事件、数据存储封装在内,而其不处理UI展示的问题,也不具有任何DOM标签。实际上借助
、
等状态管理器,上述代码可以将数据存储与修改逻辑,拆分到状态管理文件中,不过本文篇幅优先,我们不在这里介绍这些状态管理器。在一个组件中调用 SomePage
代码调用如下
此时来看我们的 SomePage
组件,它有以下能力
- 自动请求数据。
- 存储数据。
- 响应UI事件。
- 不关心如何展示的问题。
总结我们的容器组件:处理一切与展示不相关的逻辑。在上述代码末尾,我们看到一个 <UIComponent/>
组件,它正是我们要在下文介绍的展示组件
2. 展示组件(Presentational Component)
通过上方的示例代码,我们已经的
组件已经与
组件已经处理了请求数据的业务逻辑、存储数据、响应UI事件、处理UI逻辑等功能。我们还剩下展示数据等工作没做,毕竟没有界面的前端还叫前端吗~
上述 <UIComponent />
组件代码如下,此时我们采用了无状态组件SFC
当年星宿老怪率众围攻少林寺搜寻易筋经,方丈让虚竹打探一下院子里有多少人马?虚竹回到:方丈,两个人。方丈大喜,激动的来到窗前,准备看看是哪两个不要命的来了,结果却看到了千军万马。方丈不解的问虚竹:对方来了这么多人,你为什么说只来了两个?虚竹回答:是两个啊,方丈。一敌一我。
回想一下,任何的界面是不是只关心两个问题:显示什么?操作了什么?
对一个UI组件来说,要考虑的事情只有两点,划重点敲黑板,只有两点:输入与输出。
如果我有一个按钮,只需要关心按钮显示什么文字的问题(先不考虑样式问题)?文字是哪来的?外部输入进来的。
如果我还是只有一个按钮,你会关心按钮被点击时,要怎么通知外部(绑定的 click
事件),至于要通知外部的内容,要知道 click
事件的参数是由你自己确定的,换句话说是这个按钮组件决定了传什么参数,也就是输出什么。
有了上述输入与输入的概念后,如果我的组件不用关心怎么去加载数据,怎么变化数据,而且这个组件输入固定数据,就显示固定UI,操作固定UI,就对外输出固定值。比如上述的
,那么这个组件就非常适合移植。根据开闭原则:对修改关闭,对扩展开放。
- 如果我们此时需要更换UI(前端更换UI效果,太常见的需求了),只需要在调用 <
App />
时替换
组件即可,而不需要修改
与loadData
中的其它代码。 - 如果我们此时需要更改请求,只需要在调用
替换请求的地址即可。而不需要修改
组件。
此时,我们回想蚂蚁金服的ant-design
,我们会发现其组件,内聚了UI交互逻辑、明确声明所需要的数据结构,并且所有的组件全部遵循这个规则。使得我们在使用ant-design
的React
项目可以通过组合其组件,极其快速的搭建界面。
3. 特点与区别
展示组件(Presentational Component)
- 只关注页面的展示效果。
- 不关心数据怎么加载与怎么响应事件。
- 只能通过
props
的方式接收数据和触发响应事件。 - 对其它组件没有其他依赖关系。
- 对数据结构有要求,固定输入固定输出。
- 通常是无状态组件,推荐使用
SFC
。
容器组件(Container Component)
- 内部封装了业务逻辑与UI逻辑。
- 提供数据和行为给展示组件或其它容器组件。
- 往往是有状态组件,因为它们倾向于作为数据源。
- 内部可以包含容器组件和展示组件,但通常没有任何自己的
DOM
标记,除了一些包装div
,并且从不具有任何样式。
值得注意的是,本文中的容器组件,均指数据容器、业务逻辑容器、UI逻辑容器等,而非类似Bootstrap
中UI容器的概念。
结语
如前文所述, React
中的常用组件分为五大类类型:
- 受控组件与非受控组件
- 托管组件与非托管组件
- 状态组件与无状态组件
- 高阶组件与渲染属性
- 展示组件与容器组件
前三类组件类型,为 React
的基本组件类型。后两类组件类型,为 React
发展中沉淀下的组件设计模式。其目的在于使得组件达到高可复用性。在梳理过 React
应用开发中的常用组件类型后,开发者在接手 React
项目中或在设计 React
程序时,根据业务需求,选择使用合适的组件类型。
本文完~