[bug 记录]:循环引用的问题

  • Creator 版本: 3.6.2 - 3.7.0

  • 目标平台: 编辑器、chorme

  • 重现方式:

// a.ts

import c from "./c";

export class a {
	func() {
		c.get(null as any);
	}
}
export default a;

// b.ts

import { a } from "./a";
import c from "./c";

export class b extends a {
	func(): any {
		c.get(null!);
	}
}

export default b;

// c.ts

import b from "./b";

export class c {
	get(...args_as_: any[]): any {
		return null!;
	}

	close(args_: any): any {
		return new b();
	}
}

export default new c();
  • 首个报错:

  • 之前哪个版本是正常的:

  • 手机型号:

  • 手机浏览器:

  • 编辑器操作系统: windows

  • 重现概率: 100%

排查了很久,按照 es6 的规则难道不是只有在模块加载时使用了循环引用的导入才会报错吗?

我这函数在模块加载时都没调就在编辑器报错了

大概知道问题了,b 导入 a,a 此时导入了 c 造成循环引用返回空,b 继承导入的空值 a 就会报错

推荐个包,可以查找循环依赖,madge - npm

静态查找只能起个参考作用,如果知道更好的可以推荐下

2赞

分享一个使用动态模块解决问题的办法,正常使用动态模块都是 (await import("../xxx")).default 或者 (await module).default 这样的使用方式

而用代理可以改成下面的方式,返回的数据类型直接是模块导出类型,但是内在还是一个动态模块

ZPP%5}{5@IS

代码

import instance_base from "./instance_base";
import log from "./log";

/** 动态模块(用以解除循环引用) */
class dynamic_module extends instance_base {
	/**
	 * 获取模块默认导出
	 * @param module_ 动态模块
	 * @returns
	 */
	// @ts-ignore
	default<T extends Promise<any>>(module_: T): Awaited<T>["default"] {
		/** 模块导出表 */
		let module_export_tab: any;

		module_.then((v) => {
			module_export_tab = v;
		}) as any;

		return new Proxy(Object.create(null), {
			get: (target, key) => {
				if (module_export_tab) {
					return module_export_tab["default"][key];
				}
				log.error("模块未加载完成");
				return null;
			},
		});
	}

	/**
	 * 获取模块所有导出
	 * @param module_ 动态模块
	 * @returns
	 */
	// @ts-ignore
	all<T extends Promise<any>>(module_: T): Awaited<T> {
		/** 模块导出表 */
		let module_export_tab: any;
		/** 模块导出代理表 */
		const module_export_proxy_tab = {};

		module_.then((v) => {
			module_export_tab = v;
		}) as any;

		return new Proxy(Object.create(null), {
			get: (target, key) => {
				if (module_export_proxy_tab[key] === undefined) {
					module_export_proxy_tab[key] = new Proxy(Object.create(null), {
						get: (target2, key2) => {
							if (module_export_tab) {
								return module_export_tab[key][key2];
							}
							log.error("模块未加载完成");
							return null;
						},
					});
				}
				return module_export_proxy_tab[key];
			},
		});
	}
}

export default dynamic_module.instance();

我现在已经极少遇到循环引用了,我发现只要多写几个类就能避免大部分循环引用的情景 :sweat_smile:

很多时候循环引用的来源是把继承关系、常量和工厂写在了一个类里了,就特别容易循环

同为新手也十分担心循环引用问题。感谢分享。

我们现在导入脚本时,会提示循环引用的警告的。为什么还需要借助第三方 madge - npm 呢?

因为只警告了哪里导入失败,整个循环链是什么都没办法确定

循环引用是一个js里面很麻烦的问题,往往发现的比较晚,然后去改的时候发现要改动很多地方或者改不了,容易出现新的bug,最好在写完一大堆代码后可以手动执行一下检查,开发时发现问题然后解决

instance_base这个类的代码能展示一下吗?试一下你的方法

https://muzzik.gitee.io/博客/笔记/编程语言/Typescript/类型安全的单例基类.html

你也可以直接 export default new xxx;

好的,感谢!

单独复制了方法来用 返回的是一个空的代理对象 是我使用方式有问题吗

按照自己理解改了下 也只能返回一个resolved状态的promise对象

而用代理可以改成下面的方式,返回的数据类型直接是模块导出类型,但是内在还是一个动态模块

所以怎么才能直接返回模块导出的类型呀


/**
 * 获取导出模块默认成员方法(动态加载模块以解决循环引用问题)
 * @param importer 使用import()动态导入模块
 */
function getDefaultExport<T extends Promise<any>>(importer: T) {
    //创建一个空的默认导出模块对象
    let module: { default: Awaited<T>['default'] } = { default: null };
    //创建代理对象
    let proxy = new Proxy(module, {
        get: (target, key) => {
            //返回动态加载的默认模块
            target[key] = importer.then((exports: Awaited<T>) => {
                return exports['default'];
            });
            return target[key];
        }
    });
    return proxy.default;
}

你这都改的面目全非了,所以不要改你没看懂的代码

解决循环依赖把一个传入另一个不就行了。

import 加了 “.ts”, 因为我用的 deno 测试的。

b.ts

import { a } from "./a.ts";
import c from "./c.ts";

export class b extends a {
	func(): any {
		c.get(null!);
	}
}

c.initClassB(b);

export default b;

c.ts

import type b from "./b.ts";

interface Ctor<T> extends Function {
    new(...args: any[]): T
}

export class c {
    private clzB!: Ctor<b>;
    initClassB(clzB: Ctor<b>) {
        this.clzB = clzB;
    }

    get(...args_as_: any[]): any {
        return null!;
    }

    close(args_: any): b {
        return new this.clzB();
    }
}

export default new c();

事实上你这样改起来更复杂和不容易理解