本文是 "Rxjs 響應式編程 第一章:響應式" 這篇文章的學習筆記。 示例代碼地址: "【示例代碼】" 更多文章: "【《大史住在大前端》博文集目錄】" [TOC] 一. 劃重點 三句非常重要的話: 從理念上來理解,Rx模式引入了一種新的 “一切皆流” 的編程範式 從設計模式的角度來看, 是 發佈 ...
目錄
本文是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 小結
筆者只是初學,對響應式編程談不上什麼經驗,但程式的世界里終究是“沒有更好的技術,只有更適合的方案”,在合適的場景做到合適的技術選型才更重要,至於什麼樣的場景更適合響應式編程,還需要在後續的學習和實踐中慢慢體會,但無論如何,響應式編程中蘊含的工程思想和數學之美讓我贊嘆。