什么是 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;
};

根据源码及对应的注释,可以分析得出:

  1. 它也可以同时传递多个显示对象如: addChild(child1, child2, child3)

  2. 如果添加的 child 原来有父级,需要用 _removeChildAt 将它从原父级中的引用删除

    (此外还判断了如果原 parent 父级就是 silent 就为 true 不派发事件)

  3. 将 child 添加至窗口的显示列表 children 中

  4. 并且根据是否 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._getIntervalTicker._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

所以根据源码中的分析,有三种模式:

1.settimeout interval 定时间隔实现帧率 (1000毫秒 / interval)

2.RAF_SYNCHED requestAnimationFrame 加上 interval 间隔实现帧率

3.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 方法主要做了几件事儿:

  1. 如果 tickOnUpdate 属性为 true 则调用 Stage.tick 方法,props 参数用于向下传递
  2. 先后派发 drawstart 和 drawend 事件
  3. 用 setTransform 重置 context
  4. 根据条件清掉舞台后开始重绘,注意先不管 updateContext 方法后面再解析它有大作用
  5. 调用 draw 绘制,draw 内会调用继承的 Container 类上的 draw
  6. 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);
})

小结

到此,最基础的基本渲染逻辑走了一遍

1、创建 stage Container 类容器类负责管理显示对象

2、创建 image Bitmap 类用于显示图片

3、Tick 类用于更新

下一篇将分析 DisplayObject 子类的 draw 是如何 draw 的


博客园: http://cnblogs.com/willian/

github: https://github.com/willian12345/

EaselJS 源码分析系列--第一篇的更多相关文章

  1. 鸿蒙源码分析系列(总目录) | 百万汉字注解 百篇博客分析 | 深入挖透OpenHarmony源码 | v8.23

    百篇博客系列篇.本篇为: v08.xx 鸿蒙内核源码分析(总目录) | 百万汉字注解 百篇博客分析 | 51.c.h .o 百篇博客.往期回顾 在给OpenHarmony内核源码加注过程中,整理出以下 ...

  2. [Tomcat 源码分析系列] (二) : Tomcat 启动脚本-catalina.bat

    概述 Tomcat 的三个最重要的启动脚本: startup.bat catalina.bat setclasspath.bat 上一篇咱们分析了 startup.bat 脚本 这一篇咱们来分析 ca ...

  3. Thinkphp源码分析系列–开篇

    目前国内比较流行的php框架由thinkphp,yii,Zend Framework,CodeIgniter等.一直觉得自己在php方面还是一个小学生,只会用别人的框架,自己也没有写过,当然不是自己不 ...

  4. MyBatis 源码分析系列文章导读

    1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...

  5. Spring IOC 容器源码分析系列文章导读

    1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本.经过十几年的迭代,现在的 Spring 框架已经非常成熟了.Spring ...

  6. 鸿蒙内核源码分析(字符设备篇) | 字节为单位读写的设备 | 百篇博客分析OpenHarmony源码 | v67.01

    百篇博客系列篇.本篇为: v67.xx 鸿蒙内核源码分析(字符设备篇) | 字节为单位读写的设备 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一 ...

  7. 鸿蒙内核源码分析(文件概念篇) | 为什么说一切皆是文件 | 百篇博客分析OpenHarmony源码 | v62.01

    百篇博客系列篇.本篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一切皆是文件 | 51.c.h.o 本篇开始说文件系统,它是内核五大模块之一,甚至有Linux的设计哲学是" ...

  8. 鸿蒙内核源码分析(GN应用篇) | GN语法及在鸿蒙的使用 | 百篇博客分析OpenHarmony源码 | v60.01

    百篇博客系列篇.本篇为: v60.xx 鸿蒙内核源码分析(gn应用篇) | gn语法及在鸿蒙的使用 | 51.c.h.o 编译构建相关篇为: v50.xx 鸿蒙内核源码分析(编译环境篇) | 编译鸿蒙 ...

  9. 鸿蒙内核源码分析(ELF格式篇) | 应用程序入口并不是main | 百篇博客分析OpenHarmony源码 | v51.04

    百篇博客系列篇.本篇为: v51.xx 鸿蒙内核源码分析(ELF格式篇) | 应用程序入口并不是main | 51.c.h.o 加载运行相关篇为: v51.xx 鸿蒙内核源码分析(ELF格式篇) | ...

  10. 鸿蒙内核源码分析(编译环境篇) | 编译鸿蒙看这篇或许真的够了 | 百篇博客分析OpenHarmony源码 | v50.06

    百篇博客系列篇.本篇为: v50.xx 鸿蒙内核源码分析(编译环境篇) | 编译鸿蒙防掉坑指南 | 51.c.h.o 编译构建相关篇为: v50.xx 鸿蒙内核源码分析(编译环境篇) | 编译鸿蒙防掉 ...

随机推荐

  1. Tarjan学习笔寄

    tarjan算法 参考博客: https://www.cnblogs.com/nullzx/p/7968110.html https://www.cnblogs.com/ljy-endl/p/1156 ...

  2. Spring事务传播之嵌套调用

    文章目录 前言 7种传播方式 注解式事务 事务的方法之间的调用 注意事项 前言 最近在使用Spring框架时遇到了一些问题,主要是Spring的事务传播问题,一个不带事务的方法调用带事务的方法,有时候 ...

  3. JavaScript原生兼容大全-持续更新

    JavaScript兼容-持续更新 1.css非行内样式操作 // currentStyle用于IE低版本 getComputed用于主流浏览器 // element 目标元素 attribute 目 ...

  4. Node + Express 后台开发 —— 上传、下载和发布

    上传.下载和发布 前面我们已经完成了数据库的增删改查,在弄一个上传图片.下载 csv,一个最简单的后台开发就已完成,最后部署即可. 上传图片 需求 需求:做一个个人简介的表单提交,有昵称.简介和头像. ...

  5. RestTemplate发送get请求并携带请求头

    //设置请求头 HttpHeaders headers = new HttpHeaders(); headers.add("X-Access-Token", huaWenToken ...

  6. 2020-10-11:一条sql语句执行时间过长,应该如何优化?从哪些方面进行优化?

    福哥答案2020-10-11:#福大大架构师每日一题# 简单回答:执行计划调优.语句调优.索引调优.设计调优.业务调优. 中级回答:时间有限,回答得不全面.1.执行计划调优熟读执行计划,十大参数. 2 ...

  7. 2022-05-28:某公司计划推出一批投资项目。 product[i] = price 表示第 i 个理财项目的投资金额 price 。 客户在按需投资时,需要遵循以下规则: 客户在首次对项目 pr

    2022-05-28:某公司计划推出一批投资项目. product[i] = price 表示第 i 个理财项目的投资金额 price . 客户在按需投资时,需要遵循以下规则: 客户在首次对项目 pr ...

  8. python 之路,致那些年,我们依然没搞明白的编码

    摘自:金角大王https://www.cnblogs.com/alex3714/articles/7550940.html 本节内容 编码回顾 编码转换 Python的bytes类型 编码回顾 在备编 ...

  9. Python从0到1丨了解图像形态学运算中腐蚀和膨胀

    摘要:这篇文章将详细讲解图像形态学知识,主要介绍图像腐蚀处理和膨胀处理. 本文分享自华为云社区<[Python从零到壹] 四十七.图像增强及运算篇之腐蚀和膨胀详解>,作者: eastmou ...

  10. Must use destructuring props assignmenteslint

    eslint 检测提示Must use destructuring props assignmenteslint 使用对象结构就可以解决了