本文是Rxjs 响应式编程-第一章:响应式这篇文章的学习笔记。

示例代码地址:【示例代码】

更多文章:【《大史住在大前端》博文集目录】

一. 划重点

三句非常重要的话:

  • 从理念上来理解,Rx模式引入了一种新的“一切皆流”的编程范式
  • 从设计模式的角度来看,Rx模式发布订阅模式迭代器模式的组合使用
  • Rxjs对事件(流)的变换处理,可以对比lodash对数据的处理进行理解。

原文对很多基础却核心的概念都有详细的讲解,本文不再赘述。需要注意的是,理解原理是一方面,但能够熟练使用运算符来转换或查询流信息是需要很长时间积累的,建议在学习过程中,每次遇到新的运算符就主动查阅资料理解其用法,这样积少成多慢慢地就总结出开发模(tao)式(lu)了。

为了更直观地感受面向对象和响应式编程中的不同,笔者分别用两种模式实现了两个一样的小动画,Demo比较简单,就是一个不断奔跑的角色和一个无限滚动的背景图。但是就体会和理解两种开发模式而言基本够用了。

二. 面向对象编程实例

2.1 动画的基本编程范式

动画实例使用canvas画布来完成,简单动画的基本编程模式如下:

//启动函数
function startCanvasAnimation(){
//初始化舞台,舞台对象(或者叫做精灵动画类,帧动画类)
let background = new Background(ctx1,bgImg);
let bird = new Bird(ctx1,roleImg);
//把精灵动画实例集中管理
spirits.push(background);
spirits.push(bird);
//启动一个无限循环绘制暂态动画的递归函数
return requestAnimationFrame(paint)
} //每个绘制周期重复调用的绘制函数
function paint() {
//遍历精灵动画实例集合
for(let spirit of spirits){
spirit.update();//更新自己的参数
spirit.paint();//绘制精灵动画
}
return requestAnimationFrame(paint);//尾递归调用绘制函数
}

当然示例中没有涉及局部更新或其他有关渲染性能的部分,更复杂的动画需求可以直接使用引擎来实现,这不是本篇的重点。

2.2 参考代码

/**
* 角色类
*/
class Role{
constructor(ctx,img){
this.ctx = ctx; //传入画布上下文实例
this.img = img; //传入帧动画用的图片
this.pos = [0,0]; //记录帧动画初始位置
this.step = 68; //帧动画不同帧位置间距
this.index = 0;
this.ratio = 4;
} //更新自身状态
update(){
//此处通过速率控制实现了帧动画待绘制区域在雪碧图中的起始位置
if (!(this.index++ % this.ratio)) {
this.pos[1] = this.pos[1] === 748 ? 0 : this.pos[1] + this.step;
}
} //绘制
paint(){
//将角色绘制在画布的指定位置
this.ctx.drawImage(this.img, this.pos[0] , this.pos[1] , 54 , 64 , 120 , 304, 54, 64);
}
}

背景也可以当做是一个精灵动画实例,以同样的模式定义即可,示例中的角色并没有实现相对画布的运动(也就是视差),感兴趣的读者可以自己尝试实现,完整的示例代码见附件。

2.3 小结

面向对象编程中,具体的精灵类可以继承抽象精灵类,且将具体的实现封装在自己的类定义中,最后使用类似于建造者模式的方法将各个实例组织起来,有面向对象编程经验的读者对这个流程应该不会陌生。

三. 响应式编程实现

在响应式编程中,我们需要构建角色动画流背景动画流这两个可观测对象,然后将这两个流合并起来,此时就得到了一个尚未启动的动画信息流,通过subscribe( )方法启动这个流,并将绘制方法传入回调函数,就可以实现一个同样的动画了。

/**动画的rxjs响应式编程实现*/
//定义动画帧率
var rxjsRatio = 50;
var rxjsFrame = parseInt(1000/rxjsRatio,10);
//构建角色动画流
var roleStream = Rx.Observable.interval(rxjsFrame).map(i=>{return {x:0,y:(i%12)*68}});
//构建背景动画流
var bgiStream = Rx.Observable.interval(rxjsFrame).map(i=> i%800);
//合并流
var rxjsAnim = Rx.Observable.combineLatest(roleStream,bgiStream,(role, bgi)=>{
return {role,bgi}
}).subscribe(rxjsRender); //绘制角色
function rxjsPaintRole(rolePos) {
ctx2.drawImage(roleImg, rolePos.x , rolePos.y , 54 , 64 , 120 , 304, 54, 64);
} //绘制背景
function rxjsPaintBgi(offset) {
let delta = 92;
//绘制左半部分
ctx2.drawImage(bgImg , offset + delta , 0 , 800 + delta - offset , 576 , 0 , 0 , 800 + delta - offset , 400);
//绘制右半部分
ctx2.drawImage(bgImg , delta, 0 , offset, 576 , 800 - offset , 0 , offset , 400);
} //绘制
function rxjsRender(actors) {
rxjsPaintBgi(actors.bgi);
rxjsPaintRole(actors.role);
}

