open-swf源码阅读笔记-01.时钟与循环
大约 9 分钟
时钟与循环
最近在尝试写一个博客插件,洛克王国版的看板娘,想把宠物放到博客网站上。
于是昨天看了open-flash这个库的的实现,感觉这个库虽然stars不多,但是代码质量却非常高,这里简要对其中的一些代码做一些笔记。
这个库分成了多个子库
- swf-types swf文件的抽象语法树定义,原先叫swf-tree
- swf-parser 解析器
- swf-renderer 渲染器
- domu-player 基于上面这些库实现的播放器
使用
利用下面将要介绍的时钟和循环,程序可以相当简洁和完美!
import { createClock } from "./services/clock";
import { startLoop } from "./services/loop";
import { PausableClock, SchedulableClock, Loop } from "./types";
export class Application {
clock: SchedulableClock & PausableClock;
mainLoop: Loop;
constructor() {
// 这里的时钟可以暂停运行和恢复运行
this.clock = createClock()
// 这里的主循环将受到时钟的控制
// 并且当运行速度低于规定的帧率时,主循环会自动执行那些应当执行但未执行的tick
this.mainLoop = startLoop(this.clock, 1000 / 60, () => this.onTick());
}
onTick() {
}
pause(){
this.clock.pause();
}
resume(){
this.clock.resume();
}
destory() {
this.mainLoop.destroy();
}
}
时钟的接口定义
可以获取当前时间的时钟 Clock
export interface Clock {
getTime(): number;
}
可以延时执行任务的时钟 SchedulableClock
export type TimerHandle = object;
export interface SchedulableClock<H = TimerHandle> extends Clock {
setTimeout(timeout: number, handler: () => any): H;
clearTimeout(handle: H): void;
}
可以暂停和恢复的时钟 PausableClock
export interface PausableClock extends Clock {
isPaused(): boolean;
pause(): void;
resume(): void;
}
可以销毁的循环
export interface Loop {
destroy(): void;
}
Clock时钟
SystemClock系统时钟
import { Clock, PausableClock, SchedulableClock, TimerHandle } from "../types";
// 创建系统时钟
function createSystemClock(): SchedulableClock {
// 系统原生的定时器句柄
type NativeTimerHandle = number | NodeJS.Timer;
// 维护定时器句柄和系统原生的定时器句柄 利用WeakMap实现弱引用
const handles: WeakMap<TimerHandle, NativeTimerHandle> = new WeakMap();
// Object.freeze 可以防止对对象上的属性和值的修改,防止填写新属性,
// 简单来说就是锁定对象的属性
return Object.freeze({
// 提供Date.now获取系统实现
getTime: Date.now,
// 创建延时执行的定时器,返回一个定时器句柄,
// 这个句柄不是系统原生句柄,这样这个定时器句柄就只能被自己销毁。
setTimeout(timeout: number, handler: () => any): TimerHandle {
// 创建系统原生定时任务,得到系统原生定时器句柄
const nativeHandle: NativeTimerHandle = setTimeout(handler, timeout);
// 创建一个唯一的符号作为定时器句柄来代表这个系统原生定时器
const handle: TimerHandle = new Object(Symbol());
// 将定时器句柄和系统原生定时器句柄作为键值对保存
handles.set(handle, nativeHandle);
// 返回定时器句柄
return handle;
},
// 取消一个定时任务,需要提供一个定时器句柄而不是系统原生定时器句柄
clearTimeout(handle: TimerHandle): void {
// 通过定时器句柄找到系统原生定时器句柄
const nativeHandle: NativeTimerHandle | undefined = handles.get(handle);
// 如果能找到这个系统原生定时器句柄
if (nativeHandle !== undefined) {
// 通过系统原生定时器句柄来取消定时任务
clearTimeout(nativeHandle as any);
}
},
});
}
// 系统时钟常量
export const SYSTEM_CLOCK: SchedulableClock = createSystemClock();
ChildClock子时钟
/**
* Represents a currently scheduled task in a `ChildClock`.
* 代表了ChildClock子时钟上的一个延时执行的任务
*/
interface Task {
/**
* Time in the current clock when this task should be executed.
* 目标时间,当前任务应当被执行的时间,这个时间是ChildClock子时钟的时间
*/
targetTime: number;
/**
* Timer handle received by the parent clock when scheduling the task.
* This is undefined when the task is not scheduled (when the clock is paused).
* 一个定时器句柄。
* 当子时钟运行时,新添加的定时任务会派发给父时钟,父时钟会返回一个定时器句柄。
* 当子时钟暂停后,新添加的定时任务不会派发给父时钟,所以句柄为undefined。
* 当子时钟暂停时,所有定时任务都要通过这个句柄来向父时钟取消
* 当子时钟恢复时,所有定时任务又需要全部重新派发给父时钟,同时将返回的定时器句柄保存。
*/
handle?: TimerHandle;
/**
* Handler function triggered once the timeout is complete.
* 一个定时回调函数,会被父时钟执行一次。
*/
handler(): any;
}
/**
* Represents a node in a clock tree.
* ChildClock代表时钟树上的一个节点
* 可以暂停,可以派发定时任务
*/
export class ChildClock implements PausableClock, SchedulableClock {
// 父时钟是一个SchedulableClock可以派发任务的时钟
private readonly parent: SchedulableClock;
/**
* Origin of time for this clock.
* Relative to parent clock, or the UNIX epoch for the root clock.
* Resuming the clock will update the update to maintain the continuity of the time (no time is skipped).
* 当前时钟树的时间
* 相对于父时钟或UNIX时间
* 当暂停时,这个epoch会更新,已保证当前时钟的连续不间断。
*/
private epoch: number;
/**
* If this clock is not paused `undefined`, otherwise it is the time in the parent clock when `pause` was called.
* 暂停时间,该时间是父时钟的时间。
*/
private pausedAt: number | undefined;
/**
* Map from outer handles to task states.
* 定时器句柄和定时任务的句柄
* 这个定时器任务句柄不是父时钟返回的原始句柄,
* 而是当前时钟为该任务创建的唯一的句柄
* 这样用户就不能拿着这个句柄像父时钟取消
* 要取消这个任务只有当前时钟能取消。
*/
private readonly tasks: Map<TimerHandle, Task>;
/**
* @param parent Parent clock
* @param initialEpoch Time in the parent clock corresponding to the zero time in the child clock.
* Default is parent.getTime().
*
* parent:构造函数,需要提供一个父时钟
* initialEpoch:和当前时钟节点的初始时间
* + 在联机对战游戏中,应该就可以把这个initialEpoch设置为服务器时钟的时间。
* + 这样所有客户端都将以服务端的时钟运行。
* + 服务器运行在第10个tick,客户端也将运行在第10个tick
*/
constructor(parent: SchedulableClock, initialEpoch?: number) {
this.parent = parent;
// 默认值为父时钟时间,这意味着,当前时钟getTime()会得到从0开始的值
this.epoch = initialEpoch !== undefined ? initialEpoch : parent.getTime();
this.pausedAt = undefined;
// 这里没有使用弱引用,因为这里就是保存task数据的地方
this.tasks = new Map();
}
// 判断是否暂停
isPaused(): boolean {
return this.pausedAt !== undefined;
}
/**
* @return Time in milliseconds
* 获取当前时间,这个时间是当前时钟的时间。
* 这个时钟通过父时钟来计算,这意味着当父时钟暂停时,当前时钟也会暂停。
*/
getTime(): number {
// 这里源作者的代码有错误,下面是已经修改过后的
// epoch可以认为是当前时钟开始运行时的那一刻父时钟的时间
// 当在运行时,当前时间就是 父时钟当前时间-epoch开始时间
// 当在暂停时,当前时间就是,暂停的那一刻父时钟的时间pausedAt - epoch开始时间
return this.pausedAt !== undefined
? this.pausedAt - this.epoch
: this.parent.getTime() - this.epoch;
}
// 暂停时钟
pause(): void {
if (this.pausedAt === undefined) {
// 记录暂停时父时钟的时间
this.pausedAt = this.parent.getTime();
// 取消所有已经派发的任务
for (const task of this.tasks.values()) {
if (task.handle !== undefined) {
this.parent.clearTimeout(task.handle);
task.handle = undefined;
}
}
}
}
// 恢复时钟的运行
resume(): void {
if (this.pausedAt !== undefined) {
// 修改epoch,这是为了保证this.getTime()始终能获取到当前时钟的时间,保证当前时间的连续不间断。
// this.epoch+=父时钟经过的时间
this.epoch += this.parent.getTime() - this.pausedAt;
this.pausedAt = undefined;
// 恢复所有任务
for (const task of this.tasks.values()) {
task.handle = this.parent.setTimeout(
// targetTime是在当前时钟的时间,减去当前时间就是延迟时间
task.targetTime - this.getTime(),
task.handler
);
}
}
}
setTimeout(timeout: number, handler: () => any): TimerHandle {
// 创建符号代表当前时钟的任务句柄
const outerHandle: TimerHandle = new Object(Symbol());
// 创建任务
const task: Task = {
// 目标时间是当前时钟的时间+延迟
targetTime: this.getTime() + timeout,
handler: (): void => {
// 执行后需要销毁任务数据
this.tasks.delete(outerHandle);
// 执行任务
handler();
},
handle: undefined,
};
// 保存任务
this.tasks.set(outerHandle, task);
// 当前时钟没有暂停就时钟想父时钟派发任务
if (this.pausedAt === undefined) {
// 保存父时钟返回的任务句柄
task.handle = this.parent.setTimeout(timeout, task.handler);
}
// 返回子时钟的任务句柄
return outerHandle;
}
// 提供一个当前时钟的任务句柄,销毁任务
clearTimeout(handle: TimerHandle): void {
// 通过任务句柄查询任务
const task: Task | undefined = this.tasks.get(handle);
if (task !== undefined) {
// 有父时钟句柄说明这个任务在父时钟里面
// 因为当当前时钟暂停时,这些任务已经从父时钟里取消了
if (task.handle !== undefined) {
// 通过任务的父时钟句柄来销毁任务
this.parent.clearTimeout(task.handle);
}
// 在当前时钟里取消任务
this.tasks.delete(handle);
}
}
}
/**
* Create a new root clock based off real time.
* Its epoch defaults to `now`. You can provide you own epoch relative to the UNIX epoch (expressed in milliseconds).
*
* @param {number} epoch
* @return {Clock}
*
* 创建一个可暂停、可派发任务的时钟,这个时钟基于系统时钟。
* epoch默认为创建时间,这意味着这个时钟getTime()是从0开始运行的。
*/
export function createClock(
epoch: number = SYSTEM_CLOCK.getTime()
): SchedulableClock & PausableClock {
return new ChildClock(SYSTEM_CLOCK, epoch);
}
loop循环
这个循环基于时钟来实现,个人感觉这段代码写的非常漂亮。
import { SchedulableClock, TimerHandle, Loop } from "../types";
/**
* The first tick is after `1000 / frameRate` ms.
*
* @param clock Clock used to control the loop. 一个用来控制当前循环运行的时钟
* @param frameRate Ticks per second 帧率
* @param onTick 回调函数
* @return {Loop}
*/
export function startLoop(
clock: SchedulableClock,
frameRate: number,
onTick: () => any
): Loop {
// 用来记录当前时刻的时间和上一时刻的时间,方便推算经过的时间
const startTime: number = clock.getTime();
let oldTime: number = startTime;
//
let shift: number = 0;
// 记录tick数
let nextTickCount: number = 0;
// 记录定时器回调的句柄,通过这个句柄能结束循环
let handle: TimerHandle | undefined = undefined;
// 定时回调函数
function handleTick(): void {
onTick(); // 执行tick
scheduleNextTick(); // 安排下一次tick
}
function scheduleNextTick(): void {
// 获取当前时间
const curTime: number = clock.getTime();
if (nextTickCount > 0 && curTime === oldTime) {
// tslint:disable-next-line:max-line-length
const infoUri: string =
"https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#Reduced_time_precision";
console.warn(
`\`curTime === oldTime\`, possible reduced time precision, see ${infoUri}`
);
}
oldTime = curTime;
// console.log(clock.getTime());
// 自增
nextTickCount++;
// 计算下一时刻的时间,可以先不看shift
// targetTime = startTime开始时间 + fps周期 * nextTickCount周期数
// shift 是为了在tick的执行速度太慢时
const targetTime: number =
startTime + shift + (1000 * nextTickCount) / frameRate;
// timeout延迟执行时间就是目标时间减去当前时间
let timeout: number = targetTime - clock.getTime();
// timeout小于0,说明tick执行的速度小于fps的规定
if (timeout < 0) {
// -timeout就是慢了多少时间,
// 利用shift就可以在下一次计算时保证targetTime正确。
shift += -timeout;
console.warn(`Unable to maintain frameRate (missed by ${-timeout}ms)`);
// tick的执行速度慢了,所以这一次要立即执行。
timeout = 0;
}
// 但是上面的if处理逻辑似乎存在问题,
// 就是当timeout非常大时,可能已经慢了很多个tick了,
// 然而这里只执行了一个tick。这会导致tick丢失,这在某些情况下可能会产生问题(如游戏)
// 这里应该这么修改,以代替上面的if逻辑:
// while(timeout < 0){ // 保证timeout为正
// onTick(); // 执行tick,另外,如果是游戏的话,这里应该执行update更新数据,不需要执行render渲染
// timeout += 1000 / frameRate;
// nextTickCount++;
// }
// 通过时钟创建下一个定时器
handle = clock.setTimeout(timeout, handleTick);
}
scheduleNextTick();
function destroy(): void {
if (handle !== undefined) {
clock.clearTimeout(handle);
handle = undefined;
}
}
return { destroy };
}