cocos creator 循环依赖导致编辑器异常

  • Creator 版本:2.0.9

  • 目标平台: N/A 编译后都可以正常运行,仅仅是场景编辑时报错

  • 详细报错信息,包含调用堆栈:

  • 重现方式:循环依赖就可以重现,我会提供demo和详细的代码错误

  • 出现概率:100%

现象:
在层级管理器中点击特定节点(我提供的Demo中就是GameNode节点),控制台会报错:

getComponent: Type must be non-nil
    at Object.cc.errorID (D:\Program Files (x86)\CocosCreator_2.0.9\resources\engine\bin\.cache\dev\cocos2d\core\CCDebug.js:167:16)
    at getConstructor (D:\Program Files (x86)\CocosCreator_2.0.9\resources\engine\bin\.cache\dev\cocos2d\core\utils\base-node.js:29:20)
    at cc_Node.getComponent (D:\Program Files (x86)\CocosCreator_2.0.9\resources\engine\bin\.cache\dev\cocos2d\core\utils\base-node.js:399:35)
    at HTMLElement.query-node-info (D:\Program Files (x86)\CocosCreator_2.0.9\resources\app.asar\editor\builtin\scene\panel\messages\scene-query.js:1:1331)
    at Object.e._dispatch (D:\Program Files (x86)\CocosCreator_2.0.9\resources\app.asar\editor-framework\lib\renderer\panel.js:1:1941)
    at EventEmitter.o.on.s (D:\Program Files (x86)\CocosCreator_2.0.9\resources\app.asar\editor-framework\lib\renderer\ipc.js:1:2917)
    at emitMany (events.js:127:13)
    at EventEmitter.emit (events.js:204:7)

在我的样例中,报错的原因是:

IndexController类有一个类型为GameNode的属性。
同时GameNode类有一个IndexController的属性。
通过关联绑定以后,产生了循环依赖。类似于Spring的依赖注入,属性的依赖注入是不会产生错误的,所以编译以后是可以正常运行的,但是Creator的编辑器就报错了。
这个错误导致:
1、添加的用户脚本如果有循环依赖那么该属性就不能关联节点。界面显示该属性为:Null ---- Create,点击Create按钮没反应。
2、一旦我通过GameNode的属性检查器删除了其IndexController属性的关联,那么GameNode的IndexController属性就会变成:Null ---- Create,点击Create按钮没有任何反应。

以下是我使用开发人员工具跟的CocosCreator源码,便于开发人员修复这个问题:
通过electron提供的Chrome开发人员工具,我检查了这个有问题的属性:

<ui-node class="flex-1" type="undefined" typename="IndexController" droppable="node" tabindex="-1"></ui-node>

发现其type是undefined
然后通过跟踪vue的源码。。找到pushWatcher。。。找到dumpNode。。。找到__attr__属性中IndexController的没有任何default和type的信息(IndexController$_$type=null)(在attribute.js中的setClassAttr方法:proto[propName + DELIMETER + key] = value;):

BugTest\temp\quick-scripts\assets\Script\Node\GameNode.js的15行打断点:

var IndexController_1 = require("../Controller/IndexController");

发现返回值IndexController_1的的值为:{__esModule: true}
而正常情况下应该返回:{__esModule: true, default: function},其中default应该是IndexController类的构造方法。

通过分析require内部的核心代码:在module.js的413行的Module._load方法(省略部分代码):

// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call `NativeModule.require()` with the
//    filename and return the result.
// 3. Otherwise, create a new module for the file and save it to the cache.
//    Then have it load  the file contents before returning its exports
//    object.
Module._load = function(request, parent, isMain) {
  ......
  var filename = Module._resolveFilename(request, parent, isMain);

  var cachedModule = Module._cache[filename];  // ①缓存中读取
  if (cachedModule) {
    return cachedModule.exports;
  }
  ......
  var module = new Module(filename, parent);
  ......
  Module._cache[filename] = module; // ⑤⑤⑤⑤⑤⑤⑤

  tryModuleLoad(module, filename);  //②加载该模块

  return module.exports;
};

实际的流程是这样:
相关代码为:
BugTest\temp\quick-scripts\assets\Script\Node\GameNode.js
BugTest\temp\quick-scripts\assets\Script\Controller\IndexController.js
为了便于说明,下面描述以文件名就是上面的绝对路径对应的文件
1、系统初始化,调用app.asar\editor\page\project-scripts.js中的loadCommon方法加载所有\temp\quick-scripts\assets\下的js文件(加载的顺序可能会不一样,我这边是先IndexController再加载GameNode)
2、系统使用Module._compile(IndexController中的代码, IndexController.js路径)对IndexController中的代码进行编译
3、调用IndexController.js内部的

__define(__module.exports, __require, __module); 
// !!!!!!方法中的部分代码
"use strict";
cc._RF.push(module, '1cc6bz/GJtB1qeEg6Qt50cB', 'IndexController', __filename);
// Script/Controller/IndexController.ts
Object.defineProperty(exports, "__esModule", {
	value: true
});
// 下面这部分就是根据ts文件的import、@ccclass、@property等注解自动转换的代码。
var GameNode_1 = require("../Node/GameNode");  //③③③③③③③③③③③③③③③
var _a = cc._decorator,
ccclass = _a.ccclass,
property = _a.property;
var IndexController = /** @class */(function (_super) {
	__extends(IndexController, _super);
	function IndexController() {
		var _this = _super !== null && _super.apply(this, arguments) || this;
		_this.gameNode = null;
		return _this;
		// update (dt) {}
	}
	// LIFE-CYCLE CALLBACKS:
	IndexController.prototype.onLoad = function () {};
	IndexController.prototype.start = function () {};
	__decorate([
			property(GameNode_1.default)
		], IndexController.prototype, "gameNode", void 0);
	IndexController = __decorate([
				ccclass
			], IndexController);
	return IndexController;
}(cc.Component));
exports.default = IndexController; // ⑥⑥⑥⑥⑥

