EaselJS 源碼分析系列--第一篇

什麼是 EaselJS ?

image

事兒還得從 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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章