什么是 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. Ununtu服务器安装Nginx与PHP

    Ununtu服务器安装Nginx与PHP 1. 安装Nginx: 1.1 sudo apt update sudo apt install nginx 验证结果,使用命令: sudo systemct ...

  2. java项目 学生成绩管理系统 (源码+数据库文件)

    ​ 需要的私信我 备注来意:项目名称 来了就点个赞再走呗,即将毕业的兄弟有福了 文章底部获取源码 java项目  学生成绩管理 (源码+数据库文件)技术框架:java+springboot+vue+m ...

  3. API Gateway vs Load Balancer:选择适合你的网络流量管理组件

    本文从对比了 API Gateway 和 Load Balancer 的功能区别,帮助读者更好地了解他们在系统架构中扮演的角色. 作者陈泵,API7.ai 技术工程师. 原文链接 由于互联网技术的发展 ...

  4. Typecho<=1.2.0 存储型XSS 复现

    Typecho<=1.2.0 存储型XSS 影响版本 漏洞影响版本:Typecho <= 1.2.0 漏洞复现 cookie.js // 定义一个全局变量 website,值为一个具体的网 ...

  5. 【LeetCode动态规划#12】详解买卖股票I~IV,经典dp题型

    买卖股票的最佳时机 力扣题目链接(opens new window) 给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格. 你只能选择 某一天 买入 ...

  6. vue导入Excel数据并展示成表格

    前言: 用到的库参考链接: FileReader:https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader    这个在之前的下载exce ...

  7. csp-s2022游记

    ## 10.29### 民间数据:洛谷 $95+85+60+44=284$  infoj $90+40+40+44=214$  **输麻了**### 赛时经历开考前发现前面坐着 Qiuly,好可怕.开 ...

  8. java镜子之反射篇

    文章目录 注解 内置注解 元注解 反射 类的初始化 类加载器 双亲委派机制 反射方法的使用 调用类的方法.成员变量.构造器等 总结 注解和反射是Java中非常重要的知识,一些优秀开源的框架都是大量运用 ...

  9. 关于 Bash 脚本中 Shebang 的趣事

    哈喽大家好,我是咸鱼 不知道小伙伴们在写 Bash 脚本或者说看别人的 Bash 脚本的时候有没有注意过脚本的第一行 #!/bin/bash Bash 脚本的第一行往往以 #! 开头,这一行称作 sh ...

  10. 2021-07-17:一个不含有负数的数组可以代表一圈环形山,每个位置的值代表山的高度。比如, {3,1,2,4,5}、{4,5,3,1,2}或{1,2,4,5,3}都代表同样结构的环形山。山峰A和山

    2021-07-17:一个不含有负数的数组可以代表一圈环形山,每个位置的值代表山的高度.比如, {3,1,2,4,5}.{4,5,3,1,2}或{1,2,4,5,3}都代表同样结构的环形山.山峰A和山 ...