cc._RF.pop();

4、当执行到上面③③③③的地方,又会去加载GameNode.js
5、在require(‘GameNode.js’)的内部,会调用Module._compile(GameNode中的代码, GameNode.js路径)对GameNode.js中的代码进行编译
6、同样会执行到GameNode.js中的__define()方法(差异点就是其中④④④处的代码):

"use strict";
cc._RF.push(module, 'e00f6YQPKZJAbzKscQyGhyI', 'GameNode', __filename);
// Script/Node/GameNode.ts
Object.defineProperty(exports, "__esModule", {
	value: true
});
var IndexController_1 = require("../Controller/IndexController"); // ④④④④④④④④
var _a = cc._decorator
  , ccclass = _a.ccclass
  , property = _a.property;
var GameNode = /** @class */
(function(_super) {
	__extends(GameNode, _super);
	function GameNode() {
		var _this = _super !== null && _super.apply(this, arguments) || this;
		_this.indexController = null;
		return _this;
		// update (dt) {}
	}
	// LIFE-CYCLE CALLBACKS:
	GameNode.prototype.onLoad = function() {}
	;
	GameNode.prototype.start = function() {}
	;
	__decorate([property(IndexController_1.default)], GameNode.prototype, "indexController", void 0); //⑦⑦⑦⑦⑦
	GameNode = __decorate([ccclass], GameNode);
	return GameNode;
}(cc.Component));
exports.default = GameNode;

cc._RF.pop();

由于GameNode又依赖了IndexController,因此在④处我们可以看到又require(IndexController)了。
7、④处的代码内部会调用到①处的Module._load(IndexController)方法,从缓存中获取cachedModule,此时IndexController的cachedModule已经存在了,因为Module._load()中⑤⑤⑤⑤是先将module放进了缓存再加载的,开发者其实已经很小心了,因为如果不这么写,循环依赖就直接死递归了。但是这样的结果就导致了④④④处获取的是尚未加载完毕的IndexController,因为IndexController的__define()方法必须执行⑥⑥⑥,也就是exports.default = IndexController;赋值以后才算执行完毕!!!!!!!
8、④④④的IndexController_1 拿到的是没有加载完毕的IndexController对象,因此⑦⑦⑦处使用IndexController_1.default获取到的就是null,
然后导致最终编辑器中拿到的类型就是undefined。

虽然有些人肯定会说,你不要循环依赖啊,确实,去掉循环依赖确实可以解决这个问题,但是循环依赖的场景真的很多啊,就好像我们开发代码,类A引用类B,类B又引用了类A,只要不是构造函数相互依赖:只是属性相互依赖肯定不会有问题啊,比如Spring的依赖注入就允许有依赖。原理和cocos_creator最终的运行时实现一样(虽然编辑器报错了,但是代编译出来的代码最终执行的时候是肯定OK的):先创建对象,然后再依赖注入。而不是根据import去递归加载。

cocos的开发人员也可以考虑使用这一思路去修改,想办法让import部分的require代码延迟执行,即:先把一个IndexController加载完毕exports.default = IndexController; // ⑥⑥⑥⑥⑥,然后再去
var GameNode_1 = require("../Node/GameNode");
进而

_decorate(
    property(GameNode_1.default)
], IndexController.prototype, "gameNode", void 0)

是否就可以修复这个问题????
这样还可以解决空import的资源浪费问题,即:虽然ts代码import了其它类,但是代码中实际并没有用到,那么就不应该去加载import的类,代码中还是会经常出现这种情况的,Java的import也是这么处理的,即:只有当类真正被用到的时候才会去加载这个类,如果仅仅是import,即使是import的不存在的类,运行是也不会报错(编译时会)。

BugTest.zip (8.2 KB)

见文档:属性延迟定义

先看下我上传的Demo吧。
如果是使用cc.Class代码创建类,确实是应该按照你回复的,否则会直接报错。

但是我提的是编辑器的问题,而且正常构建以后可以正常运行不会报错。

Demo中的MainScene是通过编辑器正常关联起来的。

如果说为了解决编辑器报错的问题(编辑器报错,最后的程序不会报错!)把所有编辑器自动创建的使用装饰器自动管理的代码全部改为手动cc.Class、然后再通过属性延迟加载来让编辑器不报错是不是有点南辕北辙了?

谢谢,受你的启发,解决了:
原来由编辑器动态生成的使用装饰器的属性也可以使用属性延迟定义,建议官方做改进吧:

1、编辑器对生成的代码进行处理的时候统一使用延迟加载的版本,以提升编辑器的容错性。
2、如果都做延迟定义开发难度太大,那么请编辑器本身增加对此类情况做检测,给出一个友好的提示,而不是直接报一个错误,然后相关功能都异常,对于普通开发人员来说,如果不是深入研究,根本不知道错误的原因是什么。

Mark

Mark 遇到同样的问题

:neutral_face:川子 你好强大