四. 差异对比

4.1 编程理念差异

面向对象编程用类和继承封装多台来聚合关系,响应式编程用流和变换来聚合信息。

通过代码对比可以发现,在响应式编程中,我们不再用对象的概念来对现实世界进行建模,而是使用的思想对信息进行拆分和聚合。在面向对象编程中,数据信息数据更新方法绘制方法这三大要素都是描述具体类的,他们被类的定义聚合在了一起;而在响应式编程中,不再强调“关系”,而是将数据和变化聚合在一起,将处理方式聚合在一起。试想假如上面的示例中增加不同的类,障碍,怪物,积分等等,那么面向对象编程中就需要增加新的类定义,而响应式编程中就需要增加新的数据流,但是在每一个绘制的时间点拿到的暂态数据和根据这些暂态数据进行的绘制动作,其实都是一致的,区别只是关键信息的聚合方式不一样了

4.2 编程体验差异

在传统编程中,我们常常会得到一个无法直接用于最终场景的数据集合,然后需要手动做一些后处理,最终把生成可被使用的数据提供给消费模块;而响应式编程中强调的,是“直接告诉程序你最终想要获得什么数据”,然后将程序的加工流程内化到生产过程中,从而当消费模块得到数据时,直接就可以使用,而不需要再做更多的后处理,这对于消费者来说无疑是体验的提升,就好像你去买组装电脑时,商家都会帮你推荐组件送货上门还会帮你组装好,你肯定感觉服务很到位,因为大部分人的目的是使用电脑,而不是享受买电脑的过程。

4.3 数学思想差异

如果说面向对象编程思想是在描述客观世界,那么响应式编程就更像是在尝试揭示规律。

回过头再来看我们上面实现的Demo,在传统的编程中,我们的思维模式更加倾向于一种微积分的思想,也就是说我们试图描述一个精灵动画的变化时,关注的是如何从x[i]得到x[i+1],当我们得到这样一个变换方法x[i+1]=g(x[i])后,只需要在对象的属性中记录每一个时刻的x[i],然后在下一个绘制周期开始时运行这个方法计算出x[i+1],按照新的值绘制元素,用新值覆盖旧值,然后循环这个过程就可以了;而在响应式编程中,我们采取的方式是为x[i]求出一个通项公式,也就是x = f(i)这样一种数学形式的描述,它们之间的关键区别并不是函数体内逻辑的表达形式,而是在面向对象中实现的方法是有状态的(你需要用某个实例属性来标记帧动画实例当前的执行状态),而响应式编程中的方法是无状态的,是不是联想到什么了?没错,函数式编程中的纯函数。响应式编程本来就是建立在函数式编程基础之上的,只通过纯函数实现集合的映射变换。

如果你听说过傅里叶变换,应该不难发现响应式编程的思维模式和它很像,傅里叶变换可以将一个混杂的信号,拆分成若干个不同振幅频率和相位的正弦波的,这样工程师就可以独立分析自己感兴趣的部分,这是信号分析中很基本的手段。在响应式编程中,系统中的状态变化以类似的方式被拆分成了很多独立的流,如果开发者关注的某个流出现异常,只需要单独关注其数据源和用于流变换的函数链即可(当然它的数据源也可能会被拆分成若干个独立的流),而不必陷入巨大的逻辑关系网,这对于提升大型系统的调试效率来说是非常重要的。在面向对象编程中,这一点是很难做到的,更常见的情况是你修改了A方法,然后B方法就报错了,紧接着你发现这个过程竟然是递归的,最后程序崩溃了,你也崩溃了。

4.3 小结

笔者只是初学,对响应式编程谈不上什么经验,但程序的世界里终究是“没有更好的技术,只有更适合的方案”,在合适的场景做到合适的技术选型才更重要,至于什么样的场景更适合响应式编程,还需要在后续的学习和实践中慢慢体会,但无论如何,响应式编程中蕴含的工程思想和数学之美让我赞叹。

