前天談到如何避免程式碼散落四處、維護困難,帶大家改寫了物件格式,過了兩天,我突然想到,當時花太多篇幅在解釋觀念和舉例子,結果沒有實際把程式碼秀給大家看,果然還是會有點難懂,對吧?於是我以改寫後的程式碼和改寫前的程式碼擺在一起,讓大家更直接看看物件模型的優勢在哪裡。
這一整段代碼是改寫後,可以針對落葉動畫修改和維護的地方,並且在下方的五張比較圖中,分別採用部分截圖,拿去做比較,因此先上完整代碼:
function leafMaker(x, y, lifeTime){
// 基本屬性
this.pointX = x;
this.pointY = y;
this.img = leafImg;
this.width = 200;
this.height = 200 * this.img.height / this.img.width;
this.isWaitng = false;
// 落葉跟隨滑鼠的屬性
this.originX = x;
this.originY = y;
this.period = 90;
this.timer = 0;
// 落葉自然落下的屬性
this.beginX = x;
this.beginY = y;
this.timestamp = Date.now();
this.lifeTime = lifeTime;
this.rotateTheta = 0 / 180 * Math.PI;
this.rotateOmega = 40 / 180 * Math.PI;
this.revolveTheta = 0 / 180 * Math.PI;
this.revolveOmega = 90 / 180 * Math.PI;
this.fall = function(context, dT){
let rotateNow = this.rotateTheta + this.rotateOmega * dT;
let revolveNow = this.revolveTheta + this.revolveOmega * dT;
let A = Math.sin(revolveNow);
let B = Math.cos(revolveNow);
let C = Math.sin(revolveNow * 1.5);
let D = Math.cos(revolveNow * 1.5);
this.pointX = this.beginX + 500 * A;
this.pointY = this.beginY + 200 * C
+ 100 * dT;
if(this.img.complete){
context.save();
context.translate(this.pointX, this.pointY);
context.font = '32px IBM Plex Sans Arabic';
context.strokeStyle = 'rgba(179, 198, 213, 1)';
let ts = Math.floor((this.lifeTime - dT)*10)/10;
context.textAlign = 'center';
context.strokeText(ts , 0, -150);
context.rotate(rotateNow);
context.drawImage(this.img, -this.width/2, -this.height/2, this.width, this.height);
context.restore();
}
}
this.Refollow = function(frames){
this.originX = this.pointX;
this.originY = this.pointY;
this.timer = frames;
this.period = frames;
}
this.follow = function(context, targetX, targetY){
let dX = targetX - this.originX;
let dY = targetY - this.originY;
if(this.timer > 0){
let t = this.timer;
let p = this.period;
let linear = 1/p;
let easeout = Math.pow(t/p, 2) - Math.pow((t-1)/p, 2);
let easein = Math.pow(1 - (t-1)/p, 2) - Math.pow(1 - t/p, 2);
let a = input.linear;
let b = input.easein;
let c = input.easeout;
this.pointX+= (a * linear + b * easein + c * easeout) * dX;
this.pointY+= (a * linear + b * easein + c * easeout) * dY;
}
else{
this.originX = this.pointX;
this.originY = this.pointY;
}
context.drawImage(this.img, this.pointX-this.width/2, this.pointY-this.height/2, this.width, this.height);
this.timer--;
}
}
首先從初始值來看,並沒有太大的不同,比較不明顯的好處就是,每一個落葉的相關變數都是它的屬性,比如說座標X就要用leaf.originX,可以一目了然,知道這個變數屬於leaf,相比之下,右邊的originX的名稱無法和落葉產生聯繫,有機會讓人混淆。
接著我們從框架去看這件事,因為在閱讀程式碼時,一定從流程步驟開始,去了解「動畫會怎麼循環?」以及「步驟分別有哪些?」,因此這個區域的可讀性相當重要,我們可以看到左側位於32-47行,內容相當簡潔,另一方面單純用變數的做法,右側卻佔據了32-91行,閱讀上較花時間,雖然可以再包成一個函式拉出去寫,然而,有一部分的落葉動畫被寫在這裡面,未來每每要修改落葉的動畫,就必須到框架這一個區塊的程式碼,尋找它,會有額外的搜尋成本。
這邊,我們可以看到,左側的方法是緊接著初始變數定義後,查找方便,而右側是寫在剛剛說的「動畫框架」裡面,相對凌亂,即使如剛剛所說的再包成一個函式,若要查找初始變數的定義,還得去翻開另一份js檔案。
同上所述,左側的方法還是在物件的定義裡面,佔據優勢
事件監聽也是流程的一環,在UX的階段可能去探討User story、設計functional map等等,會有很豐富的使用者的體驗等著去處理,因此對於流程的設計,若能一眼看出每個部件代表的意義,可以避免浪費時間在探討細節。左側可以看到mousemove對應了leaf.Refollow,而click又對應了new leafmaker,相當簡潔;而右側只看到了一大串對於變數的賦值。
左側相當漂亮的利用物件的設計,把落葉的相關程式碼集中在一起,還保留了擴充的彈性;而右側把落葉的動畫分別散佈到各處,每次要修改,就要一次打開三個文件,可謂圖窮匕見,更不用說現在還只是一個落葉、兩種動畫,未來如果有一個角色、兩個寵物、三個道具、四種動畫,不知道右邊的程式碼會有多亂了!
雖然說是重構,不過我簡化了滑鼠的監聽事件(取消長壓的功能),主要功能都一樣,可參考:
https://jerry-the-potato.github.io/Chapter2-demo2-object/
至此,第二章順利落幕了,其實一開始就帶大家用物件撰寫程式碼也不是不行,只是過往的經驗告訴我,一開始就給予太多,只會是揠苗助長,畢竟寫程式響往的是一種自由,若要求大家從物件學起,會成為一種限制,結果就是大家背下物件的用法,卻不知道這麼做有什麼意義,還覺得很麻煩,這樣也是有可能的。
很多人寫文章都認為他懂得你也懂,是有預設條件的,當然這樣也沒問題,畢竟就是程式領域的彼此作交流,只是對於入門者來說,就不太友善了,在我的自學階段,也是中間某個時候開始,我才逐漸知道可以去看mdn文件,學習讀文件的報酬率更大,因為他會預設你還不太懂,講解得清楚又相對容易,如果看不懂,建議去問人,雖然沒有人教怎麼讀文件,但是問著問著,就會找到方法了!
這10天以來也不確定最後我能幫到誰,也許,是那個,花時間複習官方文件和仿間說法的我自己吧!