目錄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方法就報錯了,緊接着你發現這個過程居然是遞歸的,最後程序崩潰了,你也崩潰了。
筆者只是初學,對響應式編程談不上什麼經驗,但程序的世界裏終究是「沒有更好的技術,只有更適合的方案」,在合適的場景作到合適的技術選型才更重要,至於什麼樣的場景更適合響應式編程,還須要在後續的學習和實踐中慢慢體會,但不管如何,響應式編程中蘊含的工程思想和數學之美讓我讚歎。