- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
我一般是以使用角度作为切入点查看分析源码,例子中用到什么类,什么方法,再入源码.
高屋建瓴的角度咱也做不到啊,毕竟水平有限 。
pixijs 的源码之前折腾了半天都运行不起来,文档也没有明确说明如何调式 我在 github 上看到过也有歪果仁在问如何本地调式最后他放弃了转用了别的库... 还有就是 npm 在我们迷之大陆确实不太友好 。
源码 pixijs 7.3.2 版下载地址 https://github.com/pixijs/pixijs/tree/v7.3.2 。
npm 8.19.2 。
Node.js v16.18.0 。
安装命令 。
npm install 。
运行命令 。
npm start 。
serve 静态服务器全局安装 。
https://www.npmjs.com/package/serve 。
项目源码根目录下有个主包的 package.json name 是 pixi.js-monorepo 。
从名字可以看出来,这个项目是用 monorepo 方式来组织管理代码的 。
在 rollup.config.mjs 配置文件内配置有一个方法:
await workspacesRun.default({ cwd: process.cwd(), orderByDeps: true }, async (pkg) =>
{
if (!pkg.config.private)
{
packages.push(pkg);
}
});
主要作用就是遍历所有子项目,将非私有项目加入到 'packages' 数组变量中,然后分析依赖关系再打包输出 。
PixiJS 源码在 packages 目录 。
/packages 目录下每一个 "大类" 模块都是单独的项目 。
每一个 "大类" 都有自己单独 package.json 文件, 在 package.json 文件内指定自己的依赖 。
比如 app 模块的 package.json 文件内指定了依赖:
"peerDependencies": {
"@pixi/core": "file:../core",
"@pixi/display": "file:../display"
}
其中的 src 就是此"大类"源码目录,与 src 同级的 test 是此"大类"的测试用例 。
调式过程中我发现编译真的挺慢的 ... 。
为了调式大致需要以下几步 。
<script src="/bundles/pixi.js/dist/pixi.js"></script>
serve .
完成以上步骤后,你就可以在 /packages 目录下的任意源码内添加 console.log 或 debugger 进行源码调式了 。
相信上面步骤最大的挑战是 npm install T_T.
源码中添加一个 console.log 看看能不能成功输出先 。
测试的 example/simple.html 文件如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title> 最简单的例子 </title>
<style type="text/css">
*{
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<script src="/bundles/pixi.js/dist/pixi.js"></script>
<script type="text/javascript">
const app = new PIXI.Application({ width: 800, height: 600 });
document.body.appendChild(app.view);
const rectangle = PIXI.Sprite.from('logo.png');
rectangle.x = 100;
rectangle.y = 100;
rectangle.anchor.set(0.5);
rectangle.rotation = Math.PI / 4;
app.stage.addChild(rectangle);
app.ticker.add(() => {
rectangle.rotation += 0.01;
});
</script>
</body>
</html>
以上例子中实现的功能:
如果你在本地服务器环境下打开 simple.html 文件,你将会看到一个旋转的 logo.png 。
这里用到了二个类 Application、Sprite 。
Application 类是 PixiJS 的入口类在 /packages/app/src/Application.ts 。
源码中已说明这个类是创建 PixiJS 应用的便捷类,这个类会自动创建 renderer, ticker 和 root container 。
Application.ts 源码的 constructor 构造方法内添加个 console.log 试试能不能成功输出 。
Application.ts 71-85 行 。
constructor(options)
{
// The default options
options = Object.assign({
forceCanvas: false,
}, options);
this.renderer = autoDetectRenderer(options);
console.log('hello', 88888);
// install plugins here
Application._plugins.forEach((plugin) =>
{
plugin.init.call(this, options);
});
}
移除掉 typescript 类型的源码果然看起来眉清目秀一些 。
成功的关键要注意两点 :
先 npm start 项目, 作用是 watch 源码变化自动化编译到 bundles 目录 。
确保你是在本地服务器环境下打开网页就像这样访问 http://localhost:3000/examples/simple 。
打开网页调式器如果输出 hello 88888 就说明成功可以调式源码了 。
Amazing! 。
Application 的构造方法就做了两件事,创建渲染器 (renderer) 和 初始化插件 (plugin) 。
renderer 是 PixiJS 的渲染器,渲染器会根据浏览器环境自动选择渲染方式,如 WebGL、Canvas 。
_plugins 静态属性是一个用于存放插件数组 。
Application 类本身的其它主要属性:
在例子代码中 app.ticker ticker 对象即是 /packages/ticker/TickerPlugin.ts "定时器" 插件, 后面会深入其源码细节 。
autoDetectRenderer 用于自动判断使用哪种方式渲染,如 WebGL、Canvas 。
/packages/core/src/autoDetectRenderer.ts 第 41-52 行 。
export function autoDetectRenderer<VIEW extends ICanvas = ICanvas>(options?: Partial<IRendererOptionsAuto>): IRenderer<VIEW>
{
for (const RendererType of renderers)
{
if (RendererType.test(options))
{
return new RendererType(options) as IRenderer<VIEW>;
}
}
throw new Error('Unable to auto-detect a suitable renderer.');
}
显然, 通过循环检测所有的 renderers 渲染器类型 与构造函数传递过来的 options 参数进行检测返回符合条件的渲染器 。
RendererType.test 就是渲染器的一个检测方法 。
而 renderers 数组就定义在了第 29 -32 行 。
const renderers: IRendererConstructor<ICanvas>[] = [];
extensions.handleByList(ExtensionType.Renderer, renderers);
这里又用到了一个叫 extensions 的全局对象,这个全局对象顾名思议,就是用来管理所有扩展插件的,嗯,所以渲染器也是一个 extension 。
扩展插件源码文件 /packages/extensions/src/index.ts 。
官方的插件的类型有这些:
'renderer'
'application'
'renderer-webgl-system'
'renderer-webgl-plugin'
'renderer-canvas-system'
'renderer-canvas-plugin'
'asset'
'load-parser'
'resolve-parser'
'cache-parser'
'detection-parser
具体插件类或对象都是注册到对应的类型下的 。
类先通过 extensions 全局对象的 handleByList 或 handleByMap 方法注册插件类型 。
当真正添加插件时,调用的是 extensions 全局对象的 add 方法插件就会添加到对应的插件类型下 。
比如 TikerPlugin.ts ResizePlugin.ts 就是注册到了 'application' 类型下 。
又比如 load 相关的插件就注册到了 'load-parser' 类型下 。
最后具体的插件是注册到具体类的 _plugins 属性上比如: Application._plugins 。
在 /packages/extensions/src/index.ts 文件中第 240-265 行,找到 handleByList 方法 。
在 extensions/index.ts 244 行加个 console.log 打印一下:
handleByList(type: ExtensionType, list: any[], defaultPriority = -1)
{
return this.handle(
type,
(extension) =>
{
if (list.includes(extension.ref))
{
return;
}
console.log(extension.ref);
list.push(extension.ref);
list.sort((a, b) => normalizePriority(b, defaultPriority) - normalizePriority(a, defaultPriority));
},
(extension) =>
{
const index = list.indexOf(extension.ref);
if (index !== -1)
{
list.splice(index, 1);
}
}
);
},
输出:
图 1-1 。
可以看到输出了一堆 class 和 对象 (实现了 ExtensionFormat "接口" 的对象), 只知道有这些,现在还不知道具体干啥 。
把 handleByList 方法的 type 和 list 也打印出来看看 。
图 2-2 。
可以看到每个插件类型都可以拥有多个 extention 数组 。
再看看它的 add 方法 。
在 extensions/index.ts 152 - 175 行 。
add(...extensions: Array<ExtensionFormatLoose | any>)
{
extensions.map(normalizeExtension).forEach((ext) =>
{
ext.type.forEach((type) =>
{
const handlers = this._addHandlers;
const queue = this._queue;
// 如果添加的插件还没有插件类型,就放到 _queue 内存起来
if (!handlers[type])
{
queue[type] = queue[type] || [];
queue[type].push(ext);
}
else
{
// 如果已经有相应的插件类型了,就添加到对应插件类型下
handlers[type](ext);
}
});
});
return this;
},
可以看到它接收一个插件数组对象 'extensions' 将传进来的对象进行 '插件对象标准化'后,该对象拥有 type, name, priority, ref 这些属性 。
interface ExtensionFormatLoose
{
type: ExtensionType | ExtensionType[];
name?: string;
priority?: number;
ref: any;
}
PixiJS 这种插件方式的设计就是为了解耦,方便管理和扩展更多插件 。
逻辑如下:
Application.ts 在全局 extensions 对象中注册插件类型并传入用于存储插件的数组 extensions.handleByList(ExtensionType.Application, Application._plugins),
TickerPlugin.ts 在 extensions 注入至对应的 Application 类型插件数组 extensions.add(TickerPlugin),
Application.ts 在实例化时会它所有插件的 init 方法,将插件也“实例化” 。
其它插件或自定义插件实现注册与调用同样适用,不需要再进入 Application.ts 修改逻辑实现解耦 。
我们以 /packages/ticker/TickerPlugin.ts 时钟插件举例 。
在 tickerPlugin.ts 文件的最后一行有一句 extensions.add(TickerPlugin),
这一句就是将 TickerPlugin 对象添加到了Application 类的 _plugins 数组 。
TickerPlugin.ts 35 行标明了这个扩展属于 Application 类 。
static extension: ExtensionMetadata = ExtensionType.Application;
仔细观察 TickerPlugin.ts 文件,发现它并没有 constructor 构造函数 。
而是有一个公开的 init 函数,这个函数就是插件的入口函数,它会被 Application 构造函数调用并将 this 指向了 Application 对象本身 。
所以在 init 函数内访问的 this 就是 Application 对象本身 。
我们都知道与浏览器的自动更新渲染方式不同,在 canvas 更新渲染画面都是通过手动擦掉旧的像素重新绘制新像素实现的 。
时钟插件很大一部分工作就是用于管理渲染更新的,它属于 Application 类的扩展插件. 。
在 TickerPlugin.ts 的 init 方法内,文件第 115 行 。
this.ticker = options.sharedTicker ? Ticker.shared : new Ticker();
即说明实例化 Application 后自动创建了一个 Ticker 实例, sharedTicker 看名字就知道是个共享的时钟 。
共有三种 ticker: sharedTicker, systemTicker, 普通 ticker 。
只要 this.ticker 被赋值,旧的 Application render 方法会删除并添加一个新的 render 回调进入 ticker 队列, 还有个 UPDATE_PRIORITY.LOW 用来管理回调队列的优先级 。
TickerPlugin.ts 的 init 方法内,文件第 57 - 75 行:
Object.defineProperty(this, 'ticker',
{
set(ticker)
{
if (this._ticker)
{
this._ticker.remove(this.render, this);
}
this._ticker = ticker;
if (ticker)
{
ticker.add(this.render, this, UPDATE_PRIORITY.LOW);
}
},
get()
{
return this._ticker;
},
});
让我们进入 Ticker.ts 类看看 。
渲染相关的回调通过 Ticker.add 和 Ticker.addOnce 添加加到 Ticker 类中 。
顾名思义 addOnce 就是一次性的回调,我们只要理解 add 方法就可以了 。
Ticker.ts 198 - 201 行:
add<T = any>(fn: TickerCallback<T>, context?: T, priority = UPDATE_PRIORITY.NORMAL): this
{
return this._addListener(new TickerListener(fn, context, priority));
}
渲染回调还用 TickerListener.ts 类,包装了一下,包装的主要目的是将相应的渲染回调函数根据 priority 权重组成一个回调 “链表队列” 。
priority 权重在 /packages/ticker/const.ts 定义 。
TickerListener.ts 类主要的两个方法: emit 触发函数和 connect 连接函数 。
/packages/ticker/TickerListener.ts 97 - 106 行 connect 函数:
connect(previous: TickerListener): void
{
this.previous = previous;
if (previous.next)
{
previous.next.previous = this;
}
this.next = previous.next;
previous.next = this;
}
得结合 Ticker 类的 _addListener 一起看:
/packages/ticker/Ticker.ts 223 - 258 行:
private _addListener(listener: TickerListener): this
{
// For attaching to head
let current = this._head.next;
let previous = this._head;
// 如果还没有添过,就添加到 _head 后面
if (!current)
{
listener.connect(previous);
}
else
{
// priority 优先级从最高到最低
while (current)
{
if (listener.priority > current.priority)
{
listener.connect(previous);
break;
}
previous = current;
current = current.next;
}
// 如果还没有加入到链表中,则加入到链表尾部
if (!listener.previous)
{
listener.connect(previous);
}
}
this._startIfPossible();
return this;
}
可以看到通过 while 循环整个 this._head 存储的链表,根据 priority 权重找到需要插入的位置,然后插入到链表中.
如果没找到位置,则添加到链表最后 。
_addListener 函数最后还调用了 _startIfPossible 既而调用了 _requestIfNeeded 方法 。
_requestIfNeeded 即刻发起 this._tick “请求” 。
private _requestIfNeeded(): void
{
if (this._requestId === null && this._head.next)
{
// ensure callbacks get correct delta
this.lastTime = performance.now();
this._lastFrame = this.lastTime;
this._requestId = requestAnimationFrame(this._tick);
}
}
this._tick 函数定义在 Ticker.ts 的构造函数内 。
/packages/ticker/Ticker.ts 116 - 137 行 。
constructor()
{
this._head = new TickerListener(null, null, Infinity);
this.deltaMS = 1 / Ticker.targetFPMS;
this.elapsedMS = 1 / Ticker.targetFPMS;
this._tick = (time: number): void =>
{
this._requestId = null;
if (this.started)
{
// 此处触发回调函数,并传入 delta time
this.update(time);
// 回调函数执行后可能会影响 ticker状态,所以需要再次检查
if (this.started && this._requestId === null && this._head.next)
{
// 继续执行下一帧
this._requestId = requestAnimationFrame(this._tick);
}
}
};
}
_tick 函数就是每一帧都会执行 。
this._head 链表头部,为方便处理统一加一个虚拟头部节点 。
this.deltaMS 默认为 1/0.06 = 16.66666 刷新率 。
this.elaspedMS 帧间隔时间 。
即使你没有往画布中绘制任何图形,也会执行。不信你可以在 _tick 内添加一个 console.log 看看 。
当 _tick 触发时调用的就是 update 函数 。
/packages/ticker/Ticker.ts 369 - 442 行 。
update(currentTime = performance.now()): void
{
let elapsedMS;
// update 也可由用户主动触发
// 如果间隔时间是0或是负数不不需要触发通知回调
// currentTime
if (currentTime > this.lastTime)
{
// Save uncapped elapsedMS for measurement
elapsedMS = this.elapsedMS = currentTime - this.lastTime;
// cap the milliseconds elapsed used for deltaTime
if (elapsedMS > this._maxElapsedMS)
{
elapsedMS = this._maxElapsedMS;
}
elapsedMS *= this.speed;
// If not enough time has passed, exit the function.
// Get ready for next frame by setting _lastFrame, but based on _minElapsedMS
// adjustment to ensure a relatively stable interval.
if (this._minElapsedMS)
{
const delta = currentTime - this._lastFrame | 0;
if (delta < this._minElapsedMS)
{
return;
}
this._lastFrame = currentTime - (delta % this._minElapsedMS);
}
this.deltaMS = elapsedMS;
this.deltaTime = this.deltaMS * Ticker.targetFPMS;
// Cache a local reference, in-case ticker is destroyed
// during the emit, we can still check for head.next
const head = this._head;
// Invoke listeners added to internal emitter
let listener = head.next;
while (listener)
{
listener = listener.emit(this.deltaTime);
}
if (!head.next)
{
this._cancelIfNeeded();
}
}
else
{
this.deltaTime = this.deltaMS = this.elapsedMS = 0;
}
this.lastTime = currentTime;
}
额外小知识 。
对于需要高精度时间戳的动画或输入处理,performance.now() 可以提供比 Date.now() 更高的精度.
与 requestAnimationFrame 结合使用:
requestAnimationFrame 的回调函数接收一个高精度的时间戳作为参数,这个时间戳与 performance.now() 返回的时间戳是同步的.
因此,你可以使用 performance.now() 来与 requestAnimationFrame 回调中的时间戳进行比较或计算.
需要注意的是,performance.now() 返回的时间戳是相对于某个特定时间点的,而不是绝对的时间(如日期和时间)。因此,它主要用于测量时间间隔,而不是获取当前的日期和时间.
update 方法主要功能就是判断当前时间与上一次调用的时间差,如果大于最大间隔时间(需要更新一帧时)就执行回调链表 。
listener.emit(this.deltaTime),
注意 listener.emit() 执行后返回的是下一个回调函数,即 listener.next 以完成 while 循环 。
PixiJS Ticker 默认是开启的,EaselJS Ticker 直到有添加 Ticker 回调才开启 。
PixiJS Ticker 可被实例化,有构造函数,而 EaselJS Ticker 更像是一个全局对象 。
PixiJS Ticker 回调使用函数采用链表方式存储拥有可调节的权重, EaselJS Ticker 直接使用了 EventDispatcher “标准事件” 方式实现回调,回调使用数组存储没有权重可调节 。
PixiJS Ticker 使用 requestAnimationFrame 实现 tick,EaselJS Ticker 库较早,所以还支持 setTimeout 方式 。
这一章先介绍源码如何下载并搭建本地调式环境,然后用一个简单的例子来打印出调式信息 。
以 Appllication 类为入口进入源码, 了解了 PixiJS 的基本扩展插件机制 。
最后分析最重要的 Ticker 实现 。
说实话我在现实前端项目中从未用到过链表,很意外在分析PixiJS源码的时候居然发现 Ticker 回调是用链表实现的,look! 没用的知识又增加了! 。
上面 simple.html 例子中的 PIXI.Sprite 和 app.stage 还没有进入源码, 下一章先尝试进入 stage 这一部分,如果可以的话 Sprite 也过一遍 。
注:转载请注明出处博客园:王二狗Sheldon池中物 (willian12345@126.com) 。
最后此篇关于PixiJS源码分析系列:第一章从最简单的例子入手的文章就讲到这里了,如果你想了解更多关于PixiJS源码分析系列:第一章从最简单的例子入手的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我制作手机游戏,但我想为我的社交游戏制作一些简单的基于浏览器的客户端,以便我们可以更轻松地调试。最简单的引擎是什么? 我研究了一些 HTML5 引擎和 GWT,但我想听听社区的意见。我正在寻找一种能够
有一个有趣的例子,有人设法为一个只占用 1kb 的论坛创建了一个 PHP 脚本:http://www.nerdparadise.com/blogs/blake/6034/ 我想知道是否有类似的小脚本可
首先我要吐槽一下,看程序的过程中遇见了yield这个关键字,然后百度的时候,发现没有一个能简单的让我懂的,讲起来真tm的都是头头是道,什么参数,什么传递的,还口口声声说自己的教程是最简单的,最浅显易
我的proguard配置太糟糕了,我的游戏崩溃了,而且似乎不正常。在我弄清楚到底哪里出了问题之前,最简单,最安全的配置用于全部安装是什么?有没有办法使它仅更改变量名?或者只是混淆代码以使其更难阅读而不
我正在重构一些 C# 代码,其中一部分是重做一些引用,因为我们正在完全重做文件夹结构。我想做的就是进入 .csproj 或 .sln 文件并修改路径。 然而,一些引用文献有类似的路径 "../../.
免责声明:这是一个理论问题,目的是增加我的理解。我知道我总是可以使用像 JSON 库这样的工具来解决问题。 假设我想创建一个逗号分隔的值列表,这些值本身可能包含逗号。这些逗号需要先转义。假设我使用 .
我对编程完全陌生,我选择 Delphi 作为我想学习的编程语言。 我基本上想构建使用套接字填写和提交 Web 表单的工具,并且我希望它们也是多线程的。 我希望它们功能丰富且性能正确。 我并不急于这样做
我正在构建一个协作创作工具,该工具允许用户共同编辑信息空间,该信息空间是节点和链接的可视化。一个客户端应用程序中所做的更改需要反映到所有其他客户端中。由于它是可视化的,因此可能需要经常更新潜在的大数据
作为一家专门开发自定义 CMS 的公司,我们被要求在下一个项目中部署开源 CMS。 我们可以自由选择系统。对于熟悉 PHP5 中的 MVC 模型和 OOP 的团队,您会推荐什么? 有人告诉我Drupa
已结束。此问题正在寻求书籍、工具、软件库等的推荐。它不满足Stack Overflow guidelines 。目前不接受答案。 我们不允许提出寻求书籍、工具、软件库等推荐的问题。您可以编辑问题,以便
我完全是 Ember.js 菜鸟,需要了解后端注意事项,目前似乎很少有教程涵盖。对于快速原型(prototype)设计,最简单/最简单的后端设置是什么?我看到了一些 ember-rails 教程,但是
我正在寻找这种最简单、最简单的方法来启动 Java Web 服务。我曾经使用 Axis-1-on-Tomcat,但是对于 Axis 2,它变得太厚了。我正在寻找的一些偏好: 低内存占用 - 一个包含最
我多年来一直使用旧版 openGL 和 cocoa,但现在我正在努力过渡到 openGL 3.2。互联网上有几个例子,但它们都太复杂了(许多甚至在 XCode 5.1 下不再编译)。有人可以编写一个最
我正在构建一个简单的应用程序,它应该将开关/支票簿的状态存储 7 天。我遇到的唯一问题是我用来构建所述应用程序的网站不适合手机上的本地存储。没有通过本地存储或链接到在线数据库来存储开关/支票簿状态的良
关闭。这个问题是opinion-based .它目前不接受答案。 想要改进这个问题? 更新问题,以便 editing this post 可以用事实和引用来回答它. 关闭 7 个月前。 Improv
有时我需要 1 个用户类型元素的集合(或任何其他容器)并以这种方式创建它们: boost::assign::list_of(typeVariable).convert_to_container >()
我的页面上有一个 DropDownList 和一个 TextBox。当用户在 DropDownList 中选择“其他”选项时,我想在其右侧显示一个文本框。我不想使用传统的回发技术。我希望这种交互是在客
说到编码,我还很年轻,而且我听说过很多关于组织的事情。有些使用部分,有些使用 div,有些使用 div 作为按钮,其他使用 css 中的输入来更改它。作为一个喜欢让他的代码干净、简单易懂但又正确的人,
我需要将带有变量项的 ContentValues 转换为 JSON 字符串,我可以将其保存到数据库中,并在以后用作 HTTP 请求的正文。网络上的所有内容都只会以相反的方向进行转换。 最佳答案 每当您
我的电脑上安装了 Python 3.7。想用tensorflow,发现基本不支持3.7,所以想(也)安装Python 3.6。 关于如何做到这一点有什么建议吗?我是否必须卸载 3.7 并将其替换为 3
我是一名优秀的程序员,十分优秀!