【响应式编程的思维艺术】 (2)响应式Vs面向对象的更多相关文章

  1. 【响应式编程的思维艺术】 (5)Angular中Rxjs的应用示例

    目录 一. 划重点 二. Angular应用中的Http请求 三. 使用Rxjs构建Http请求结果的处理管道 3.1 基本示例 3.2 常见的操作符 四. 冷热Observable的两种典型场景 4 ...

  2. 【响应式编程的思维艺术】 (1)Rxjs专题学习计划

    目录 一. 响应式编程 二. 学习路径规划 一. 响应式编程 响应式编程,也称为流式编程,对于非前端工程师来说,可能并不是一个陌生的名词,它是函数式编程在软件开发中应用的延伸,如果你对函数式编程还没有 ...

  3. 【响应式编程的思维艺术】 (3)flatMap背后的代数理论Monad

    目录 一. 划重点 二. flatMap功能解析 三. flatMap的推演 3.1 函数式编程基础知识回顾 3.2 从一个容器的例子开始 3.3 Monad登场 3.4 对比总结 3.5 一点疑问 ...

  4. eventproxy 介绍这款好用的工具,前端事件式编程的思维

    前端事件式编程 <script src="eventproxy.js"></script> <script> // EventProxy此时是一 ...

  5. [译] Swift 的响应式编程

    原文  https://github.com/bboyfeiyu/iOS-tech-frontier/blob/master/issue-3/Swift的响应式编程.md 原文链接 : Reactiv ...

  6. springboot2 webflux 响应式编程学习路径

    springboot2 已经发布,其中最亮眼的非webflux响应式编程莫属了!响应式的weblfux可以支持高吞吐量,意味着使用相同的资源可以处理更加多的请求,毫无疑问将会成为未来技术的趋势,是必学 ...

  7. (转)Spring Boot 2 (十):Spring Boot 中的响应式编程和 WebFlux 入门

    http://www.ityouknow.com/springboot/2019/02/12/spring-boot-webflux.html Spring 5.0 中发布了重量级组件 Webflux ...

  8. Spring Boot 2 (十):Spring Boot 中的响应式编程和 WebFlux 入门

    Spring 5.0 中发布了重量级组件 Webflux,拉起了响应式编程的规模使用序幕. WebFlux 使用的场景是异步非阻塞的,使用 Webflux 作为系统解决方案,在大多数场景下可以提高系统 ...

  9. [转]springboot2 webflux 响应式编程学习路径

    原文链接 spring官方文档 springboot2 已经发布,其中最亮眼的非webflux响应式编程莫属了!响应式的weblfux可以支持高吞吐量,意味着使用相同的资源可以处理更加多的请求,毫无疑 ...

随机推荐

  1. 图解Raft之日志复制

    日志复制可以说是Raft集群的核心之一,保证了Raft数据的一致性,下面通过几张图片介绍Raft集群中日志复制的逻辑与流程: 在一个Raft集群中只有Leader节点能够接受客户端的请求,由Leade ...

  2. linux crontab 执行mysqldump全局备份为空

    今天遇到个问题,在定时备份时 去查看备份文件,发现大小竟然为0,执行 备份sh文件备份, 备份的sql文件大小正常.试了几种办法. 最终解决办法: 问题原因: 因为我设置的环境变量 就直接在sh中 使 ...

  3. 编译部署 Mysql 5.7

    1.环境准备 RHEL7.4(最小化安装)  64bit   2G 内存 (1G 内存编译将近一个小时) 磁盘空间 15G 以上. 配置为本地yum 源 从MySQL5.7版本开始,安装MySQL需要 ...

  4. sql server创建登录出发器后导致登录失败--解决方案

    1.选择sql server配置管理器---sql server服务--右键属性--启动参数--添加-f.-m两个参数并重启sql server服务 2.重新启动sql server以windos身份 ...

  5. Flutter 读写本地文件

    文档 注意 安装 path_provider 插件后重启f5, 而不是等待热更新 demo import 'dart:io'; import 'dart:async'; import 'package ...

  6. K8s 入门

    中文文档:https://www.kubernetes.org.cn/kubernetes%E8%AE%BE%E8%AE%A1%E6%9E%B6%E6%9E%84 小结大白话 Portainer 挺好 ...

  7. Python函数式编程之闭包

    -------------------------函数式编程之*******闭包------------------------ Note: 一:简介 函数式编程不是程序必须要的,但是对于简化程序有很 ...

  8. C语言复习2_运算符

    今天复习一下C语言的运算符 1.赋值运算符 单等号 = 顺序是:从右往左 2.复合运算符 #include <stdio.h> #include <stdlib.h> int ...

  9. 二分(HDU2289 Cup)

    贴代码: 题目意思:已知r水的下半径,R水的上半径,H为圆台高度,V为水的体积,求水的高度,如图: 水的高度一定在0-100,所以在这个区间逐步二分,对每次二分出的水的高度,计算相应的体积,看看计算出 ...

  10. 如何在微信小程序定义全局变量、全局函数、如何实现 函数复用 模块化开发等问题详解

    1.如何定义全局数据 在app.js的App({})中定义的数据或函数都是全局的,在页面中可以通过var app = getApp();  app.function/key的方式调用,不过我们没有必要 ...