资讯专栏INFORMATION COLUMN

【响应式编程的思维艺术】 (2)响应式Vs面向对象

Tonny / 2625人阅读

摘要:本文是响应式编程第一章响应式这篇文章的学习笔记。通过代码对比可以发现,在响应式编程中,我们不再用对象的概念来对现实世界进行建模,而是使用流的思想对信息进行拆分和聚合。

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

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

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

[TOC]

一. 划重点

三句非常重要的话:

从理念上来理解,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 小结

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/105300.html

相关文章

  • 响应编程思维艺术】 (1)Rxjs专题学习计划

    摘要:由于技术栈的学习,笔者需要在原来函数式编程知识的基础上,学习的使用。笔者在社区发现了一个非常高质量的响应式编程系列教程共篇,从基础概念到实际应用讲解的非常详细,有大量直观的大理石图来辅助理解流的处理,对培养响应式编程的思维方式有很大帮助。 showImg(https://segmentfault.com/img/bVus8n); [TOC] 一. 响应式编程 响应式编程,也称为流式编程...

    lscho 评论0 收藏0
  • 从命令响应(一)

    摘要:响应式命令式这两种编程风格的思维方式是完全相反的。第二种方式是工人主动去找工人索取生产手机所要的零件,然后生产一台完整的手机,这两种方式就对应的响应式和命令式。 angular2中内置了rxjs,虽然框架本身并没有强制开发者使用响应式风格来组织代码,但是从框架开发团队的角度可以看出他们必然是认同这种编程风格的。rxjs本质是基于函数式编程的响应式风格的库,函数式相对于面向对象来说更加抽...

    JayChen 评论0 收藏0
  • 响应编程思维艺术】 (5)Angular中Rxjs应用示例

    摘要:本文是响应式编程第四章构建完整的应用程序这篇文章的学习笔记。涉及的运算符每隔指定时间将流中的数据以数组形式推送出去。中提供了一种叫做异步管道的模板语法,可以直接在的微语法中使用可观测对象示例五一点建议一定要好好读官方文档。 本文是【Rxjs 响应式编程-第四章 构建完整的Web应用程序】这篇文章的学习笔记。示例代码托管在:http://www.github.com/dashnoword...

    shenhualong 评论0 收藏0
  • 响应编程思维艺术】 (3)flatMap背后代数理论Monad

    摘要:本文是响应式编程第二章序列的深入研究这篇文章的学习笔记。函数科里化的基本应用,也是函数式编程中运算管道构建的基本方法。四资料参考函数式编程指南 本文是Rxjs 响应式编程-第二章:序列的深入研究这篇文章的学习笔记。示例代码托管在:http://www.github.com/dashnowords/blogs 更多博文:《大史住在大前端》目录 showImg(https://segme...

    MorePainMoreGain 评论0 收藏0
  • 响应编程思维艺术】 (4)从打飞机游戏理解并发与流融合

    摘要:本文是响应式编程第三章构建并发程序这篇文章的学习笔记。笔者在自己的实现中又加入了右键切换飞船类型的功能,必须得说开发游戏的确比写业务逻辑要有意思。由于没有精确计算雪碧图的坐标,所以在碰撞检测时会有一些偏差。 本文是Rxjs 响应式编程-第三章: 构建并发程序这篇文章的学习笔记。示例代码托管在:http://www.github.com/dashnowords/blogs 更多博文:《大...

    mudiyouyou 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<