你必定注意到博文的標題變了成了「TypeScript 版 ...」。在上一篇 JavaScript 版俄羅斯方塊——轉換爲 TypeScript 中,它就變成了 TypeScript 實現。而在以前的 JavaScript 版俄羅斯方塊——重構 中,只重構了數據結構部分,控制(業務邏輯)部分由於過於複雜,只是進行了表面的重構。因此如今來對控制部分進行更深刻的重構。javascript
傳送門
重構不是盲目的,必定仍是要先進行一些分析。html
Puzzle
職責很明確,負責繪製,除此以外,剩下的就是數據、狀態和對它們的控制。java
從上圖能夠看出來,用於繪製的數據主要就是 block
和 matrix
了。對於 block
,須要控制它的位置變更和旋轉,而 block
降低到底以後,會經過 固化
變成 matrix
的部分數據,而因爲 固化
形成 matrix
數據變更以後,可能會產生若干整行有效數據,這時候須要觸發 刪除行
操做。全部 block
和 matrix
的變更,都應該引發 Puzzle
的重繪。處理這部分控制過程的對象,且稱之爲 BlockController
。node
遊戲過程當中方塊會定時下落,這是由 Timer
控制的。Timer
每達到一個 interval
所指示的時間,就會向 BlockController
發送消息,通知它執行一次 moveDown
操做。jquery
block
從 固化
操做開始,直到 刪除行
操做完成這一段時間,不該處理 Timer
的消息。考慮到這一過程結束時最好不須要等到下一時鐘週期,因此在這段時間最好中止 Timer
,因此這裏應該通知暫停。git
說到暫停,在以前就分析過,除了 BlockController
要求的暫停外,還有多是用戶手工請求暫暫停。只有當兩種暫停狀態都取消的時候,才應該繼續下落方塊。因此這裏須要一個 StateManager
來管理狀態,除了暫停外,順便把遊戲的 over
狀態一併管理了。因此 StateManager
須要接受 BlockController
和 CommandPanel
的消息,並根據狀態計算結果來通知 Timer
是暫停仍是繼續。es6
另外一方面,因爲 BlockController
有 刪除行
操做,這個操做的發生意味着要給用戶加分,因此須要通知 InfoPanel
加分。而 InfoPanel
加分到必定程度會引發加速,它須要本身內部判斷並處理這個過程。不過加速就意味着時鐘週期的變更,因此須要通知 Timer
。typescript
按照圖示及上述過程,其實在以前的版本已經基本實現,相互之間的通知實現得並不十分清晰,部分是經過事件來實現的,也有部分是經過直接的方法調用來實現的。顯然,深刻重構就是要把這個結構搞清楚。npm
各控制器之間須要要相互通知,並根據獲得的通知來進行處理。若是有一個統一的消息(通知)處理中心,結構會不會看起來更簡單一些呢?json
BlockController
其實上已經處理了大部分以前 Tetris
所作的工做。因此不妨把 Tetris
改名爲 BlockController
,再新建個 Tetris
來專門處理各類通知。通知統一經過事件來實現,不過若是涉及到一些較長的過程(好比刪除動畫),能夠考慮經過 Promise 來實現。
BlockController
要管理 block
和 matrix
兩個數據,還要處理 block
的移動和變形,以及處理 block
的固化,以及 matrix
的刪除行操做等,甚至還負責了刪除行動畫的實現。
因此爲了簡化代碼結構,BlockController
應該專一於 block
的管理,其它的操做,應該由別的類來完成,好比 MatrixController
、EraseAnimator
等。
爲了將 BlockController
從「繁忙的事務」中解救出來,首先是解耦。解耦比較流行的思想是 IoC(Inversion of Control,控制反轉) 或者 DI(Dependency Injection,依賴注入)。不過這裏用的是另外一種思想,消息驅動,或者事件驅動。通常狀況下消息驅動用於異步處理,而事件驅動用於同步處理。這個程序中基本上都是同步過程,因此採用事件便可。
雖然以前的 JavaScript 版就已經用到了事件,不過處理的過程有限。常常上圖的分析,對須要處理的事件進行了擴展。另外因爲以前是直接使用的 jQuery 的事件,用起來有點繁瑣,處理函數的第一個參數必定是是 event 對象,而 event 對象實際上是不多用的。因此先實現一個本身的 Eventable
。
事件支持看起來好像多複雜同樣,但實際上很是簡單。
首先,事件處理的外部接口就三個:
on
註冊事件處理函數,就是將事件處理函數添加到事件處理函數列表off
註銷事件處理函數,即從事件處理函數列表中刪除處理函數trigger
觸發事件(一般是內部調用),依次調用對應的事件處理函數事件都有名稱,對應着一個事件處理函數列表。爲了便於查找事件,這應該定義爲一個映射表,其鍵是事件名稱,值爲處理函數列表。TypeScript 能夠用接口來描述這個結構
interface IEventMap { [type: string]: Array<(data?: any) => any>; }
Eventable
對象中會維護一上述的映射表對象
private _events: IEventMap;
on(type: string, handler: Function)
註冊一個事件名爲 type
的處理函數。因此,是從 _events
裏找到(或添加)指定名稱的列表,並在列表裏添加 handler
(this._events[type] || (this._events[type] = [])).push(handler);
若是不但願 type
區分大小寫,能夠首先對 type
進行 toLowerCase()
處理。
在上面已經把 _events
的結構說清楚了,off()
的處理就容易理解了。若是 off()
沒有參數,直接把 _events
清空或者從新賦值一個新的 {}
便可;若是 off(type: string)
這種形式的調用,則從 delete _events[type]
就能達到目的;只有在給了 handler
的時候麻煩一點,須要先取出列表,再從列表中找到 handler
,把它去除掉。
trigger()
的處理過程就更容易了,按 type
找到列表,遍歷,依次調用便可。
以前一直很糾結一個問題:若是要把 Eventable
作成像 jQuery 同樣的鏈式調用,那就必須 return this
,可是若是把方法定義爲 Eventable
類型,子類實現的時候就只能鏈調 Eventable
的方法,而不是子類的方法(由於返回固定的 Eventable
類型。後來終於從 StackOverflow 上查到答案就在文檔中:Advanced Types : Polymorphic this types。
原來能夠將方法定義爲 this
類型。是的,這裏的 this
表示一種類型而不是一個對象,表示返回的是本身。返回類型會根據調用方法的類來決定,即便子類調用的是父類中返回 this
的方法,也能夠識別爲返回類型是子類類型。
class Father { test(): this { return this; } } class Son extends Father { doMore(): this { return this; } } // 這會識別出 test() 返回 Son 類型而不是 Father 類型 // 因此能夠直接調用 doMore() new Son().test().doMore();
IoC 和 DI 實現,像 Java 的 Spring,.NET 的 Unity,一般都會有一個集中配置的地方,有多是 XML,也有多是 @Configure 註釋的 Config 類(Spring 4)等……
這裏也採用這種思想,寫一個類來集中配置事件。以前已經將 Tetris
的事情交給了 BlockController
去處理,這裏用 Tetris
來處理這個事情正好。
class Tetris { constructor() { // 生成各部件的實例 } private setup() { this.setupEvents(); this.setupKeyEvents(); } private setupEvents() { // 將各部件的實例之間用事件關聯起來 } private setupKeyEvents() { // 處理鍵盤事件 // 從 BlockController 中拆分出來的鍵盤事件處理部分 } run() { // 開始 BlockController 的工做 // 並啓動 Timer } }
刪除行這部分邏輯相對獨立,能夠從 BlockController
中剝離出來,取名 Eraser
。那麼 Eraseer
須要處理的事情包括
check()
rowCount
erase()
其中 erase()
中須要經過 setInterval()
來控制刪除動畫,這是一個異步過程。因此須要回調,或者 Promise …… 不過既然是爲了作技術嘗試,不妨用新一點的技術,async/await 怎麼樣?
Eraser 的邏輯部分是直接照搬原來的實現,因此這裏主要討論 async/await 實現。
TypeScript 的編譯目標參數 target
設置爲 es2015
或者 es6
的時候,容許使用 async/await 語法,它編譯出來的 JavaScript 是使用 es6 的 Promise
來實現的。而咱們須要的是 es5 語法的實現,因此又得靠 Babel 了。Babel 的 presets es2017
、stage-3
等都支持將 async/await 和 Promise 轉換成 es5 語法。
不過此次使用 Babel 不是從 JavaScript 源文件編譯成目標文件。而是利用 gulp 的流管道功能,將 TypeScript 的編譯結果直接送給 Babel,再由 Babel 轉換以後輸出。
這裏須要安裝 3 個包
npm install --save-dev gulp-babel babel-preset-es2015 babel-preset-stage-3
同時須要修改 gulpfile.js 中的 typescript
任務
gulp.task("typescript", callback => { const ts = require("gulp-typescript"); const tsProj = ts.createProject("tsconfig.json", { outFile: "./tetris.js" }); const babel = require("gulp-babel"); const result = tsProj.src() .pipe(sourcemaps.init()) .pipe(tsProj()); return result.js .pipe(babel({ presets: ["es2015", "stage-3"] })) .pipe(sourcemaps.write("../js", { sourceRoot: "../src/scripts" })) .pipe(gulp.dest("../js")); });
請注意到 typescript
任務中 ts.createProject()
中覆蓋了配置中的 outFile
選項,將結果輸出爲 npm 項目所在目錄的文件。這是一個 gulp 處理過程當中虛擬的文件,並不會真的存儲於硬盤上,但 Babel 會覺得它獲得的是這個路徑的文件,會根據這個路徑去 node_modules
中尋找依賴庫。
編譯沒問題了,但運行會有問題,由於缺乏 babel-polyfill,也就是 Babel 的 Promise 實現部分。先經過 npm 添加包
npm install --save-dev babel-polyfill
這個包下面的 dist/polyfill.min.js
須要在 index.html
中加載。因此在 gulpfile.js 中像處理 jquery.min.js
那樣,在 libs
任務中加一個源便可。以後運行 gulp build
會將 polyfill.min.js
拷貝到 /js
目錄中。
關於 async/await 語法,我曾在 閒談異步調用「扁平」化 一文中討論過。雖然那篇博文中只討論了 C# 而不是 JavaScript 的 async/await,可是最後那部分使用了 co 庫的 JavaScript 代碼對理解 async/await 頗有幫助。
在 co 的語法中,經過 yield
來模擬了 await
,而 yeild
後面接的是一個 Promise 對象。await
後面跟着的民是一個 Promise 對象,而它「等待」的,就是這個 Promise 的 resolve,並將 resolve 的的值傳遞出去。
相應的,async
則是將一個返回 Promise 的函數是能夠等待的。
因爲 await
必須出如今 async
函數中,因此最終調用 async erase()
的部分用 async IIFE 實現:
(async () => { // do something before this._matrix = await eraser.erase(); // do something after // do more things })();
上面的代碼 IIFE 中 await
後面的部分至關於被封裝成了一個 lambda,做爲 eraser.erase().then()
的第一個回調,即
// 等效代碼 (() => { // do something before eraser.erase().then(r => { this._matrix = r; // do something after // do more things }); })();
這個程序結構比較簡單,並不能很好的體現 async/await 的好處,不過它對於簡化瀑布式回調和 Promise 的 then 鏈確實很是有效。
之前對於 Matrix
這個類是加了刪、刪了加,一直沒能很好的定位。如今因爲程序結構已經發生了較大的變化,Matrix
的功能也能更清晰的定義出來了。
createRow()
、createMatrix()
width
和 height
Block
的各個點固化下來 - addBlockPoints()
BlockPoint
對象 - set()
getFullRows()
removeRows()
BlockPoint
列表 - fasten()
isPutable()
JavaScript/TypeScript 版俄羅斯方塊是以技術研究爲目的而寫,到此已經能夠告一段落了。因爲它不是以遊戲體驗爲目的寫的一個遊戲程序,因此在體驗上還有不少須要改進的地方,就留給有興趣的朋友們研究了。
傳送門