什么是 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. SpringBoot 使用 Sa-Token 完成权限认证

    一.设计思路 所谓权限认证,核心逻辑就是判断一个账号是否拥有指定权限: 有,就让你通过. 没有?那么禁止访问! 深入到底层数据中,就是每个账号都会拥有一个权限码集合,框架来校验这个集合中是否包含指定的 ...

  2. vue3.0

    https://www.yuque.com/gdnnth/vue-v3 http://www.liulongbin.top:8085/#/ https://www.yuque.com/woniuppp ...

  3. Prometheus-Operator使用ServiceMonitor监控配置时遇坑与解决总结

    摘要 本文范围: Prometheus-Operator & kube-prometheus 安装:以及在解决使用ServiceMonitor时遇到的坑. Prometheus Operato ...

  4. Microsoft Loop初体验

    目前AI copilot无法使用. 问题 图片 在设置中可以打开实验选项 简单开箱使用 很多人说微软的loop竞品是notion,那么作为卡片盒双链笔记软件,最热门的应用当然是notion.从loop ...

  5. 飞桨Paddle动转静@to_static技术设计

    一.整体概要 在深度学习模型构建上,飞桨框架支持动态图编程和静态图编程两种方式,其代码编写和执行方式均存在差异: 动态图编程: 采用 Python 的编程风格,解析式地执行每一行网络代码,并同时返回计 ...

  6. golang在编程语言排行榜上排名第10,请不要说golang已死。

    四月头条:编程语言 Zig 进入 TIOBE 指数前 50 名 最近,我们讨论了高性能编程语言的出现.由于需要处理的数据量越来越大,这些编程语言正在蓬勃发展.因此,C 和 C++ 在前十名中表现良好, ...

  7. 2020-12-02:mysql中,一张表里面有 ID 自增主键,当 insert 了 17 条记录之后,删除了第 15,16,17 条记录,再把 Mysql 重启,再 insert 一条记录,这条记

    2020-12-02:mysql中,一张表里面有 ID 自增主键,当 insert 了 17 条记录之后,删除了第 15,16,17 条记录,再把 Mysql 重启,再 insert 一条记录,这条记 ...

  8. 2022-01-10:路径交叉。给你一个整数数组 distance 。 从 X-Y 平面上的点 (0,0) 开始,先向北移动 distance[0] 米,然后向西移动 distance[1] 米,向南

    2022-01-10:路径交叉.给你一个整数数组 distance . 从 X-Y 平面上的点 (0,0) 开始,先向北移动 distance[0] 米,然后向西移动 distance[1] 米,向南 ...

  9. linux中使用jenkins自动部署前端工程

    1.去年在自己的服务器上安装了jenkins,说用来自己研究一下jenkins自动化部署前端项目,jenkins安装好了,可是一直没管,最近终于研究了一下使用jenkins自动化部署,以此记录下来. ...

  10. NOIP2021游记

    前言: 今年我是以初中生的身份参加的 NOIP,不计奖,不排名,就去试试水. 考得也不好,幸好没计奖. 正文: 早上 7 点: 到LNBS,在旁边吃了早饭,很好吃. 早上 8 点: 校门口照相,然后进 ...