目录html
本文是Rxjs 响应式编程-第一章:响应式这篇文章的学习笔记。前端
示例代码地址:【示例代码】git
更多文章:【《大史住在大前端》博文集目录】github
三句很是重要的话:编程
Rx模式
是发布订阅模式和迭代器模式的组合使用Rxjs
对事件(流)的变换处理,能够对比lodash
对数据的处理进行理解。原文对不少基础却核心的概念都有详细的讲解,本文再也不赘述。须要注意的是,理解原理是一方面,但可以熟练使用运算符来转换或查询流信息是须要很长时间积累的,建议在学习过程当中,每次遇到新的运算符就主动查阅资料理解其用法,这样聚沙成塔慢慢地就总结出开发模(tao)式(lu)了。canvas
为了更直观地感觉面向对象和响应式编程中的不一样,笔者分别用两种模式实现了两个同样的小动画,Demo比较简单,就是一个不断奔跑的角色和一个无限滚动的背景图。可是就体会和理解两种开发模式而言基本够用了。segmentfault
动画实例使用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);//尾递归调用绘制函数 }
固然示例中没有涉及局部更新或其余有关渲染性能的部分,更复杂的动画需求能够直接使用引擎来实现,这不是本篇的重点。函数式编程
/** * 角色类 */ 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); } }
背景也能够当作是一个精灵动画实例,以一样的模式定义便可,示例中的角色并无实现相对画布的运动(也就是视差),感兴趣的读者能够本身尝试实现,完整的示例代码见附件。函数
面向对象编程中,具体的精灵类能够继承抽象精灵类,且将具体的实现封装在本身的类定义中,最后使用相似于建造者模式的方法将各个实例组织起来,有面向对象编程经验的读者对这个流程应该不会陌生。
在响应式编程中,咱们须要构建角色动画流
和背景动画流
这两个可观测对象,而后将这两个流合并起来,此时就获得了一个还没有启动的动画信息流
,经过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); }
面向对象编程用类和继承封装多台来聚合关系,响应式编程用流和变换来聚合信息。
经过代码对比能够发现,在响应式编程中,咱们再也不用对象
的概念来对现实世界进行建模,而是使用流
的思想对信息进行拆分和聚合。在面向对象编程中,数据信息,数据更新方法,绘制方法这三大要素都是描述具体类的,他们被类的定义聚合在了一块儿;而在响应式编程中,再也不强调“关系”,而是将数据和变化聚合在一块儿,将处理方式聚合在一块儿。试想假如上面的示例中增长不一样的类,障碍,怪物,积分等等,那么面向对象编程中就须要增长新的类定义,而响应式编程中就须要增长新的数据流,可是在每个绘制的时间点拿到的暂态数据和根据这些暂态数据进行的绘制动做,其实都是一致的,区别只是关键信息的聚合方式不同了。
在传统编程中,咱们经常会获得一个没法直接用于最终场景的数据集合,而后须要手动作一些后处理,最终把生成可被使用的数据提供给消费模块;而响应式编程中强调的,是“直接告诉程序你最终想要得到什么数据”,而后将程序的加工流程内化到生产过程当中,从而当消费模块获得数据时,直接就可使用,而不须要再作更多的后处理,这对于消费者来讲无疑是体验的提高,就好像你去买组装电脑时,商家都会帮你推荐组件送货上门还会帮你组装好,你确定感受服务很到位,由于大部分人的目的是使用电脑,而不是享受买电脑的过程。
若是说面向对象编程思想是在描述客观世界,那么响应式编程就更像是在尝试揭示规律。
回过头再来看咱们上面实现的Demo,在传统的编程中,咱们的思惟模式更加倾向于一种微积分
的思想,也就是说咱们试图描述一个精灵动画的变化时,关注的是如何从x[i]
获得x[i+1]
,当咱们获得这样一个变换方法x[i+1]=g(x[i])
后,只须要在对象的属性中记录每个时刻的x[i]
,而后在下一个绘制周期开始时运行这个方法计算出x[i+1]
,按照新的值绘制元素,用新值覆盖旧值,而后循环这个过程就能够了;而在响应式编程中,咱们采起的方式是为x[i]
求出一个通项公式,也就是x = f(i)
这样一种数学形式的描述,它们之间的关键区别并非函数体内逻辑的表达形式,而是在面向对象中实现的方法是有状态的(你须要用某个实例属性来标记帧动画实例当前的执行状态),而响应式编程中的方法是无状态的,是否是联想到什么了?没错,函数式编程中的纯函数。响应式编程原本就是创建在函数式编程基础之上的,只经过纯函数实现集合的映射变换。
若是你据说过傅里叶变换
,应该不难发现响应式编程的思惟模式和它很像,傅里叶变换能够将一个混杂的信号,拆分红若干个不一样振幅频率和相位的正弦波的,这样工程师就能够独立分析本身感兴趣的部分,这是信号分析中很基本的手段。在响应式编程中,系统中的状态变化以相似的方式被拆分红了不少独立的流,若是开发者关注的某个流出现异常,只须要单独关注其数据源和用于流变换的函数链便可(固然它的数据源也可能会被拆分红若干个独立的流),而没必要陷入巨大的逻辑关系网,这对于提高大型系统的调试效率来讲是很是重要的。在面向对象编程中,这一点是很难作到的,更常见的状况是你修改了A方法,而后B方法就报错了,紧接着你发现这个过程居然是递归的,最后程序崩溃了,你也崩溃了。
笔者只是初学,对响应式编程谈不上什么经验,但程序的世界里终究是“没有更好的技术,只有更适合的方案”,在合适的场景作到合适的技术选型才更重要,至于什么样的场景更适合响应式编程,还须要在后续的学习和实践中慢慢体会,但不管如何,响应式编程中蕴含的工程思想和数学之美让我赞叹。