什么是 EaselJS ?
事儿还得从 Flash 说起,因为我最早接触的就是 Flash, 从 Flash 入行编程的
Flash 最早的脚本是 Actionscript2.0 它的 1.0 我是没用过。
Actionscript2.0 与 Javascript 非常像(es3 时代的 Javascript)
后来又推出了完全面向对象的 Actionscript3.0
而毕业后的我也开始入坑成为 Actionscript3.0 编程人员,之后工作需要变成了前端开发人员
我印象中当时还没有专门叫 “前端” 的岗位
这导致了我后来看到 ES5, ES6 版本的 Javascript 后感触很深,甚至有些怨念(想想为啥ES3 与 ES5 中间少了个 ES4?, 还是和 Adobe 与各大公司之间的恩怨有关)
后来的 Javascript 的很多语法都借鉴了 Actionscript3.0,包括 Typescript 也与 Actionscript3.0 非常像
Flash 倒在了时代的滚滚洪流之中, 它的脚本当然也一起被冲走了
后来 Adobe 公司的动画制作工具 Flash Animation 因为要适应 “新时代” 的 HTML5 不得不将脚本适配成 Javascript
CreateJS 框架就是被集成在 Flash Animation 内用于支持 HTML5 的
我后来的前端工作中也在很多活动页中使用过 CreateJS
当然也使用过 Google 推出的 PixiJS 等 EaselJS 分析结束后再深入分析下 PixiJS 的源码看看有啥不同之处吧
PixiJS 它属于后起之秀肯定是优于 CreateJS 的
但由于 CreateJS 的语法与 Actionscript3.0 大致保持一至,这对我这样从 Flash 时代过来的人非常友好,天然亲近
我在工作中更倾向于使用 CreateJS
而 EaselJS 是 CreateJS 框架的一部分,负责 ui 在 canvas 上的渲染与交互
2023 年来回头看 CreateJS 真是遥远啊,现在看它基本上很少再更新了。“贼稳定”
但它依然可以作为你操作 canvas 的基础库,老当益壮,
个人认为它的源码还是非常值得借鉴与参考的
源码下载地址: https://github.com/CreateJS/EaselJS
我重点将从示例代码使用的视角作为切入点,分析 EaselJS 源码如何运行
源码作者的注释相当的详细,连注释都值得借鉴
debugger 说明
看源码最重要的是可以进行 debugger
src/* 下即为未打包的各个 js 源码, 主要分析的就是这里
lib/easeljs-NEXT.js 为js全部源码打包成的一个文件
examples/* 目录为例子,可直接用浏览器打开
examples 内的例子引用的 js 就是 easeljs-NEXT.js
, 由于其未混淆压缩,所以可以直接在此文件内 debugger
后面用到的源码片断是来自 src/easeljs/* 目录下单个类,单个 JS 文件
在单个源码中 debugger 是没有用的,因为还没有构建!!
那么从最简单的示例代码开始入手
通过下面几行简单的代码即可在 canvas 上显示添加的图片并且图片从左向右运动
var stage = new createjs.Stage("canvasElementId"); var image = new createjs.Bitmap("imagePath.png"); stage.addChild(image); createjs.Ticker.addEventListener("tick", handleTick); function handleTick(event) { image.x += 10; stage.update(); }
Stage
第一行 var stage = new createjs.Stage("canvasElementId");
舞台类 Stage 在 src/easeljs/display/Stage.js
构造方法:
function Stage(canvas) { ... }
构造函数通过传入 canvas 或 canvas id 字符串得到 canvas ,通过源码内的说明可以得知,它支持多个 Stage 渲染到单个 canvas 上
紧接着构造函数后的一句
var p = createjs.extend(Stage, createjs.Container);
表示 Stage 类继承自 Container 类
extend 来自通用函数 src/createjs/utils/extend.js
createjs.extend = function(subclass, superclass) { "use strict"; function o() { this.constructor = subclass; } o.prototype = superclass.prototype; return (subclass.prototype = new o()); };
功能很简单,通过方法对象的 prototype 在 Js 中实现继承
Container
容器类 Container 在 src/easeljs/display/Container.js
它是一个可嵌套的显示列表(display list)
在 Container.js 92 行, 表示 Container 继承自 DisplayObject
var p = createjs.extend(Container, createjs.DisplayObject);
并且在最后 708 行有一句,"提升" promote
createjs.Container = createjs.promote(Container, "DisplayObject");
promote 来自通用函数 src/createjs/utils/promote.js
createjs.promote = function(subclass, prefix) { "use strict"; var subP = subclass.prototype, supP = (Object.getPrototypeOf&&Object.getPrototypeOf(subP))||subP.__proto__; if (supP) { subP[(prefix+="_") + "constructor"] = supP.constructor; // constructor is not always innumerable for (var n in supP) { if (subP.hasOwnProperty(n) && (typeof supP[n] == "function")) { subP[prefix + n] = supP[n]; } } } return subclass; };
如果仅仅使用 extend ,那么如果子类 subclass 与父类中有同名方法,父类的方法就无法被子类访问到了
promote 的作用是在子类中创建父类同名方法的引用并带上父类的名称作为前缀
Container 构造函数内第一句就是:
// Container.js 源码 58 行 this.DisplayObject_constructor();
此处就是子类 Container 调用 父类 DisplayObject 构造函数,相当于 super
Container 类的 draw 方法与父类 DisplayObject draw 方法重名,promote 后就可以用 DisplayObject_draw 调用
// Container.js 160 行 p.draw = function(ctx, ignoreCache) { if (this.DisplayObject_draw(ctx, ignoreCache)) { return true; } ...
注意: subP.__proto__
已不被推荐
遵循 ECMAScript 标准,符号 someObject.[[Prototype]] 用于标识 someObject 的原型。
内部插槽 [[Prototype]] 可以通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() >函数来访问。
这个等同于 JavaScript 的非标准但被许多 JavaScript 引擎实现的属性 proto 访问器。
为在保持简洁的同时避免混淆,在我们的符号中会避免使用 obj.proto,而是使用 obj.[[Prototype]] 作为代替。其对应于 Object.getPrototypeOf(obj)。
DisplayObject
再看 DisplayObject 类,在 src/easeljs/display/DisplayObject.js
它继承自 EventDispatcher 类 src/createjs/events/EventDispatcher.js
EventDispatcher 到顶了,不再有继承的父类
很明显,这是一个事件收集与派发类
构造方法:
function EventDispatcher() { this._listeners = null; this._captureListeners = null; }
构造函数内 有私有属性 _listeners
和 _captureListeners
用于分别收集冒泡类与捕捉类的事件
与浏览器提供的原生事件非常相似
继承此类的所有显示对象 DisplayObject 每个单独的显示对象都拥有 addEventListener、 removeEventListener 等事件方法
Bitmap 图像类
使用方法: var image = new createjs.Bitmap("imagePath.png");
图像类 Bitmap 在 src/easeljs/display/Bitmap.js 源码代码量很少,它也继承自 DisplayObject
从 Bitmap.js 的 68 行源码及注释得知,其构造函数支持传递 image, video, canvas (另一个 canvas, 比如用于实现离屏渲染), 也可以是一个也没有 getImage 方法的对象
根据传入的参数构建的图象存入 image 属性内
addChild 添加子显示对象
是 Container 实例方法
stage.addChild(image);
源码如下:
// Container.js 193-207 行 p.addChild = function(child) { if (child == null) { return child; } var l = arguments.length; if (l > 1) { for (var i=0; i<l; i++) { this.addChild(arguments[i]); } return arguments[l-1]; } // Note: a lot of duplication with addChildAt, but push is WAY faster than splice. var par=child.parent, silent = par === this; par&&par._removeChildAt(createjs.indexOf(par.children, child), silent); child.parent = this; this.children.push(child); if (!silent) { child.dispatchEvent("added"); } return child; };
根据源码及对应的注释,可以分析得出:
- 它也可以同时传递多个显示对象如:
addChild(child1, child2, child3)
- 如果添加的 child 原来有父级,需要用
_removeChildAt
将它从原父级中的引用删除
(此外还判断了如果原 parent 父级就是 silent 就为 true 不派发事件) - 将 child 添加至窗口的显示列表 children 中
- 并且根据是否 silent 派发 added 事件
那么 par._removeChildAt()
方法就是根据传递的 index 移除对应位置的子对象并它将的 parent 置为 null
// Container.js 源码 588-595 行 p._removeChildAt = function(index, silent) { if (index < 0 || index > this.children.length-1) { return false; } var child = this.children[index]; if (child) { child.parent = null; } this.children.splice(index, 1); if (!silent) { child.dispatchEvent("removed"); } return true; };
Ticker
Ticker 主要的作用是实现画布的重绘,逐帧重绘
使用例子 createjs.Ticker.addEventListener("tick", handleTick);
Ticker 源码在 src/createjs/utils/Ticker.js
注释中说明此类不能被实例化
Ticker 类也没有继承任何类,它使用 createjs.EventDispatcher.initialize 直接注入了 EventDispatcher 类的方法
// Ticker.js 源码 198 - 208 Ticker.removeEventListener = null; Ticker.removeAllEventListeners = null; Ticker.dispatchEvent = null; Ticker.hasEventListener = null; Ticker._listeners = null; createjs.EventDispatcher.initialize(Ticker); // inject EventDispatcher methods. Ticker._addEventListener = Ticker.addEventListener; Ticker.addEventListener = function() { !Ticker._inited&&Ticker.init(); return Ticker._addEventListener.apply(Ticker, arguments); };
注意在 Ticker._addEventListener = Ticker.addEventListener;
回调函数置换拦截
拦截的目的是注入 !Ticker._inited&&Ticker.init();
用于tick事件添加回调后延迟初始化
意谓着如果没有回调,则不用初始化
如果有tick回调,则会执行 Ticker.init();
// Ticker.js 源码 415 - 423 行 Ticker.init = function() { if (Ticker._inited) { return; } Ticker._inited = true; Ticker._times = []; Ticker._tickTimes = []; Ticker._startTime = Ticker._getTime(); Ticker._times.push(Ticker._lastTime = 0); Ticker.interval = Ticker._interval; };
如果不是 debugger 调试还真难看出是哪里开始自动调用 tick 回调
特别注意 Ticker.init
源码中的 Ticker.interval = Ticker._interval;
这一行
Ticker.interval 作了读取与设置拦截,会分别调用 Ticker._getInterval
和 Ticker._setInterval
方法,
// Ticker.js 源码 401 - 406 行 try { Object.defineProperties(Ticker, { interval: { get: Ticker._getInterval, set: Ticker._setInterval }, framerate: { get: Ticker._getFPS, set: Ticker._setFPS } }); } catch (e) { console.log(e); }
Ticker._setInterval();
内又调用了 Ticker._setupTick()
Ticker._setupTick()
内的再通过条件判断重新调用 Ticker._setupTick()
// Ticker.js 源码 573 - 587 行 Ticker._setupTick = function() { if (Ticker._timerId != null) { return; } // avoid duplicates var mode = Ticker.timingMode; if (mode == Ticker.RAF_SYNCHED || mode == Ticker.RAF) { var f = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame; if (f) { Ticker._timerId = f(mode == Ticker.RAF ? Ticker._handleRAF : Ticker._handleSynch); Ticker._raf = true; return; } } Ticker._raf = false; Ticker._timerId = setTimeout(Ticker._handleTimeout, Ticker._interval); };
源码中默认使用 setTimeout 实现递归调用
也可以通过指定 Ticker.timingMode 来实现使用 requestAnimationFrame实现递归调用 如: createjs.Ticker.timingMode = createjs.Ticker.RAF
所以根据源码中的分析,有三种模式:
- settimeout interval 定时间隔实现帧率 (1000毫秒 / interval)
- RAF_SYNCHED requestAnimationFrame 加上 interval 间隔实现帧率
- RAF 纯 requestAnimationFrame 根据显示器刷新频率(如果显示器刷新频率是 60Hz 那么 每秒间隔 16.6666667 = 1000/60 调用一次)
至此就是循环调用 createjs.Ticker.addEventListener("tick", handleTick); 的 handleTick 回调了
function handleTick(event) { image.x += 10; stage.update(); }
update()
handleTick 回调内调用 stage.update()
即将所有的绘制逻辑绘制至 Stage 舞台上
Stage 类的 update 方法主要做了几件事儿:
- 如果 tickOnUpdate 属性为 true 则调用 Stage.tick 方法,props 参数用于向下传递
- 先后派发 drawstart 和 drawend 事件
- 用 setTransform 重置 context
- 根据条件清掉舞台后开始重绘,注意先不管 updateContext 方法后面再解析它有大作用
- 调用 draw 绘制,draw 内会调用继承的 Container 类上的 draw
- Container 类的 draw 内调用其显示列表内显示对象各自的 draw 方法,这样就完成了显示列表的绘制
// Stage 类 源码 357 - 378 行 p.update = function(props) { if (!this.canvas) { return; } if (this.tickOnUpdate) { this.tick(props); } if (this.dispatchEvent("drawstart", false, true) === false) { return; } createjs.DisplayObject._snapToPixelEnabled = this.snapToPixelEnabled; var r = this.drawRect, ctx = this.canvas.getContext("2d"); ctx.setTransform(1, 0, 0, 1, 0, 0); if (this.autoClear) { if (r) { ctx.clearRect(r.x, r.y, r.width, r.height); } else { ctx.clearRect(0, 0, this.canvas.width+1, this.canvas.height+1); } } ctx.save(); if (this.drawRect) { ctx.beginPath(); ctx.rect(r.x, r.y, r.width, r.height); ctx.clip(); } this.updateContext(ctx); this.draw(ctx, false); ctx.restore(); this.dispatchEvent("drawend"); };
Stage实例方法 update 内为啥还要调用 tick?
继续查看 Stage 的 tick() 内又调用的是 _tick()
而 _tick
再调用 继承的 Container 类的_tick
Container.js 类的源码 553-561 行 _tick()
方法内先调用显示列表内各显示对象的 _tick()
所以它的用处是调用显示对象实例上监听的 tick 事件,意味着可以像下面这样使用,image 为显示对象实例
image.addEventListener('tick', () => { console.log(1111); })
调用完显示列表后再调用继承的 DisplayObject 的 _tick()
因此不仅是 Stage 舞台上的显示对象,Stage 的实例 stage 也可以监听 tick 事件
stage.addEventListener('tick', () => { console.log(1111); })
小结
到此,最基础的基本渲染逻辑走了一遍
- 创建 stage Container 类容器类负责管理显示对象
- 创建 image Bitmap 类用于显示图片
- Tick 类用于更新