CocosCreator ts代码 循环引用 报错处理
循环引用原理

如图所示,如果你在 A类 需要引用 B类;而 B类 又恰好引用了 A类。那么就出现了循环引用的情况。

实际环境中,循环引用的链条结构并不明晰,也有可能出现上图的情况。
项目代码存在循环引用的情况,这可能会引起报错,也可能不会,这视情况而定。

说回标题,在 CocosCreator 进行开发的过程中,在不经意间,通常在你针对某个需求修改了一定量的代码之后,突然出现报错。
这是很恼人的,因为很多时候,你很难去判断具体是哪块修改引起的报错。
这个问题困扰了我一段时间,直到最近,在项目即将上线之际,宿命论般地再次爆发了循环引用报错。
曾经我用一些笨方法使报错不再出现,但这一次笨方法不再有效,我意识到我必须正面对抗了。
循环引用分析工具
最基本的,我们需要知道我们项目代码中都存在哪些循环引用情况。这样才能对症下药地去进行处理。
这里介绍一个工具 madge ,可以通过 npm -g install madge 安装。
生成报告
完成安装之后可以通过 madge --circular path/to/a.ts 这个格式的命令来列出所有与这个文件相关的循环引用情况。

阅读报告
以 第201条记录 为例。

这表示:
FightDlg.ts 引用了 EntryDlg.ts;
EntryDlg.ts 引用了 HeroChooseDlg.ts;
HeroChooseDlg.ts 引用了 FightDlg.ts (记录中省略了)
(大家如果感兴趣可以自己按照自己的报告记录去对应的文件中走一遍。)
ts文件选择
至于命令中应该选择哪个 ts文件。
可以参考着报错信息,如前面的报错,就可以尝试选择 ABuffCom.ts 来进行分析入口。
或则选择项目的入口文件。
常用命令
另外,我还习惯这样使用这个命令。
- 搭配 grep 进行过滤
madge --circular ABuffCom.ts | grep "ABuffCom"
- 搭配 > 输出到文件
madge --circular ABuffCom.ts > rlt.logmadge --circular ABuffCom.ts | grep "ABuffCom" > rlt.log
循环引用处理技巧
好的,根据报错,我们知道哪个文件出现循环引用,又通过工具知道了这些 循环引用链条 的细节,接下来就是具体地解决报错了。
好的我们碰到了一个由于循环引用导致的报错,对于报错我们要分辨两种报错:编译错误 和 逻辑错误:
- 编译错误:调用 tsc 编译 ts代码 时报错;在IDE编译项目代码时报错。
- 逻辑错误:成功编译代码,但在实际运行中报错。通常是 某个变量undefine 或则 调用栈栈满。
逻辑错误
问题重现
接下来我们重现一个场景,这个场景下的项目代码能够通过编译,但实际执行后会因为循环引用报错。
这个场景里包括了两个应用了单例模式的类,且这两个类互相引用。
// A.ts
import B from "./B";
export default class A {
private static _me: A;
public static me(): A {
if (!this._me) {
this._me = new A(B.me())
}
return this._me
}
constructor(private b: B) {
}
}
A.me()
// B.ts
import A from "./A";
export default class B {
private static _me: B;
public static me(): B {
if (!this._me) {
this._me = new B(A.me())
}
return this._me
}
constructor(private a: A) {
}
}
这种写法能够编译成功,但实际执行后会报以下错误:
(顺道说,就算把两个类写在同一个文件也是会报错的。)

这种情况下,A的单例在 构造 时需要用到B的单例,而B的单例在 构造 时又需要用到A的单例,从而在实际运行中引起 调用栈栈满 报错。
问题解决
这个问题有多种解决方法。
在这个场景中,我们要做的是解除两个类的单例在初始化中的依赖,我们可以通过将其中某个类实际的初始化从构造器中拆出单独的接口中实现。
以下是一种处理方式。
// A.ts
import B from "./B";
export default class A {
private static _me: A;
public static me(): A {
if (!this._me) {
this._me = new A()
}
return this._me
}
private _b: B;
constructor() {
}
public init(b: B){
this._b = b;
}
}
A.me()
A.me().init(B.me())
这种处理方式有两个前提条件:
- A 在构造器中并不需要使用 B。
- B 在构造器中并不需要 init() 后的A。
那其它的情况怎么办?那就需要我们按照实际情况分析、调整代码了,这里不深入讨论。
有时你只需要加些丑代码(我们不需要完美的代码,我相信);有时候则需要你重新考虑互相引用的类之间的关系。
总之经过努力,虽然两个类循环引用,但 代码能跑 就行。
编译错误
由于 CocosCreator 对项目代码的处理方式,很多时候就算按照我们的设计,代码在实际运行中就算不会有逻辑错误。它也会给出 编译报错。
如下图的报错 “Found possible circular reference”。

接下来我会介绍几个小技巧用来“规避”这种编译错误。
当然如果编译通过后的代码实际上确有逻辑错误,最终还是需要我们再花些力气去修复。
方法一、写在一个文件中
有时候,把两个互相引用的类写在一个文件就能消除编译报错。
方法二、从单例类入手
假设报错的文件是 P.ts。
通过 madge 分析得循环引用链条:P.ts > ... > foo.ts > SingletonMgr.ts > bar.ts > ... 。
其中 SingletonMgr.ts 采用了单例模式实现。
// SingletonMgr.ts
/**
* ... some code
*/
export default class SingletonMgr {
private static _me: SingletonMgr;
public static get me(): SingletonMgr {
if (!this._me) this._me = new SingletonMgr()
return this._me;
}
private constructor() { }
/**
* ... some code
*/
}
// foo.ts
/**
* ... some code
*/
export default class foo {
private _mgr: SingletonMgr;
constructor() {
this._mgr = SingletonMgr.me;
}
/**
* ... some code
*/
}
这种情况下,我们可以通过一些手段从 SingletonMgr.ts 切断该链条,如下。
例子
我们创建一个 CR.ts,其中用到了 import type 语法,import type 是用来协助进行类型检查和声明的,编译后并不会在代码中留下痕迹。
// CR.ts
import type SingletonMgr from "./SingletonMgr"
var _singletonMgr: SingletonMgr = null!;
export default {
SingletonMgr: _singletonMgr,
}
然后在 SingletonMgr.ts 的构造器中给 CR.SingletonMgr 赋值。
// SingletonMgr.ts
import CR from "./CR"
/**
* ... some code
*/
export default class SingletonMgr {
private static _me: SingletonMgr;
public static get me(): SingletonMgr {
if (!this._me) this._me = new SingletonMgr()
return this._me;
}
private constructor() {
CR.SingletonMgr = this;
}
/**
* ... some code
*/
}
最后修改 foo.ts 中对 SingletonMgr 单例 的获取。
// foo.ts
import CR from "./CR"
/**
* ... some code
*/
export default class foo {
private get _mgr(): SingletonMgr{
return CR.SingletonMgr
};
constructor() {
}
/**
* ... some code
*/
}
这样子处理后,foo.ts > SingletonMgr.ts 已经不复存在,从而断掉了 循环引用链条。
不过这种处理方式没办法无脑地采用,比如:foo.ts得在文件中其它代码获取 CR.SingletonMgr 之前完成 CR.SingletonMgr 的赋值。
但相信对于各位 CTO 这并不是什么难事,不再赘述。

浙公网安备 33010602011771号