响应式编程(Reactive Programming,或称为反应式编程)、响应式设计(布局)、响应式系统,因为都带有「响应式」(中文),致使没有相关使用经验的人员,或可能对这几个完全不同领域的事物,误以为是同一概念的不同说法。此篇文章,旨在阐述各自是什么,从而解释它们之间存在的区别。
当人们在软件开发和设计的背景下,谈论“反应式”时,通常意味着以下三件事之一:
- 反应式系统(架构和设计);
- 反应式编程(基于声明性事件);
- 函数反应式编程(FRP);
如果要以“响应式”一词来谈论,或许还要加上响应式设计(Responsive Design)。需要申明的是,响应式编程(RP
:Reactive Programming)、响应式设计(布局)、响应式系统,是三个完全不同的概念。此外,响应式编程,和 函数式编程 (Functional Programming)、 函数式响应式编程 (FRP
:Functional Reactive Programming),也是不尽相同、但容易混淆的概念。
响应式编程
响应式编程 (也称反应式编程,英语:Reactive Programming),是一种面向 数据串流
(data stream)和 变化传播
(propagation of change)的声明式(Declarative)「编程范式」。这意味着可以在编程语言中,很方便地表达静态或动态的数据流,而相关的计算模型,会自动
将变化的值通过数据流进行传播。
实际举例
对于 a=b+c
这个表达式的处理,在命令式编程中,会先计算 b+c
的结果,再把此结果赋值给 变量 a,因此 b,c 两值的变化不会对 变量 a 产生影响。但在响应式编程中,变量 a 的值会随时跟随 b,c 的变化而变化。
电子表格程序,就是响应式编程的一个实例。单元格可以包含字面值或类似 "=B1+C1" 的公式,而包含公式的单元格的值,会依据其他单元格的值的变化而变化。在 MVC 软件架构中,响应式编程允许将相关模型的变化,自动反映到视图上,反之亦然。
诞生初衷
响应式编程范型,基于 Edward A. Lee 和 David G. Messerschmitt 在 1987 年提出的同步数据流程编程范型。它最初是为了:简化交互式用户界面的创建和实时系统动画的绘制,而提出来的一种方法,但它本质上是一种通用的编程范式。价值在于:易于编写、易于维护;及时响应。
响应式编程特点
传统的编程方式,是顺序执行的:上一个任务没有完成,需要等待,直至完成之后,才会执行下一个任务。无论是提升机器的性能还是代码的性能,本质上都需要依赖上一个任务的完成。如果需要响应迅速,就得把同步执行的方式换成异步,方法执行变成消息发送。这变成了异步编程的方式,它是响应式编程的重要特性之一。
- 异步编程:提供了合适的异步编程模型,能够挖掘多核 CPU 的能力、提高效率、降低延迟和阻塞等。
- 数据流:基于数据流模型,响应式编程提供一套统一的 Stream 风格的数据处理接口。和 Java 8 中的 Stream 相比,响应式编程除了支持静态数据流,还支持动态数据流,并且允许复用和同时接入多个订阅者。
- 变化传播:简单来说就是以一个「数据流」为输入,经过一连串操作转化为另一个数据流,然后分发给各个订阅者的过程。这就有点像函数式编程中的组合函数(compose),将多个函数串联起来,把一组输入数据转化为格式迥异的输出数据。
响应式编程优点
响应式编程在执行过程中是异「步非阻塞」,可以充分利用多核 CPU 的性能,并且可以由底层的执行模型,负责通过数据流来自动传播变化,可以做到更实时的响应;使用响应式编程风格开发的程序,能支持更大的 吞吐量
(备注:支持更大的吞吐量,不代表程序跑的更快;因为响应式编程只是让执行的主线程不会因为某些耗时操作而阻塞,导致其阻塞后面的请求,但实际执行耗时操作的子线程消耗的时间依然是没有变化)。
响应式编程缺点
- 响应式编程风格,与传统的命令式编程风格差异大,且响应式编程主要是异步的,相较于同步的编程方式在思维上有所变化,需要一定的学习成本;
- 因为其基于「事件」驱动,每次触发事件会使用新线程执行;如果在一个不会阻塞的程序中使用,线程切换可能比程序本身执行耗时要多;
- 对于异常的处理,因为基于回调或者声明式的原因,在使用匿名回调时,没有寻址能力,意味着在整个数据处理链中,每个处理节点的处理成功或错误的状态,不会向外界发送相应信号,其中某个节点出错异常可能会被吞掉,或者进行了相关处理,但得到的异常信息少,不好定位。
具体实现
ReactiveX ,是响应式编程原则的一种实现,通过使用可观察序列,组成异步和基于事件的程序。使用 RX,您的代码创建并订阅名为 Observables 的数据流。虽然反应式编程是关于概念的,但 RX 为您提供了一个惊人的工具箱。通过结观察者和迭代器模式以及函数式习语,RX 为您提供了超能力。您拥有一系列功能来组合、合并、过滤、转换和创建数据流。
值得一提的是,ReactiveX 可以说无处不在,对前端、跨平台、后端都进行了适配,并提供相对应用的工具。在 Web 上可以使用 RxJS (A reactive programming library for JavaScript. 已有 27.5K Star),在移动设备上使用 Rx.NET 和 RxJava 操作 UI 事件和 API 响应。在 Java 平台,支持响应式编程的流行库有 Reactor , RxJava (Reactive Extensions for the JVM), Vert.x 等,而在Java9中,实现了响应式流规范(Reactive Streams)所定义的相关接口,让Java本身支持了响应式编程,不需要通过其他三方库实现。
此外,前端领域还有很多其他更进一步的实现,诸如 Cycle.js :用于可预测代码的功能性和反应式 JavaScript 框架。
RxJS:JS 的反应式编程库
RxJS JavaScript 的反应式(响应式)编程库,它通过使用可观察(Observable)序列,来编写异步和基于事件的程序。它提供了一个核心类型 Observable,附属类型 (Observer、 Schedulers、 Subjects) 和受 Array 启发的操作符 (map、filter、reduce、every 等等),这些数组操作符可以把异步事件作为集合来处理。🉑️将 RxJS 视为事件的 Lodash
。
响应式系统
响应式系统定义
在 反应式宣言 (发布于 2014 年 9 月,提出了一组系统架构的原则,阐述了响应式系统应该具有的特性,相比响应式编程更抽象)中给出「响应式系统」的诞生背景及定义:
在不同领域工作的组织正在独立地发现构建看起来相同的软件的模式。这些系统更健壮、更有弹性、更灵活,更能满足现代需求。
这些变化正在发生,因为近年来应用程序需求发生了巨大变化。就在几年前,一个大型应用程序拥有数十台服务器、几秒钟的响应时间、数小时的离线维护和千兆字节的数据。如今,应用程序部署在从移动设备到运行数千个多核处理器的基于云的集群的所有设备上。用户期望毫秒级的响应时间和 100% 的正常运行时间。数据以 PB 为单位。昨天的软件架构根本无法满足今天的需求。
我们认为系统架构需要一种连贯的方法,并且我们相信所有必要的方面都已经被单独识别:我们需要响应式、弹性、弹性和消息驱动的系统。我们称这些反应系统。
响应式系统特性
反应式宣言 中指出,一个响应式系统,应该具有的 4 个特性:即时响应性、回弹性、弹性、消息驱动。
- 即时响应性(Responsive):系统应该尽可能地即时响应客户端的请求;
- 回弹性(Resilient):系统应该在出现问题时依然能保持即时响应性,同时故障应该能被遏制,不会扩散导致整个系统崩溃,并且故障的部分应该做到独立恢复,这就要求系统结构应该是通过组件/模块组合而构建出来的,确保每个组件/模块之间相互隔离,组件的故障恢复可以委托其他外部组件来恢复(也就是说,系统中的组件应该有熔断,不会造成牵一发动全身的灾难性场面,有多副本形成高可用,挂了能够自动恢复,是不是高可用可伸缩架构的要求?);
- 弹性(Elastic):系统应该能做到根据输入速率的变化做出反应,适当的调整系统所占资源或者系统的负载(即:能够自动缩/扩容,能够做到负载均衡;在不同负载下,系统的吞吐量、响应性不存在区别);
- 消息驱动(Message Driven):使用消息,可以让系统中具体的各功能组件具有松耦合,隔离,位置透明,边界清晰明确的特点,配合背压,可以做到及时让上游服务进行流控,负载管理等。
与响应式编程的区别
- 响应式编程,很适合用于开发响应式系统,但不是唯一的选择;
- 响应式编程:专注于通过临时数据流进行计算,往往基于事件驱动;
- 响应式系统:通过分布式系统的通信和协调关注弹性和弹性。基于消息驱动;
关于「事件驱动」和「消息驱动」的区别别,反应式宣言 官网给出了详尽的解释:
一条消息就是一则被送往一个明确目的地的数据。一个事件则是达到某个给定状态的组件,发出的一个信号。在一个消息驱动系统中,可寻址到的接收者等待消息的到来然后响应它,否则保持休眠状态。在一个事件驱动系统中,通知的监听者被绑定到消息源上,这样当消息被发出时它就会被调用。这意味着一个事件驱动系统,专注于可寻址的事件源;而消息驱动系统,专注于可寻址的接收者。
响应式系统,需要考虑数据一致性(data consistency)、跨结点沟通(cross-node communication)、协调(coordination)、版本控制(versioning)、编排(orchestration)、错误管理(failure management)、关注与责任分离(concerns and responsibilities)等内容。
总结:响应式(反应式)编程,是一种在组件本地管理内部逻辑和数据流转换的伟大技术,作为优化代码清晰度、性能和资源效率的一种方式。响应式(反应式)系统,作为一组架构原则,强调分布式通信,并提供了解决分布式系统弹性和弹性的工具。
函数式编程
函数式编程 (Functional Programming),或称函数程序设计、泛函编程,是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。比起指令式编程,函数式编程更加强调程序执行的结果,而非执行的过程;倡导利用若干简单的执行单元,让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。更多感悟,有在 2022,值得学习系列之:函数式编程 一文中做过更详尽阐述。
函数响应式编程
函数式响应式编程(FRP
) 是一种编程范式(于 1997 年被 Conal Elliott 提出/定义);它采用函数式编程的基础部件(如 map、reduce、filter 等),进行响应式编程(异步数据流程编程)。FRP 被用于 GUI、机器人和音乐方面的编程,旨在通过显式的建模时间,来简化这些问题。
随着移动设备普遍,用户量远超桌面设备,很多用户使用移动设备(手机、平板等)浏览网站、应用程序。于是,除了传统的桌面端,对移动小屏设备的良好支持,提供移动设备用户体验,已成为所有网站设计的一个必备要素。现在,主流的解决方案是:响应式网页设计(Responsive Web Design)和自适应网页设计(Adaptive Web Design)。
诞生背景
早年设计 Web 时,页面是以适配特定的屏幕大小为考量创建的。如果用户正在使用比设计者考虑到的更小或者更大的屏幕,那么结果从多余的滚动条,到过长的行和没有被合理利用的空间,不一而足。
随着人们使用的屏幕尺寸的种类越来越多,出现了响应式网页设计的概念(Responsive Web Design,RWD
),RWD 指的是允许 Web 页面,适应不同屏幕宽度因素等,进行「布局和外观」的调整的一系列实践。这是改变我们设计多设备网页的方式的思想。“响应式设计”这个词是 Ethan Marcotte 在 2010 年首度提出的 。
深入解释
需要声明的是:响应式 Web 设计不是单独的技术,它是描述 Web 设计的一种方式、或者是一组最佳实践的一个词,它是用来建立可以响应查看内容的设备的样式的一个词;也许将其描述为“网页自适应”,对于他人更容易理解。现代的 CSS 布局方式基本上就是响应式的,而且我们在 Web 平台上内置了新的东西,使得设计响应式站点变得容易。
相关技术
如果做好网站响应式设计?在面试前端开发人员时,比较喜欢考察的问题。如果您有从事前端开发经验,无论是 Web、原生应用、小程序、还是快应用,相信对于响应式设计,并不陌生。接下来依据经验,谈下做好网站响应式(从超大屏幕的 iMac、到 Pad、及不同尺寸的手机),需要在哪几个方向上下功夫:
- 设计:毕竟屏幕尺寸不同,所能展示内容以及方式,多少会存在存在差异,这就要求在做 UI、交互设计时,就当区别对待,提供多套设计稿;
- 布局:Flexible Box(
flex
)、CSS 网格(grid
),可以简便、完整、响应式地实现各种页面布局;尤其 Flex 布局,近些年最为流行;
- 媒体查询:Media queries 非常实用技术,尤其是想要根据设备的大致类型,或者特定的特征和设备参数,来修改网站或应用程序时;
- 单位:与 Web 早期只用
px
单位不同,如今 100%
、em
、rem
、vw
、vh
,及 calc() 方法,是做好响应式设计中重要组成部分,在实际项目中当按需使用;
- viewport meta tag :必备。并非所有设备都具有相同的宽度;您应该确保您的页面在各种屏幕尺寸和方向上都能正常工作。告诉移动端浏览器,它们应该将视口宽度设定为设备的宽度,将文档放大到其预期大小的 100%,在移动端以你所希望的为移动优化的大小展示文档;
- JS、DOM 入手:必要时候,尤其针对设计上存在区别,有必要通过判断,提供不同组件或 DOM 节点,以及 JavaScript 逻辑处理,以适配不同屏幕尺寸 UI、交互设计;
- 响应式图像:设备宽高比差异日益增大,基于图片缩放显然已不能解决问题;可使用
<picture>
元素和 <img>
srcset
和sizes
特性,提供多种尺寸(描述图像最适合的屏幕尺寸和分辨率的元数据),浏览器将会选择对设备最合适的图像,以确保用户下载尺寸适合他们使用的设备的图像。这是响应式设计中重要组成部分。
截止目前,响应式 Web 设计,其中推荐性的处理方式(哲学),已经延伸至其他应用领域;轻应用如小程序、 快应用 ,新的移动应用框架 Flutter 等,也都是基于 Flex 布局、媒体查询等基础上,融合更多细节策略(诸如:AspectRatio、FittedBox),来支持响应式设计。如想了解更多,可参见 Flutter 文档: Creating responsive and adaptive apps 。
响应式设计 VS 自适应设计
概念区别
响应式设计:
Responsive web design (RWD) is an approach to web design that makes web pages render well on a variety of devices and window or screen sizes. (From Wikipedia)
响应式网页设计是一种使网页能够在不同窗口大小或不同屏幕大小的设备上都良好展示的网页设计技术。
自适应设计:
Adaptive web design(AWD) is a process of server-side detection that chooses a design layout and size to display. The adaptive design will serve different versions of the site (or page) to different devices based on common screen sizes and resolutions. (From Wikipedia)
自适应网页设计,是一种后端检测技术,它(根据用户代理)选择布局和大小合适的页面。自适应设计会根据屏幕尺寸和分辨率,为不同设备提供不同版本的网站(网页)。
尽管两者的结果都是:使得网站在不同的设备上都能布局良好,却从根本上是不同的技术。响应式是一个页面在不同大小的屏幕(窗口)上布局不一样,而自适应是根据用户代理,使用不同版本的网页。
效果区别
- 响应式网站: 倾城之链 (各端访问,提供相同域名)。
- 自适应网站: 京东 (移动端,重定向到其他域名)。
所使用区别
响应式设计所涉及的技术,在上文已做说明。而自适应设计,是需要多个版本的网页;一般而言,移动设备一套,桌面设备一套,服务器收到用户请求后,根据 HTTP 请求中的 UA 头判断设备类型,选择合适的网站版本。主要通过以下几种方法来完成“适配”工作:
- 域名重定向:(
example.com
=>mobile.example.com
) ;
- 路径区分与重定向:(
example.com
=>example.com/mobile/
);
- 多模板与动态选择:同样域名,后端根据 UA 类型选择合适的模板;
整体对比
值得一提的是,采用自适应设计,也需要做「响应式设计」。如果各端设备 UI 交互,与具体逻辑相差不大,强烈建议完全基于响应式设计;反之,可以考虑自适应设计;但,您需要知道的是,多套代码,维护成本相对高。
正如这世间没有完美解决问题方案一样,软件的世界 没有银弹
:响应式编程、声明式编程、函数式编程,相比命令式编程、面向对象编程,虽然为一些场景,提出了更为优雅的解决思路,但同样也并不是完美的存在;比如其学习成本偏高,使得很难快速流行开来(背后是深层数学理论)。但是,如果您能熟练掌握,确实能大幅提升代码的简洁性、“可读性”、可扩展性。知其然,用其妙,不拘一格,必要兼容并包,才能最大限度优化项目、系统以及工作流;如此一来,方能提升效率,节省时间,从而拥有更多时间陪伴爱人、享受美好,学习更多,以打造优良之循环。
2022 年 08 月 08 日写于〔深圳福田〕,原本首发于 Ghost 所构建的博客 响应式编程 | 响应式系统 | 响应式设计 - 静轩之别苑 。
参考文章/文献