EaselJS 原始碼分析系列--第一篇

2023-07-14 21:00:19

什麼是 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/