你必定注意到博文的标题变了成了“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 版俄罗斯方块是以技术研究为目的而写,到此已经能够告一段落了。因为它不是以游戏体验为目的写的一个游戏程序,因此在体验上还有不少须要改进的地方,就留给有兴趣的朋友们研究了。
传送门